Compare commits

...

191 Commits

Author SHA1 Message Date
Shin Yamamoto 0a613a7339 Version 2.0.0 2020-10-01 08:26:01 +09:00
Shin Yamamoto b4a518fe3e Suppress 'brew update' error on travis ci 2020-09-30 21:53:47 +09:00
Shin Yamamoto 538e046368 Change SurfaceView.contentView to an optional type 2020-09-30 20:19:19 +09:00
Shin Yamamoto bb44d14370 Double lay out the surface view for uistackview's intrinsic behavior 2020-09-30 20:19:19 +09:00
Shin Yamamoto 230ec689d4 Prevent a crash in a device rotation 2020-09-22 15:20:42 +09:00
Shin Yamamoto c7605a94b9 Fix a content view layout using intrinsic anchor 2020-09-21 15:00:50 +09:00
Shin Yamamoto 864104de3e Recalculate state constraints in layout activation for intrinsic layout
Because sometimes UIView.systemLayoutSizeFitting() returns a different
size between an onscreen and offscreen view which includes UIStackView.

This behavior depends on UIStackView behavior and the lib can not match
the difference.
2020-09-21 15:00:50 +09:00
Shin Yamamoto a8e1cfa2bc Fix intrinsic layout 2020-09-21 15:00:50 +09:00
Shin Yamamoto 207dd27113 Fix Samples app 2020-09-21 15:00:50 +09:00
Shin Yamamoto 802b6f9b6b Ease time out conditions for ControllerTests.test_moveTo_bottomEdge() 2020-09-19 16:06:17 +09:00
Shin Yamamoto 0ff9d5dd9b ci: reduce the build time 2020-09-19 15:12:47 +09:00
Shin Yamamoto 6e509481bb Ease time out conditions in ControllerTests.test_moveTo() 2020-09-19 14:00:14 +09:00
Shin Yamamoto 054a21f3f8 ci: support xcode 12 and fix errors
- Since April 2020 Xcode 11 is required to submit an app so this drops
  Xcode 10.3(Swift 5.0) build.
- Specify `-workspace` for testing
- Travis CI doesn't have Xcode 11.7 environment.
2020-09-19 13:16:09 +09:00
Shin Yamamoto 18e739fc7b Merge branch 'release-1.7.6' into v2-dev 2020-09-19 11:24:39 +09:00
Shin Yamamoto ca7596e1ca Version 1.7.6 2020-09-19 11:09:30 +09:00
Shin Yamamoto 38103da2eb Update travis ci config 2020-09-05 14:46:23 +09:00
Shin Yamamoto 0c2275def1 Address test failures on iOS 10 and 11 2020-09-05 14:46:23 +09:00
Shin Yamamoto a0da1b99c0 Use BackdropView.dismissalTapGestureRecognizer to dismiss panels in Samples 2020-09-05 14:46:23 +09:00
Shin Yamamoto 427814839a Fix typo 2020-09-05 14:46:23 +09:00
Shin Yamamoto 007f9af3eb Enable the removal interaction at any positions upon the conditions (#335)
If a library consumer allows a panel projectable movement with the
FloatingPanelBehavior object, the panel is able to invoke the removal
interaction when the next moving position projected the momentum is
hidden.
2020-09-03 23:07:33 +09:00
Michal Raška da4e1d26d3 Fix quick pull down (#385) 2020-09-03 21:41:16 +09:00
Shin Yamamoto 2ce1375ce7 Fix an issue where keyboard opens above image picker (#381)
This issue was reported in
https://github.com/SCENEE/FloatingPanel/issues/369.

The cause of that is the containerViewWillLayoutSubviews() of
FloatingPanelPresentationController is called in presenting a
UIImagePickerViewController. As a result, a FloatingPanelController
view is added unnecessarily.

I don't know the reason why the method is called, but this patch
resolves the issue.

By the way, the issue doesn't happen when a FloatingPanelController
shows as a child view controller
2020-09-03 20:42:21 +09:00
Shin Yamamoto 016bf3e185 Revise the migration guide according to @zntfdr reviews 2020-08-31 21:12:35 +09:00
Shin Yamamoto 214942f918 Fix the panel behavior of 'Show Detail Panel' on 'fitToBounds' mode in Samples app 2020-08-29 16:17:02 +09:00
Shin Yamamoto af53ac1964 Update README 2020-08-29 15:23:41 +09:00
Shin Yamamoto 991166534f Add the migration guide for FloatingPanel 2.0 2020-08-29 15:19:32 +09:00
Shin Yamamoto 39feab437d Fix layouts using backdropAlpaFor(position:) in Samples app 2020-08-29 13:28:15 +09:00
Shin Yamamoto ba8d37e8e0 Update .gitignore 2020-08-22 15:33:30 +09:00
Shin Yamamoto 885493f2e5 Add doc comments 2020-08-21 17:42:02 +09:00
Shin Yamamoto 65fd04975d Clean up core code 2020-08-12 12:26:18 +09:00
Shin Yamamoto 1a45f43232 Update the file license headers
I reconsidered the author part because it's better to make the ownership
obvious to get rid of problems according to the unclearness.
2020-08-12 09:37:52 +09:00
Greg Hazel 28c384aa0d use 'prominent' blur effect (#379)
'prominent' blue effect is adaptive to dark mode.
2020-08-10 12:36:47 +09:00
Shin Yamamoto 99c922bf3a Update doc comment 2020-08-10 12:22:14 +09:00
Shin Yamamoto 009033be79 Fix typo 2020-08-10 12:22:14 +09:00
Shin Yamamoto 5d2e6dd417 Fix method names in core 2020-08-10 12:22:14 +09:00
Christopher Truman 9c71a47d9b Small typo fixes (#378) 2020-08-10 09:55:10 +09:00
Shin Yamamoto db0d73b428 2.0.0 Beta 1 2020-07-11 13:15:36 +09:00
Shin Yamamoto 968cb47163 Remove an unnecessary file 2020-07-11 13:15:33 +09:00
Shin Yamamoto 6f28edc92c Clean up SampleObjC code 2020-07-11 13:04:13 +09:00
Shin Yamamoto 2b94139435 Fix platform names in available conditions 2020-07-11 13:00:15 +09:00
Shin Yamamoto cb21b4075d Fix podspec 2020-07-11 13:00:15 +09:00
Shin Yamamoto d23e17c20b Fix SwiftPM errors 2020-07-11 13:00:15 +09:00
Shin Yamamoto 92f2a089ee Update README 2020-07-11 13:00:15 +09:00
Shin Yamamoto 983f9e2835 Remove a line with only whitespace 2020-07-11 13:00:15 +09:00
Shin Yamamoto 14fad8b745 Add Layout{Anchoring,References} and Position 2020-07-11 13:00:15 +09:00
Shin Yamamoto 098bd14a03 Remove an unused import 2020-07-11 13:00:15 +09:00
Shin Yamamoto cfa4047adf Add State.swift 2020-07-11 13:00:15 +09:00
Shin Yamamoto bbf437735f Modify Logger display name 2020-07-11 13:00:15 +09:00
Shin Yamamoto 5540c94e6c Add a mark comment 2020-07-11 13:00:15 +09:00
Shin Yamamoto 78981651c4 Modify NumericSpringAnimator
- Change the access control of isRunning
- Remove an unused parameter
- Fix format
2020-07-11 13:00:15 +09:00
Shin Yamamoto 5df377e43d Change the file order 2020-07-11 13:00:15 +09:00
Shin Yamamoto 02af74894a Rename Tests 2020-07-11 13:00:15 +09:00
Shin Yamamoto 8533ecfcdf Simplify file and object names 2020-07-11 13:00:15 +09:00
Shin Yamamoto 6d37df16b2 Reorganize the project structure 2020-07-11 13:00:15 +09:00
Shin Yamamoto d4542a4948 Use incremental complication mode for code coverage 2020-07-11 09:48:25 +09:00
Shin Yamamoto 0d99c14847 Update doc comments 2020-07-11 09:48:25 +09:00
Shin Yamamoto fc1aed9605 Replace 'decelerate' term with 'attract' 2020-07-11 09:48:25 +09:00
Shin Yamamoto ebf0bcc07f Rename constraint props 2020-07-11 09:48:25 +09:00
Shin Yamamoto fb0221226f Add constraint identifiers 2020-07-11 09:48:25 +09:00
Shin Yamamoto 6277bfc89f Update docs 2020-07-11 09:48:25 +09:00
Shin Yamamoto 4181918fc5 Add IDETemplateMacros.plist 2020-07-11 09:48:25 +09:00
Shin Yamamoto cd06dfdb28 Update the license headers 2020-07-11 09:48:25 +09:00
Shin Yamamoto 9509e3c32b Add .swiftformat 2020-07-11 09:48:25 +09:00
Shin Yamamoto b574cf97a0 Update Maps.app
* Support iPad panel
* Change the panel's content mode
* Add DetailViewController for left/right panels.
2020-07-11 09:48:25 +09:00
Shin Yamamoto c2fc35fc41 Update Stocks.app 2020-07-11 09:48:25 +09:00
Shin Yamamoto 1bbb4dcc23 Update Samples.app
* Add BottomEdgeInteractionLayout sample
* Support v2
    * Use FloatingPanelSurfaceAppearance
    * Fix 'Tab Bar > Layout 3' in Samples.app
    * Remove interactionBuffer(for:) from Samples.app
2020-07-11 09:48:25 +09:00
Shin Yamamoto f9a5386dc7 Add SamplesObjC app
* Add build job in .travis.yml
2020-07-11 09:48:25 +09:00
Shin Yamamoto c962395581 Fix LayoutAdapter.setUpAnimationEdgeConstraint(to:) 2020-07-11 09:48:25 +09:00
Shin Yamamoto 015454238a Use LayoutAdapter.positon in itself 2020-07-11 09:48:25 +09:00
Shin Yamamoto a5341f37c6 Fix a surface container constraint on left position 2020-07-11 09:48:25 +09:00
Shin Yamamoto c519404752 Fix LayoutAdapter.position(for:) 2020-07-11 09:48:25 +09:00
Shin Yamamoto 4c9ebbbd61 Fix containerOverflow 2020-07-11 09:48:25 +09:00
Shin Yamamoto 0d958b2d04 Rename edgeY to edgePosition 2020-07-11 09:48:25 +09:00
Shin Yamamoto ab7b16e789 Fix the timing to call floatingPanelDidMove delegate 2020-07-11 09:48:25 +09:00
Shin Yamamoto c482da8ceb Rename 'stateAnchors' to 'anchors' 2020-07-11 09:48:25 +09:00
Shin Yamamoto b348684ed5 Rename 'anchorPosition' to 'position' 2020-07-11 09:48:25 +09:00
Shin Yamamoto 6d63ccd754 Fix the rounding corners of the grabber handle 2020-07-11 09:48:25 +09:00
Shin Yamamoto 579bcbd9dd Calculate the actual frame duration in numeric springing 2020-07-11 09:48:25 +09:00
Shin Yamamoto bcba7c8844 Improve delegate methods for the removal interaction 2020-07-11 09:48:25 +09:00
Shin Yamamoto 0adb374146 Prevent potential errors of unsatisfiable constraints 2020-07-11 09:48:25 +09:00
Shin Yamamoto 74d7cdfb42 Fix unsatisfiable constraints on fitToBounds mode 2020-07-11 09:48:25 +09:00
Shin Yamamoto 3de61d5ee2 Reset maskedCorners 2020-07-11 09:48:25 +09:00
Shin Yamamoto ba1b6170eb Add FloatingPanelController.untrack(scrollView:) for multiple scroll views' tracking 2020-07-11 09:48:25 +09:00
Shin Yamamoto 1f4daa04c2 Fix hidden state handling with 1 state anchor 2020-07-11 09:48:25 +09:00
Shin Yamamoto 5cebed07d8 Use multi string literal for long log prints 2020-07-11 09:48:25 +09:00
Shin Yamamoto 9b33687c10 Modify FloatingPanelPanGestureRecognizer name 2020-07-11 09:48:25 +09:00
Shin Yamamoto da4162b307 Fix failure requirements of the pan gesture in the grabber area 2020-07-11 09:48:25 +09:00
Shin Yamamoto 92ca4909ce Intercept delegate methods of the pan gesture recognizer
This replaces floatingPanel(_:shouldRecognizeSimultaneouslyWith)
delegate method.
2020-07-11 09:48:25 +09:00
Shin Yamamoto 65d03846a1 Clean up code 2020-07-11 09:48:25 +09:00
Shin Yamamoto fe86659db7 Refactor updateStaticConstraint() 2020-07-11 09:48:25 +09:00
Shin Yamamoto 153bd32cfe Rename fpc.scrollView to fpc.trackingScrollView 2020-07-11 09:48:25 +09:00
Shin Yamamoto 99fd41c58b Suppress the shadow fade-out animation when the surface jumps to a location. 2020-07-11 09:48:25 +09:00
Shin Yamamoto d217c3ad2b Rename heightConstraint to staticConstraint 2020-07-11 09:48:25 +09:00
Shin Yamamoto 0f678d20fe Support multiple shadows in the surface appearance 2020-07-11 09:48:25 +09:00
Shin Yamamoto 102c9b3019 Fix the height break after rotating a device twice in static mode 2020-07-11 09:48:25 +09:00
Shin Yamamoto fd4e6fea61 Add UISpringTimingParameters(decelerationRate:frequencyResponse:initialVelocity:) 2020-07-11 09:48:25 +09:00
Shin Yamamoto 54570ac7c6 Fix updating the container height when the content mode is changed
This change is necessary to fix a layout break when the contentMode is
changed after laying out a floating panel.
For example, the following patch lets the bug reproduce in Maps.app.

```
diff --git a/Examples/Maps/Maps/ViewController.swift b/Examples/Maps/Maps/ViewController.swift
index c5f25f1..b71c313 100644
--- a/Examples/Maps/Maps/ViewController.swift
+++ b/Examples/Maps/Maps/ViewController.swift
@@ -14,7 +14,6 @@ class ViewController: UIViewController, MKMapViewDelegate {
     override func viewDidLoad() {
         super.viewDidLoad()

-        fpc.contentMode = .fitToBounds
         fpc.delegate = fpcDelegate

         // Set a content view controller
@@ -33,6 +32,7 @@ class ViewController: UIViewController, MKMapViewDelegate {

     override func viewDidAppear(_ animated: Bool) {
         super.viewDidAppear(animated)
+        fpc.contentMode = .fitToBounds

         // Must be here
         searchVC.searchBar.delegate = self
```
2020-07-11 09:48:25 +09:00
Shin Yamamoto 38359e25ad v2.0
* Core
    * Support bottom/left/right positioned panel.
    * Replace FloatingPanelPosition with FloatingPanelState
    * Redefine FloatingPanelPosition
    * Fix pan interaction of the intrinsic layout
    * Allow to modify a target position
    * Fix floating point problem
    * Use UIScrollView.adjustedContentInset by default
    * Add shouldDecelerate(to:)
    * Rename contentOrigin to contentOffsetForPinning
    * Improve initialLocation

* Introduce numeric springing
    * Support moving a panel with numeric springing
    * Update FloatingPanelBehavior
    * Remove {top,Bottom}InteractionBuffer from FloatingPanelLayout
    * Remove the move animator to be same as UIScrollView behavior
        * Add floatingPanelDidMove(_:) tests in calling move(to:animated:)
    * Update FloatingPanelControllerDelegate
        * Move {add,remove}PanelAnimator delegation

* Add FloatingPanelBehaviorAdapter
    * Modify access to `behavior` prop

* Update FloatingPanelLayout
    * Support bottom/left/right positioned panel.
    * Add anchoredPosition property
    * Introduce FloatingLayoutAnchor & FloatingPanelIntrinsicLayoutAnchor
    * Add FloatingPanelReferenceEdge
    * Rename FloatingPanelDefaultLayout to FloatingPanelBottomLayout

* Update FloatingPanelLayoutAdapter
    * Add LayoutAdapter.sortedDirectionalPositions
    * Reimplement positionY(for:)
    * Add edge{most,least}state
    * Add orderedStates
    * Add edge{Most,Least} properties in LayoutAdapter
    * Modify `bottomOverflow` value and rename it to `containerOverflow`
    * Modify access to `layout` prop

* Add FloatingPanelLayoutAnchoring/LayoutAnchor/IntrinsicLayoutAnchor
    * Add FloatingPanel{Intrinsic}LayoutAnchor tests

* Update FloatingPanelController
    * Add surfaceLocation
    * update addPanelToParent API
    * Update layout reloading logic
    * Rename updateLayout() to invalidateLayout()
    * Fix contentViewController access level

* Update FloatingPanelControllerDelegate
    * Add floatingPanelDidEndDragging(_ vc:willDecelerate:)
    * Update floatingPanelWillBeginDecelerating delegate method
    * Remove behavior delegate method
    * Reimplement the removal action
        * Use floatingPanel(_:shouldRemoveAt:with:) delegate

* Update SurfaceView
    * Support bottom/left/right positioned panel.
    * Add FloatingPanelSurfaceAppearance
    * Add SurfaceView.intrinsicContentSize
    * Rename FloatingPanelSurfaceView.add(contentView:)
    * GrabberHandle for bottom edge grabber
    * Rename GrabberHandleView to FloatingPanelGrabberView
    * Rename containerMargin & contentInsets to content{Margins,Padding}
    * Rename bottomOverflow to containerOverflow

* ObjC Support
    * Add @objc to FloatingPanelControllerDelegate
    * Add @objc to FloatingPanelController
    * Use @objc protocol FloatingPanelBehavior
    * Redefine FloatingPanelState for objc.

* Update Utils
    * Modify logger prefix
    * Update extensions for left/right positions

* Update unit tests
    * Update layout tests
    * Add test_surfaceView_{contentView,containerView}
    * Add test_positionY_top2bottom()
    * Add test_moveTo_bottomEdge()
    * Update test_warningRetainCycle()
    * Fix FloatingPanelControllerTests
    * Fix LayoutTests
    * Fix FloatingPanelStateTests
    * Fix FloatingPanelTest
2020-07-11 09:48:25 +09:00
Shin Yamamoto 2cf63838ae Drop preserveContentVCLayoutIfNeeded() 2020-07-11 09:48:25 +09:00
Shin Yamamoto 49e8545aec Rename trackedScrollView to trackingSrollView 2020-07-11 09:48:25 +09:00
Shin Yamamoto b34ea2444d Disable dismissal action of the backdrop by default
fix #341
2020-07-11 09:48:25 +09:00
Shin Yamamoto 984d4c41d8 Replace 'precondition' to 'assert' 2020-07-11 09:48:25 +09:00
Shin Yamamoto 8d372452b4 Remove 'unavailable' annotations 2020-07-11 09:48:25 +09:00
Shin Yamamoto 2fc16bb1f4 Upgrade swift version to 5.0 2020-07-11 09:48:25 +09:00
Leko Murphy 8903e4e610 don't remove panel on view disappearance (#367) 2020-07-11 09:46:41 +09:00
Shin Yamamoto 5634de2eee Merge pull request #371 from knchst/modify/readme
Modify README.md sample code.
2020-07-11 09:42:16 +09:00
Kenichi Saito 1957ae3919 Modify README 2020-07-09 19:59:20 +09:00
Shin Yamamoto a4f8c0528c Merge pull request #356 from SCENEE/release-1.7.5
Release 1.7.5
2020-06-04 08:30:31 +09:00
Shin Yamamoto e4548b26bd Release 1.7.5 2020-06-03 22:33:02 +09:00
Shin Yamamoto d540b1ddde Fix the panel behavior in a sheet modal (#358)
* Add "Show Panel in Sheet Modal" sample
* Fix the behavior in a sheet modal
2020-06-03 22:31:30 +09:00
Shin Yamamoto aaeb752911 No need to recognize both of the pan gesture and dismiss gesuter of sheet modal 2020-05-30 10:11:12 +09:00
Grigory 966caad519 Fix the constraints break on fitToBounds mode (#359) 2020-05-30 09:44:58 +09:00
Shin Yamamoto a62c3a23dc Fix some view controllers in Samples.app for a sheet modal 2020-05-23 09:07:01 +09:00
Shin Yamamoto 7bbc3d5910 fix the behavior in a sheet modal 2020-05-23 08:12:01 +09:00
Shin Yamamoto e2afb1e22f Add "Show Panel in Sheet Modal" sample 2020-05-23 08:11:30 +09:00
Shin Yamamoto 8cca1178fd fix {top,bottom} constant's boundary in updating panel interactively (#352)
Because {top,bottom}Y can be {less,more} than the SafeArea/Superview bounds,
if a minus value is set to the inset for {top,bottom} most position.
2020-05-21 08:59:52 +09:00
Shin Yamamoto a09a0e9e32 fix the animation velocity's sign (#354)
Using a directional distance to calculate an animation velocity fixes an
issue where a panel's animation was wrong when a user swipes up a panel
at the top most position.
2020-05-16 09:18:59 +09:00
Shin Yamamoto 43c76faa20 fix invalid safearea insets in a table view with static cells (#353)
See also https://github.com/SCENEE/FloatingPanel/issues/330
2020-05-16 09:17:34 +09:00
Shin Yamamoto 5787a350ab fix the memory leak of FloatingPanelController object (#350) 2020-05-15 07:41:18 +09:00
Shin Yamamoto 9abb80de64 Fix an invalid indicator insets of the tracking scroll view (#346) 2020-04-29 14:22:34 +09:00
Shin Yamamoto 7d90458d99 Support the initial hidden position not including the supported positions (#345) 2020-04-29 14:20:49 +09:00
Shin Yamamoto 5d5f14acd8 fix a build error on Xcode 11.4 (#337) 2020-03-30 17:52:00 +09:00
Shin Yamamoto bed519f0c0 Merge pull request #326 from SCENEE/release-1.7.4
Release 1.7.4
2020-02-29 18:52:15 +09:00
Shin Yamamoto 7c47e2e20e Release 1.7.4 2020-02-29 13:25:11 +09:00
Federico Zanetello f909cd4101 ignore .swiftpm/ folder (#324) 2020-02-29 13:20:50 +09:00
Federico Zanetello 8c24aa3fc9 fix api typo (#325)
fixes floatingPanel(_ vc: FloatingPanelController, contentOffsetForPinning) name
2020-02-29 13:18:09 +09:00
Shin Yamamoto 7531d80f1c Merge pull request #323 from SCENEE/release-1.7.3
Release 1.7.3
2020-02-29 10:40:38 +09:00
Shin Yamamoto c4c1906cae Release 1.7.3 2020-02-28 21:00:44 +09:00
Shin Yamamoto 4f717c5840 update github issue template 2020-02-28 21:00:40 +09:00
Shin Yamamoto fbd83ef500 fix a run script error 2020-02-26 22:53:44 +09:00
Shin Yamamoto 2a91145366 Fix breaking content offset of the tracked scroll view (#315)
* Convert the tracked scroll view frame to the surface coordinate space
2020-02-26 22:49:51 +09:00
Shin Yamamoto f5d72aa0a5 Add failure requirements for multiple panels (#322)
* add multi panel sample
2020-02-26 22:47:52 +09:00
Shin Yamamoto 65f67c98f4 Add floatingPanel(_:contentOffsetForPinning:) delegate method (#314)
* add floatingPanel(_:contentOffsetForPinning:)
* add 'Show NavigationController' sample
* fix the initial content offset in a navigation bar with  a large text
    The content offset preservation should be applied only when
    `FloatingPanelController.contentInsetAdjustmentBehavior` is `.always`.
    This is because the library user loses control of the initial offset.
2020-02-24 11:16:10 +09:00
Shin Yamamoto 1f79c2573f Merge pull request #316 from jacksonjude/master
Minor README.md typo
2020-02-15 10:32:03 +09:00
jacksonjude bc840dde46 Minor README.md typo 2020-02-08 22:36:50 -08:00
Shin Yamamoto 57ed039857 Merge pull request #312 from SCENEE/release-1.7.2
Release 1.7.2
2020-01-30 09:29:00 +09:00
Shin Yamamoto 11f0e8c84e Release 1.7.2 2020-01-29 11:33:11 +09:00
Shin Yamamoto dd19c866d4 Merge pull request #311 from SCENEE/return-childvc-to-consult
Return the child view controller to consult
2020-01-29 11:31:01 +09:00
Shin Yamamoto 801fed9843 Return the child view controller to consult 2020-01-29 08:54:53 +09:00
Shin Yamamoto 847b5c0917 Merge pull request #310 from SCENEE/fix-didenddecelerating-call
Fix delegate calls
2020-01-28 22:39:37 +09:00
Shin Yamamoto c64056ca7b Make floatingPanelDidEndDragging's velocity zero when it won't animate
Ideally, it's better to define a delegate method like
scrollViewDidEndDragging(_:willDecelerate:) in FloatingPanelControllerDelegate
to notify whether a panel will be decelerated or not.
However it's a broken change so I add this change as workaround.
The delegate method definition will be improved on v2.0.
2020-01-28 11:40:31 +09:00
Shin Yamamoto 269c3e29b5 Call floatingPanelDidEndDecelerating even if an animation interrupted 2020-01-27 16:50:16 +09:00
Shin Yamamoto 002bbb4a4a Merge pull request #307 from SCENEE/iss-293
Fix a panel's move-up while dragging it down
2020-01-21 20:40:48 +09:00
Shin Yamamoto 14011a5bc2 Fix grabber area behavior
The grabber area was not working expectedly.
2020-01-18 17:27:58 +09:00
Shin Yamamoto 23f2242c9a Fix a panel's move-up in dragging it down
This issue is that a panel moved up while dragging it
down if content offset of the tracking scroll view in
a content view controller was greater than its top interaction buffer.

Ref. #293
2020-01-18 17:26:38 +09:00
Shin Yamamoto 4fd92a4002 Fix Maps.app's crash on device after the second launch (#306)
This seems to be Xcode 11's bug of linking frameworks.
2020-01-18 15:05:46 +09:00
Ramesh R C 9c57089b0e Add FloatingPanelController.nearbyPosition (#303)
* Added nearbyPosition : always a position of a user's finger.
* debugging nearby position in Maps.app.
* Added test cases move with nearby position.
2020-01-09 13:26:30 +09:00
Shin Yamamoto 3b11cdc72a Merge pull request #291 from SCENEE/release-1.7.1
Release 1.7.1
2019-11-27 21:59:57 +09:00
Shin Yamamoto 4edaad2cf4 Release 1.7.1 2019-11-21 21:17:11 +09:00
Shin Yamamoto 92fc0621e2 Merge pull request #292 from TadeasKriz/patch-1
Improve manual `show` and `hide` example.
2019-11-21 21:16:32 +09:00
Tadeas Kriz e9f4392c48 Improve manual show and hide example. 2019-11-20 15:56:17 +01:00
Shin Yamamoto 4df40becaf Merge pull request #290 from SCENEE/fix/addpanel
Pass parent to didMove(toParent:)
2019-11-20 10:36:22 +09:00
Shin Yamamoto ba11e7c7d7 Pass parent to didMove(toParent:)
4ad7f11 commit causes the wrong parameter.
2019-11-20 09:51:47 +09:00
Shin Yamamoto ae671f22c6 Merge pull request #288 from SCENEE/fix-swiftinterface-error
Rename the internal FloatingPanel object for .swiftinterface issue
2019-11-19 23:01:36 +09:00
Shin Yamamoto f22f58212b Clean up lines with only white spaces 2019-11-19 18:37:52 +09:00
Shin Yamamoto 54ff1c360d Rename 'FloatingPanel' type for '.swiftinterface' issue
See also https://forums.swift.org/t/frameworkname-is-not-a-member-type-of-frameworkname-errors-inside-swiftinterface/28962
2019-11-19 18:37:44 +09:00
dmytrofrolov1 772d6c3ef3 Improve the surface position evaluation and top scroll bouncing of content
* Evaluate the surface position approximately by 1px with a display scale
* Allow a top scroll bouncing of content without closing floating panel when a user scrolls it a lot
2019-11-19 18:29:20 +09:00
Shin Yamamoto a94c3b3c26 Merge pull request #287 from SCENEE/fix-module-stability
Enable the swift module interfaces
2019-11-15 22:07:01 +09:00
Shin Yamamoto d0ffc4ceb1 Enable the swift module interfaces 2019-11-15 14:32:33 +09:00
Shin Yamamoto 597ce487aa Merge pull request #275 from peka2/patch-1
Update README
2019-10-09 00:32:57 +09:00
peka2 87eb8d94fd Update README 2019-10-08 18:58:01 +09:00
Shin Yamamoto 4944fc516a Update README 2019-10-05 14:09:41 +09:00
Shin Yamamoto 7537384339 Merge pull request #273 from SCENEE/release-1.7.0
Release 1.7.0
2019-10-05 14:07:40 +09:00
Shin Yamamoto 8fd134512f Release v1.7.0 2019-09-28 23:02:45 +09:00
Shin Yamamoto f566fc6475 Add a sample of panel including a PageVC content 2019-09-28 23:02:45 +09:00
Shin Yamamoto 9cbcb48a9b Merge pull request #266 from SCENEE/add-containerinsets
Add FloatingPanelSurfaceView.containerMargins
2019-09-28 22:47:21 +09:00
Shin Yamamoto 817956cef3 Update README 2019-09-28 21:49:47 +09:00
Shin Yamamoto 3c1aa7aa42 Fix FloatingPanelFullScreenLayout is not working 2019-09-28 13:18:58 +09:00
Shin Yamamoto 7598e8f160 ci: Update xcode versions 2019-09-28 13:18:58 +09:00
Shin Yamamoto ba011e7242 Add 'Show with ContainerMargins' sample 2019-09-28 13:18:44 +09:00
Shin Yamamoto ecdf20db8f Add FloatingPanelSurfaceView.containerMargins
`FloatingPanelSurfaceView.containerTopInset` is replaced by the top
inset.
2019-09-28 13:18:44 +09:00
Shin Yamamoto 3a7f39321c Update allowsRubberBanding() sample 2019-09-28 13:18:44 +09:00
Shin Yamamoto e75d83e7a4 Merge pull request #270 from SCENEE/release-1.6.6
Release 1.6.6
2019-09-28 13:17:37 +09:00
Shin Yamamoto 2cdb4a6bc2 Suppress UITableViewAlertForLayoutOutsideViewHierarchy alert
Following the alert suggestion
> [TableView] Warning once only: UITableView was told to layout its visible
> cells and other contents without being in the view hierarchy (the table
> view or one of its superviews has not been added to a window).
2019-09-16 21:58:28 +09:00
Shin Yamamoto 1a4d5a7954 CI: Add Xcode 11 & Swift 5.1 build
* Add a build on Xcode 11 image with SUPPORTS_MACCATALYST=NO because Xcode 11 runs on macOS 10.14 in Travis CI
* Add Swift 5.1 badge
2019-09-16 21:56:28 +09:00
Shin Yamamoto 22ef3e7cd9 Merge pull request #253 from SCENEE/release-1.6.5
Release 1.6.5
2019-08-31 13:48:32 +09:00
Nikolay Derkach f8b8176988 Support bottom content inset for container view (#257)
and also fix height of a content view resized by the inset
Fix #256
2019-08-31 12:43:53 +09:00
Shin Yamamoto 0c5bf2bfe9 Merge pull request #252 from hartbit/updating-layout
Improve the documentation of floatingPanelDidChangePosition
2019-08-24 17:02:33 +09:00
David Hart 8b45517915 Improve floatingPanelDidChangePosition and tigger it on removal 2019-08-22 14:09:02 +02:00
Shin Yamamoto 3cca07fefd Merge pull request #251 from rikusouda/fix/call_did_end_remove_by_backdrop_view
Call floatingPanelDidEndRemove when dismiss with tap on backdrop view
2019-08-21 15:21:55 +09:00
Yuki Yoshioka 276ae23f13 Call floatingPanelDidEndRemove when dismiss with tap on backdrop view
By #205 added dismissing by backdrop view. But `floatingPanelDidEndRemove` is not called from it.
2019-08-19 14:34:11 +09:00
Shin Yamamoto c1b2ffeb78 Merge pull request #212 from joshuafinch/feature/cocoapods-1.7
Add support for specifying swift_versions in CocoaPods 1.7 and above
2019-08-15 13:47:44 +09:00
Shin Yamamoto 262ee34201 Merge pull request #247 from SCENEE/release-1.6.4
Release 1.6.4
2019-08-09 23:52:04 +09:00
Shin Yamamoto 53719bd94a Feat elastic layout (#145)
* Move the prepareLayout(in:) call
* Support 'fitToBounds' content mode
* Add NSLayoutConstraint.{de}activate(constraint:)
* Update README
2019-08-03 14:45:35 +09:00
Nikolay Derkach 935b7d9e10 Allow to disable tap on backdrop view for panel dismissal (#205)
* Add 'FloatingPanelBackdropView. dismissalTapGestureRecognizer'
* Enable tap on backdropview gesture recognizer only for the modal presentation
2019-08-03 12:37:39 +09:00
Shin Yamamoto e3bf19b972 Merge pull request #224 from SCENEE/feat-position-reference
Add FloatingPanelLayout.positionReference
2019-07-27 15:22:18 +09:00
Shin Yamamoto c36f09d3e9 Update README 2019-07-27 13:47:13 +09:00
Shin Yamamoto 9936a89118 Add FloatingPanelLayoutTests.test_positionReference() 2019-07-27 11:44:33 +09:00
Shin Yamamoto 562424cd8f Add FloatingPanelLayout.positionReference 2019-07-27 11:44:33 +09:00
Shin Yamamoto 0c3fb83d0a Merge pull request #241 from SCENEE/release-1.6.3
Release 1.6.3
2019-07-26 23:00:45 +09:00
Joshua Finch 23846dbf23 Add support for CocoaPods version 1.7 swift_version 2019-06-01 22:56:21 +01:00
86 changed files with 8330 additions and 4330 deletions
+18 -5
View File
@@ -2,7 +2,7 @@
>
> Please remove this line and everything above it before submitting.
### Short description
### Description
### Expected behavior
@@ -12,16 +12,29 @@
**Code example that reproduces the issue**
**How do you display panel(s)?**
* Add as child view controllers
* Present modally
**How many panels do you displays?**
* 1
* 2+
### Environment
**Library version**
**Installation method**
- [ ] CocoaPods
- [ ] Carthage
- [ ] Git submodules
* CocoaPods
* Carthage
* Swift Package Manager
**iOS version(s)**
**Xcode version**
+4
View File
@@ -17,6 +17,7 @@ DerivedData/
*.perspectivev3
!default.perspectivev3
xcuserdata/
IDEWorkspaceChecks.plis
## Other
*.moved-aside
@@ -30,6 +31,9 @@ xcuserdata/
*.dSYM.zip
*.dSYM
## Swift Package Manager Specific
.swiftpm/
## Playgrounds
timeline.xctimeline
playground.xcworkspace
+3
View File
@@ -0,0 +1,3 @@
--header "// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license."
--disable andOperator,anyObjectProtocol,blankLinesAroundMark,blankLinesAtEndOfScope,blankLinesAtStartOfScope,blankLinesBetweenScopes,braces,consecutiveBlankLines,consecutiveSpaces,duplicateImports,elseOnSameLine,emptyBraces,hoistPatternLet,indent,isEmpty,leadingDelimiters,linebreakAtEndOfFile,linebreaks,numberFormatting,ranges,redundantBackticks,redundantBreak,redundantExtensionACL,redundantFileprivate,redundantGet,redundantInit,redundantLet,redundantLetError,redundantNilInit,redundantObjc,redundantParens,redundantPattern,redundantRawValues,redundantReturn,redundantSelf,redundantVoidReturnType,semicolons,sortedImports,spaceAroundBraces,spaceAroundBrackets,spaceAroundComments,spaceAroundGenerics,spaceAroundOperators,spaceAroundParens,spaceInsideBraces,spaceInsideBrackets,spaceInsideComments,spaceInsideGenerics,spaceInsideParens,specifiers,strongOutlets,strongifiedSelf,todos,trailingClosures,trailingCommas,trailingSpace,typeSugar,unusedArguments,void,wrapArguments,yodaConditions
+42 -34
View File
@@ -2,12 +2,6 @@ language: objective-c
branches:
only:
- master
cache:
directories:
- /usr/local/Homebrew
- $HOME/Library/Caches/Homebrew
before_cache:
- brew cleanup
env:
global:
- LANG=en_US.UTF-8
@@ -15,52 +9,66 @@ env:
jobs:
include:
- stage: "Builds"
osx_image: xcode9.4
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
name: "Swift 4.2"
- script: xcodebuild -scheme FloatingPanel SWIFT_VERSION=5.0 clean build
osx_image: xcode10.2
name: "Swift 5.0"
- script: xcodebuild -scheme FloatingPanel SWIFT_VERSION=5.1 SUPPORTS_MACCATALYST=NO clean build
script: xcodebuild -scheme FloatingPanel SWIFT_VERSION=5.1 SUPPORTS_MACCATALYST=NO clean build
# SUPPORTS_MACCATALYST=NO because Xcode 11 runs on macOS 10.14 in Travis CI
osx_image: xcode11
osx_image: xcode11.3
name: "Swift 5.1"
- script: xcodebuild -scheme FloatingPanel SWIFT_VERSION=5.2 clean build
osx_image: xcode11.6
name: "Swift 5.2"
- script: xcodebuild -scheme FloatingPanel SWIFT_VERSION=5.3 clean build
osx_image: xcode12
name: "Swift 5.3"
- 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'
script: xcodebuild clean test -scheme FloatingPanel -workspace FloatingPanel.xcworkspace -destination 'platform=iOS Simulator,OS=10.3.1,name=iPhone SE (1st generation)'
osx_image: xcode11.6
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
osx_image: xcode11.6
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)"
- script: xcodebuild clean test -scheme FloatingPanel -workspace FloatingPanel.xcworkspace -destination 'platform=iOS Simulator,OS=12.4,name=iPhone X'
osx_image: xcode11.6
name: "iPhone X (iOS 12.4)"
- script: xcodebuild clean test -scheme FloatingPanel -workspace FloatingPanel.xcworkspace -destination 'platform=iOS Simulator,OS=13.6,name=iPhone 11'
osx_image: xcode11.6
name: "iPhone X (iOS 13.6)"
- stage: Build examples
osx_image: xcode10.2
script: xcodebuild -scheme Maps -sdk iphonesimulator clean build
- stage: "Build examples"
osx_image: xcode11.6
script: xcodebuild -workspace FloatingPanel.xcworkspace -scheme Maps -sdk iphonesimulator clean build
name: "Maps"
- script: xcodebuild -scheme Stocks -sdk iphonesimulator clean build
osx_image: xcode10.2
- script: xcodebuild -workspace FloatingPanel.xcworkspace -scheme Stocks -sdk iphonesimulator clean build
osx_image: xcode11.6
name: "Stocks"
- script: xcodebuild -scheme Samples -sdk iphonesimulator clean build
osx_image: xcode10.2
- script: xcodebuild -workspace FloatingPanel.xcworkspace -scheme Samples -sdk iphonesimulator clean build
osx_image: xcode11.6
name: "Samples"
- script: xcodebuild -workspace FloatingPanel.xcworkspace -scheme SamplesObjC -sdk iphonesimulator clean build
osx_image: xcode11.6
name: "SamplesObjC"
- stage: Carthage
osx_image: xcode10.2
- stage: "Swift Package Manager"
script: swift build -Xswiftc "-sdk" -Xswiftc "`xcrun --sdk iphoneos --show-sdk-path`" -Xswiftc "-target" -Xswiftc "arm64-apple-ios13.0"
osx_image: xcode11.6
name: "iPhone OS"
- script: swift build -Xswiftc "-sdk" -Xswiftc "`xcrun --sdk iphonesimulator --show-sdk-path`" -Xswiftc "-target" -Xswiftc "x86_64-apple-ios13.0-simulator"
osx_image: xcode11.6
name: "iPhone Simulator"
- stage: "Carthage"
# Carthage doesn't fix the issue with Xcode 12: https://github.com/Carthage/Carthage/releases/tag/0.36.0
osx_image: xcode11.6
before_install:
- brew update
- brew outdated carthage || brew upgrade carthage
script:
- carthage build --no-skip-current
- stage: CocoaPods
osx_image: xcode10.2
- stage: "CocoaPods"
osx_image: xcode11.6
before_install:
- gem install cocoapods
script:
- pod spec lint --allow-warnings
- pod lib lint --allow-warnings
@@ -0,0 +1,294 @@
# FloatingPanel 2.0 Migration Guide
FloatingPanel 2.0 is the latest major release of FloatingPanel. As a major release, following Semantic Versioning conventions, 2.0 introduces API-breaking changes.
This guide is provided in order to ease the transition of existing applications using FloatingPanel 1.x to the latest APIs, as well as explain the design and structure of new and updated functionality.
## Updated Minimum Requirements
* Swift 5.0
* iOS 11 (iOS 10 is still the deployment target, but not tested well)
* Xcode 11.0
## Benefits of Upgrading
* __Top, left and right positioned panel__
* FloatingPanel is not just a library for a bottom positioned panel, but also top, left and right positioned ones.
* __Objective-C compatibility__
* The entire APIs are exposed in Objective-C. So you can use them in Objective-C directly.
* __Flexible and explicit layout customization__
* `FloatingPanelLayout` is redesigned. There is no implicit rules to lay out a panel anymore.
* __New spring animation without UIViewPropertyAnimator__
* The new spring animation uses [Numeric springing](http://allenchou.net/2015/04/game-math-precise-control-over-numeric-springing/) which is a very powerful tool for procedural animation. Therefore a library consumer is easy to modify a panel behavior by 2 paramters of the deceleration rate and response time.
* __Handle the panel position anytime__
* `floatingPanelDidMove(_:)` delegate method is also called while a panel is moving. The method behavior becomes same as `scrollViewDidScroll(_:)` in `UIScrollViewDelegate`. And in the method a library consumer is able to change a panel location.
* __Update the removal interaction's invocation__
* Now you can invoke the removal interaction at any time where you want. There is no restrictions in the library.
* __Fix many issues depending on API design__
* See the following sections for details.
## API Name Changes
* `FloatingPanelPosition` is now `FloatingPanelState`.
* `FloatingPanelPosition` in v2 is used to specify a panel position(top, left, bottom and right) in a screen.
* `FloatingPanelSurfaceView` is `SurfaceView` only in Swift.
* `FloatingPanelBackdropView` is `BackdropView` only in Swift.
* `FloatingPanelGrabberHandleView` is `GrabberView` only in Swift.
* "decelerate" term is replaced with "attract" because the panel's behavior is not unidirectional, but going back and forth so that it is settled to a location.
## `FloatingPanelController`
* `layout` and `behavior` properties can be changed directly without using the delegate methods.
```swift
fpc.behavior = SearchPaneliPadBehavior()
fpc.layout = SearchPaneliPadLayout()
fpc.invalidateLayout() // If needed
```
* The second argument of `addPanel(toParent:)` changes to specify an index of subviews of a view in which a panel is added.
```diff
- public func addPanel(toParent parent: UIViewController, belowView: UIView? = nil, animated: Bool = false) {
+ public func addPanel(toParent parent: UIViewController, at viewIndex: Int = -1, animated: Bool = false) {
```
* `surfaceOriginY` is now `surfaceLocation`.
* `updateLayout` is now `invalidateLayout`.
* The scroll tracking API is changed a bit to support multiple scroll view tracking in the future.
* Now `untrack(scrollView:)` is used to disable the scroll tracking.
## `FloatingPanelControllerDelegate`
* `floatingPanelDidEndDragging(_ vc:willAttract:)` is added to check whether a panel will continue to move after dragging.
* `floatingPanelDidMove(_:)` behavior changes. The method is also called in the spring animation.
* The removal interaction delegate is updated.
* `floatingPanel(_:shouldRemoveAt:with:)` is added to determine whether it invokes the removal interaction in any state.
* `floatingPanelWillRemove(_:)` is added.
* `floatingPanel(_: FloatingPanelController, layoutFor size: CGSize)` is added to respond to a layout change in regular size classes on iPad.
```swift
func floatingPanel(_ fpc: FloatingPanelController, layoutFor size: CGSize) -> FloatingPanelLayout {
if aCondition(for: size) {
return SearchPanelLayout()
}
return SearchPanel2Layout()
}
```
* The `targetState` argument type of `floatingPanelWillEndDragging(_:withVelocity:targetState:)` is changed from `FloatingPanelState` to `UnsafeMutablePointer<FloatingPanelState>` to modify a target state on demand.
```swift
func floatingPanelWillEndDragging(_ vc: FloatingPanelController, withVelocity velocity: CGPoint, targetState: UnsafeMutablePointer<FloatingPanelState>) {
switch targetState.pointee {
case .full:
// do something...
case .half:
if aCondition {
targetState.pointee = .tip
}
default:
break
}
}
```
### Deprecated APIs
* `floatingPanel(_:behaviorFor:)`
* Please update `FloatingPanelController.behavior` directly.
* `floatingPanel(_:shouldRecognizeSimultaneouslyWith:)`
* Please use `FloatingPanelController.panGestureRecognizer.delegateProxy`.
## `FloatingPanelLayout`
* `position` property is added to determine a panel position.
* `initialPosition` is now `initialState`.
* `supportedPositions` and `insetFor(position:)` are replaced with `anchors` property.
* `backdropAlphaFor(position:)` is now `backdropAlpha(for:)`.
```swift
class SearchPanelPadLayout: FloatingPanelLayout {
let position: FloatingPanelPosition = .top
let initialState: FloatingPanelState = .tip
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
return [
...
]
}
func backdropAlpha(for state: FloatingPanelState) -> CGFloat {
return 0.3
}
...
}
```
### New `FloatingPanelLayoutAnchoring` classes
The following objects adopting `FloatingPanelLayoutAnchoring` protocol are added to configure the flexible and explicit layout.
#### `FloatingPanelLayoutAnchor`
This class is used to specify a panel layout using insets from a rectangle area of the superview or safe area.
* `FloatingPanelFullScreenLayout` is replaced with anchors using `.superview` reference guide.
* `FloatingPanelLayoutAnchor(fractionalInset:edge:referenceGuide:)` lets you lay out a panel at a relative position in a reference rectangle area.
```swift
// Before:
class MyPanelLayout: FloatingPanelLayout {
var initialPosition: FloatingPanelPosition {
return .half
}
func insetFor(position: FloatingPanelPosition) -> CGFloat? {
switch position {
case .full: return 16.0
case .half: return 262.0
case .tip: return 44.0
case .hidden: return nil
}
}
}
// After:
class MyPanelLayout: FloatingPanelLayout {
var position: FloatingPanelPosition = .bottom
var initialState: FloatingPanelState { .half }
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
return [
.full: FloatingPanelLayoutAnchor(absoluteInset: 16.0, edge: .top, referenceGuide: .safeArea),
.half: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .superview),
.tip: FloatingPanelLayoutAnchor(absoluteInset: 44.0, edge: .bottom, referenceGuide: .safeArea)
]
}
}
```
#### `FloatingPanelIntrinsicLayoutAnchor`
This class is used to specify a panel layout using offsets from the intrinsic size layout.
* This replaces `FloatingPanelIntrinsicLayout`.
* This is also able to configure a fractional layout in the intrinsic size.
```swift
// Before:
class MyPanelIntrinsicLayout: FloatingPanelIntrinsicLayout {
var initialPosition: FloatingPanelPosition {
return .half
}
func insetFor(position: FloatingPanelPosition) -> CGFloat? {
switch position {
case .full: return 16.0
case .half: return 262.0
case .tip: return 44.0
case .hidden: return nil
}
}
}
// After:
class MyPanelIntrinsicLayout: FloatingPanelLayout {
var position: FloatingPanelPosition = .bottom
var initialState: FloatingPanelState { .full }
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
return [
.full: FloatingPanelIntrinsicLayoutAnchor(absoluteOffset: 16.0, referenceGuide: .safeArea)
.half: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 0.5, referenceGuide: .safeArea)
.tip: FloatingPanelLayoutAnchor(absoluteInset: 44.0, edge: .bottom, referenceGuide: .safeArea)
]
}
}
```
### Deprecated APIs
* `.topInteractionBuffer` and `.bottomInteractionBuffer`.
* Please control the max/min range of the motion in `floatingPanelDidMove(_:)` delegate method as below.
```swift
func floatingPanelDidMove(_ fpc: FloatingPanelController) {
if fpc.isAttracting == false {
let loc = fpc.surfaceLocation
let minY = fpc.surfaceLocation(for: .full).y - 6.0
let maxY = fpc.surfaceLocation(for: .tip).y + 6.0
fpc.surfaceLocation = CGPoint(x: loc.x, y: min(max(loc.y, minY), maxY))
}
}
```
## `FloatingPanelBehavior`
* `.springDecelerationRate` and `.springResponseTime` properties are added to control the new spring effect of Numeric springing.
### Deprecated APIs
* `addAnimator(_:to:)`, `removeAnimator(_:from:)`
* They are moved into `floatingPanel(_:animatorForPresentingTo:)` and `floatingPanel(_:animatorForDismissingWith:)` of `FloatingPanelControllerDelegate` because they are used for view transitions.
* `interactionAnimator(_:to:with:)`, `moveAnimator(_:from:to:)`
* They are removed because the animators are replaced with the new spring effect.
* `removalVelocity`, `removalProgress`
* They are replaced with `floatingPanel(_:shouldRemoveAt:with:)` of `FloatingPanelControllerDelegate`
* `removalInteractionAnimator(_:with:)`
* It is integrated with `floatingPanel(_:animatorForDismissingWith:)` of `FloatingPanelControllerDelegate`.
## `SurfaceView`
* `SurfaceAppearance` class and `SurfaceView.appearance` property are added to specify the rounding corners, shadows and background color.
* `SurfaceView.appearance` property avoids `Ambiguous use of 'cornerRadius'` error, for instance.
* `SurfaceAppearance` enables to apply layered box shadows into a surface to materialize it.
```swift
// Before:
fpc.surfaceView.cornerRadius = 6.0
fpc.surfaceView.backgroundColor = .clear
fpc.surfaceView.shadowHidden = false
fpc.surfaceView.shadowColor = .black
fpc.surfaceView.shadowOffset = CGSize(width: 0, height: 16)
fpc.surfaceView.shadowRadius = 16.0
// After:
let appearance = SurfaceAppearance()
appearance.cornerRadius = 8.0
appearance.backgroundColor = .clear
let shadow = SurfaceAppearance.Shadow()
shadow.color = .black
shadow.offset = CGSize(width: 0, height: 16)
shadow.radius = 16
shadow.spread = 8
appearance.shadows = [shadow]
fpc.surfaceView.appearance = appearance
```
* These properties are changed for the top, left and right positioned panel.
* `grabberTopPadding` is now `grabberHandlePadding`.
* `topGrabberBarHeight` is now `grabberAreaOffset`.
* `grabberHandleWidth` and `grabberHandleHeight` are replaced with `grabberHandleSize`.
## `BackdropView`
* The dismissal action of the backdrop is disabled by default.
* You can enable it to set `BackdropView.dismissalTapGestureRecognizer.isEnabled` to `true`.
## `FloatingPanelPanGestureRecognizer`
* `delegateProxy` property is added to intercept the gesture recognizer delegate.
```swift
func layoutPanelForPad() {
fpc.behavior = SearchPaneliPadBehavior()
fpc.panGestureRecognizer.delegateProxy = self
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return false
}
```
## Miscellaneous
* `UISpringTimingParameters(decelerationRate:frequencyResponse:initialVelocity:)` initializer is added.
* The directory structure and file names in the Xcode project changes.
+26 -2
View File
@@ -7,6 +7,8 @@
objects = {
/* Begin PBXBuildFile section */
543844BD23D2BE2000D5EDE4 /* MapKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 543844BC23D2BE2000D5EDE4 /* MapKit.framework */; };
549A5F59244673FE0025F312 /* SearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 549A5F58244673FE0025F312 /* SearchViewController.swift */; };
549D23D2233C77D5008EF4D7 /* FloatingPanel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 549D23D1233C77D5008EF4D7 /* FloatingPanel.framework */; };
549D23D3233C77D5008EF4D7 /* FloatingPanel.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 549D23D1233C77D5008EF4D7 /* FloatingPanel.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
54B5112A216C3D840033A6F3 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B51129216C3D840033A6F3 /* AppDelegate.swift */; };
@@ -14,6 +16,8 @@
54B5112F216C3D840033A6F3 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 54B5112D216C3D840033A6F3 /* Main.storyboard */; };
54B51131216C3D860033A6F3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 54B51130216C3D860033A6F3 /* Assets.xcassets */; };
54B51134216C3D860033A6F3 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 54B51132216C3D860033A6F3 /* LaunchScreen.storyboard */; };
54E26CB624A989090066C720 /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54E26CB524A989090066C720 /* Utils.swift */; };
54E26CB824A98E310066C720 /* DetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54E26CB724A98E310066C720 /* DetailViewController.swift */; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
@@ -31,6 +35,8 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
543844BC23D2BE2000D5EDE4 /* MapKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MapKit.framework; path = System/Library/Frameworks/MapKit.framework; sourceTree = SDKROOT; };
549A5F58244673FE0025F312 /* SearchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewController.swift; sourceTree = "<group>"; };
549D23D1233C77D5008EF4D7 /* FloatingPanel.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = FloatingPanel.framework; sourceTree = BUILT_PRODUCTS_DIR; };
54B51126216C3D840033A6F3 /* Maps.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Maps.app; sourceTree = BUILT_PRODUCTS_DIR; };
54B51129216C3D840033A6F3 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
@@ -39,6 +45,8 @@
54B51130216C3D860033A6F3 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
54B51133216C3D860033A6F3 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
54B51135216C3D860033A6F3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
54E26CB524A989090066C720 /* Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utils.swift; sourceTree = "<group>"; };
54E26CB724A98E310066C720 /* DetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailViewController.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -46,6 +54,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
543844BD23D2BE2000D5EDE4 /* MapKit.framework in Frameworks */,
549D23D2233C77D5008EF4D7 /* FloatingPanel.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -53,12 +62,21 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
543844BB23D2BE1F00D5EDE4 /* Frameworks */ = {
isa = PBXGroup;
children = (
543844BC23D2BE2000D5EDE4 /* MapKit.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
54B5111D216C3D840033A6F3 = {
isa = PBXGroup;
children = (
549D23D1233C77D5008EF4D7 /* FloatingPanel.framework */,
54B51128216C3D840033A6F3 /* Maps */,
54B51127216C3D840033A6F3 /* Products */,
543844BB23D2BE1F00D5EDE4 /* Frameworks */,
);
sourceTree = "<group>";
};
@@ -75,6 +93,9 @@
children = (
54B51129216C3D840033A6F3 /* AppDelegate.swift */,
54B5112B216C3D840033A6F3 /* ViewController.swift */,
549A5F58244673FE0025F312 /* SearchViewController.swift */,
54E26CB724A98E310066C720 /* DetailViewController.swift */,
54E26CB524A989090066C720 /* Utils.swift */,
54B5112D216C3D840033A6F3 /* Main.storyboard */,
54B51130216C3D860033A6F3 /* Assets.xcassets */,
54B51132216C3D860033A6F3 /* LaunchScreen.storyboard */,
@@ -155,8 +176,11 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
549A5F59244673FE0025F312 /* SearchViewController.swift in Sources */,
54B5112C216C3D840033A6F3 /* ViewController.swift in Sources */,
54E26CB824A98E310066C720 /* DetailViewController.swift in Sources */,
54B5112A216C3D840033A6F3 /* AppDelegate.swift in Sources */,
54E26CB624A989090066C720 /* Utils.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -305,7 +329,7 @@
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = "";
INFOPLIST_FILE = Maps/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -324,7 +348,7 @@
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = "";
INFOPLIST_FILE = Maps/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
+1 -7
View File
@@ -1,10 +1,4 @@
//
// AppDelegate.swift
// Maps
//
// Created by Shin Yamamoto on 2018/10/09.
// Copyright © 2018 Shin Yamamoto. All rights reserved.
//
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
import UIKit
+53 -17
View File
@@ -1,11 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14313.18" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
<device id="retina5_9" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="16096" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
<device id="retina5_9" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14283.14"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16087"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
@@ -27,7 +25,7 @@
<rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
</view>
<blurEffect style="light"/>
<blurEffect style="prominent"/>
</visualEffectView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
@@ -51,18 +49,18 @@
</objects>
<point key="canvasLocation" x="136.80000000000001" y="133.00492610837438"/>
</scene>
<!--Search Panel View Controller-->
<!--Search View Controller-->
<scene sceneID="kXy-li-p3C">
<objects>
<viewController storyboardIdentifier="SearchPanel" useStoryboardIdentifierAsRestorationIdentifier="YES" id="0S1-Lk-JgE" customClass="SearchPanelViewController" customModule="Maps" customModuleProvider="target" sceneMemberID="viewController">
<viewController storyboardIdentifier="SearchViewController" useStoryboardIdentifierAsRestorationIdentifier="YES" id="0S1-Lk-JgE" customClass="SearchViewController" customModule="Maps" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="Ncl-E9-yRn">
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<visualEffectView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Ye3-uU-bq3">
<rect key="frame" x="0.0" y="0.0" width="375" height="900"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="ED1-gT-FBj">
<rect key="frame" x="0.0" y="0.0" width="375" height="900"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<searchBar contentMode="redraw" searchBarStyle="minimal" translatesAutoresizingMaskIntoConstraints="NO" id="Zcj-SE-gb8">
@@ -70,7 +68,7 @@
<textInputTraits key="textInputTraits"/>
</searchBar>
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" showsHorizontalScrollIndicator="NO" showsVerticalScrollIndicator="NO" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="D7r-re-InH">
<rect key="frame" x="0.0" y="66" width="375" height="748"/>
<rect key="frame" x="0.0" y="66" width="375" height="746"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<view key="tableHeaderView" contentMode="scaleToFill" id="u28-LY-hIh" customClass="SearchHeaderView" customModule="Maps" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="375" height="116"/>
@@ -173,7 +171,7 @@
<rect key="frame" x="0.0" y="144" width="375" height="70"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="LzC-B9-Adb" id="evr-60-laS">
<rect key="frame" x="0.0" y="0.0" width="375" height="69.666666666666671"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="70"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="like" translatesAutoresizingMaskIntoConstraints="NO" id="GEk-yE-lLq">
@@ -185,7 +183,7 @@
</constraints>
</imageView>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" distribution="fillEqually" spacing="2" translatesAutoresizingMaskIntoConstraints="NO" id="Gfl-Oy-rsy">
<rect key="frame" x="57" y="11.999999999999996" width="303" height="45.666666666666657"/>
<rect key="frame" x="57" y="12" width="303" height="46"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" tag="1" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Favorites" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Spf-8L-Ne6">
<rect key="frame" x="0.0" y="0.0" width="303" height="22"/>
@@ -194,7 +192,7 @@
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="0 Places" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="gyo-3V-7U8">
<rect key="frame" x="0.0" y="24" width="303" height="21.666666666666671"/>
<rect key="frame" x="0.0" y="24" width="303" height="22"/>
<fontDescription key="fontDescription" type="system" pointSize="13"/>
<color key="textColor" red="0.57647058819999997" green="0.57647058819999997" blue="0.57647058819999997" alpha="1" colorSpace="calibratedRGB"/>
<nil key="highlightedColor"/>
@@ -227,18 +225,18 @@
<constraint firstAttribute="trailing" secondItem="D7r-re-InH" secondAttribute="trailing" id="BES-GA-Btp"/>
<constraint firstItem="D7r-re-InH" firstAttribute="leading" secondItem="ED1-gT-FBj" secondAttribute="leading" id="UTe-YL-17h"/>
<constraint firstItem="Zcj-SE-gb8" firstAttribute="top" secondItem="ED1-gT-FBj" secondAttribute="top" constant="6" id="apU-Pd-PEO"/>
<constraint firstAttribute="bottom" secondItem="D7r-re-InH" secondAttribute="bottom" constant="86" id="vfS-Lx-TXz"/>
<constraint firstAttribute="bottom" secondItem="D7r-re-InH" secondAttribute="bottom" id="vfS-Lx-TXz"/>
<constraint firstItem="D7r-re-InH" firstAttribute="top" secondItem="Zcj-SE-gb8" secondAttribute="bottom" constant="4" id="vro-cd-B9c"/>
<constraint firstItem="Zcj-SE-gb8" firstAttribute="leading" secondItem="ED1-gT-FBj" secondAttribute="leading" id="wMb-L2-Z0W"/>
</constraints>
</view>
<blurEffect style="extraLight"/>
<blurEffect style="prominent"/>
</visualEffectView>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="Ye3-uU-bq3" firstAttribute="leading" secondItem="G74-X7-Za8" secondAttribute="leading" id="Kr2-sU-ZWZ"/>
<constraint firstItem="Ye3-uU-bq3" firstAttribute="bottom" secondItem="Ncl-E9-yRn" secondAttribute="bottom" constant="88" id="aWM-s3-3o4"/>
<constraint firstItem="Ye3-uU-bq3" firstAttribute="bottom" secondItem="Ncl-E9-yRn" secondAttribute="bottom" id="aWM-s3-3o4"/>
<constraint firstItem="Ye3-uU-bq3" firstAttribute="trailing" secondItem="G74-X7-Za8" secondAttribute="trailing" id="fEL-8y-Acc"/>
<constraint firstItem="Ye3-uU-bq3" firstAttribute="top" secondItem="Ncl-E9-yRn" secondAttribute="top" id="w77-ba-FrJ"/>
</constraints>
@@ -254,6 +252,44 @@
</objects>
<point key="canvasLocation" x="789.60000000000002" y="133.5832083958021"/>
</scene>
<!--Detail View Controller-->
<scene sceneID="5tH-Ya-PzB">
<objects>
<viewController storyboardIdentifier="DetailViewController" id="Tp2-MF-IFz" customClass="DetailViewController" customModule="Maps" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="FmO-AT-4Y7">
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<visualEffectView opaque="NO" contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="c3d-2e-0b1">
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="9fL-a5-0LS">
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" showsHorizontalScrollIndicator="NO" showsVerticalScrollIndicator="NO" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="kP7-56-wlG">
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</tableView>
</subviews>
<constraints>
<constraint firstItem="kP7-56-wlG" firstAttribute="leading" secondItem="9fL-a5-0LS" secondAttribute="leading" id="6gf-GO-cnv"/>
<constraint firstAttribute="bottom" secondItem="kP7-56-wlG" secondAttribute="bottom" id="WrH-tz-UQF"/>
<constraint firstItem="kP7-56-wlG" firstAttribute="top" secondItem="9fL-a5-0LS" secondAttribute="top" id="aJk-bz-lVc"/>
<constraint firstAttribute="trailing" secondItem="kP7-56-wlG" secondAttribute="trailing" id="sE0-9V-Rot"/>
</constraints>
</view>
<blurEffect style="extraLight"/>
</visualEffectView>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<viewLayoutGuide key="safeArea" id="ctv-Dd-JUc"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="EDp-D2-xcT" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1447" y="133"/>
</scene>
</scenes>
<resources>
<image name="food" width="60" height="60"/>
@@ -0,0 +1,7 @@
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
import UIKit
class DetailViewController: UIViewController {
var item: LocationItem?
}
@@ -0,0 +1,181 @@
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
import UIKit
// MARK: - UITableViewDelegate
extension ViewController: UITableViewDelegate {
func setUpSearchView() {
searchVC.loadViewIfNeeded()
searchVC.tableView.delegate = self
searchVC.searchBar.placeholder = "Search for a place or address"
let isPad = (traitCollection.userInterfaceIdiom == .pad)
searchVC.items = [
.init(mark: "mark", title: "Marked Location" + (isPad ? " (Left panel)" : ""), subtitle: "Golden Gate Bridge, San Francisco"),
.init(mark: "mark", title: "Marked Location" + (isPad ? " (Right panel)" : ""), subtitle: "San Francisco Museum of Modern Art"),
]
searchVC.items.append(contentsOf: (0...98).map {
.init(mark: "like", title: "Favorites", subtitle: "\($0) Places")
})
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
// Show a detail panel
switch indexPath.row {
case 0:
detailVC.item = searchVC.items[safe: 0]
// Show detail vc in the left positioned panel
switch traitCollection.userInterfaceIdiom {
case .pad:
detailFpc.layout = DetailPanelPadLeftLayout()
detailFpc.surfaceView.containerMargins = UIEdgeInsets(top: 0.0, left: 16.0, bottom: 0.0, right: 0.0)
default:
detailFpc.layout = DetailPanelPhoneLayout()
detailFpc.surfaceView.containerMargins = .zero
}
detailFpc.addPanel(toParent: self, animated: true)
case 1:
detailVC.item = searchVC.items[safe: 1]
// Show detail vc in the right positioned panel
switch traitCollection.userInterfaceIdiom {
case .pad:
detailFpc.layout = DetailPanelPadRightLayout()
detailFpc.surfaceView.containerMargins = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 16.0)
default:
detailFpc.layout = DetailPanelPhoneLayout()
detailFpc.surfaceView.containerMargins = .zero
}
detailFpc.addPanel(toParent: self, animated: true)
default:
break
}
}
}
// MARK: - Models
struct LocationItem {
let mark: String
let title: String
let subtitle: String
init(mark: String, title: String, subtitle: String) {
self.mark = mark
self.title = title
self.subtitle = subtitle
}
}
// MARK: -
class SearchViewController: UIViewController, UITableViewDataSource {
@IBOutlet weak var tableView: UITableView!
@IBOutlet weak var searchBar: UISearchBar!
@IBOutlet weak var visualEffectView: UIVisualEffectView!
var items: [LocationItem] = []
// For iOS 10 only
private lazy var shadowLayer: CAShapeLayer = CAShapeLayer()
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
searchBar.setSearchText(fontSize: 15.0)
hideHeader(animated: false)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
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
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
if let cell = cell as? SearchCell, let item = items[safe: indexPath.row] {
cell.iconImageView.image = UIImage(named: item.mark)
cell.titleLabel.text = item.title
cell.subTitleLabel.text = item.subtitle
}
return cell
}
func showHeader(animated: Bool) {
changeHeader(height: 116.0, aniamted: animated)
}
func hideHeader(animated: Bool) {
changeHeader(height: 0.0, aniamted: animated)
}
private func changeHeader(height: CGFloat, aniamted: Bool) {
guard let headerView = tableView.tableHeaderView, headerView.bounds.height != height else { return }
if aniamted == false {
updateHeader(height: height)
return
}
tableView.beginUpdates()
UIView.animate(withDuration: 0.25) {
self.updateHeader(height: height)
}
tableView.endUpdates()
}
private func updateHeader(height: CGFloat) {
guard let headerView = tableView.tableHeaderView else { return }
var frame = headerView.frame
frame.size.height = height
self.tableView.tableHeaderView?.frame = frame
}
}
class SearchCell: UITableViewCell {
@IBOutlet weak var iconImageView: UIImageView!
@IBOutlet weak var titleLabel: UILabel!
@IBOutlet weak var subTitleLabel: UILabel!
}
class SearchHeaderView: UIView {
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.clipsToBounds = true
}
}
extension UISearchBar {
func setSearchText(fontSize: CGFloat) {
if #available(iOS 13, *) {
let font = searchTextField.font
searchTextField.font = font?.withSize(fontSize)
} else {
let textField = value(forKey: "_searchField") as! UITextField
textField.font = textField.font?.withSize(fontSize)
}
}
}
+19
View File
@@ -0,0 +1,19 @@
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
import UIKit
extension Collection {
subscript (safe index: Index) -> Element? {
return indices.contains(index) ? self[index] : nil
}
}
extension UIViewController {
var isLandscape: Bool {
if #available(iOS 13.0, *) {
return view.window?.windowScene?.interfaceOrientation.isLandscape ?? false
} else {
return UIApplication.shared.statusBarOrientation.isLandscape
}
}
}
+351 -240
View File
@@ -1,46 +1,51 @@
//
// Copyright © 2018 Shin Yamamoto. All rights reserved.
//
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
import UIKit
import MapKit
import FloatingPanel
class ViewController: UIViewController, MKMapViewDelegate, UISearchBarDelegate, FloatingPanelControllerDelegate {
var fpc: FloatingPanelController!
var searchVC: SearchPanelViewController!
class ViewController: UIViewController {
typealias PanelDelegate = FloatingPanelControllerDelegate & UIGestureRecognizerDelegate
// Search Panel
lazy var fpc = FloatingPanelController()
lazy var fpcDelegate: PanelDelegate =
(traitCollection.userInterfaceIdiom == .pad) ? SearchPanelPadDelegate(owner: self) : SearchPanelPhoneDelegate(owner: self)
lazy var searchVC =
storyboard?.instantiateViewController(withIdentifier: "SearchViewController") as! SearchViewController
// Detail Panel
lazy var detailFpc = FloatingPanelController()
lazy var detailFpcDelegate: PanelDelegate =
(traitCollection.userInterfaceIdiom == .pad) ? DetailPanelPadDelegate(owner: self) : DetailPanelPhoneDelegate(owner: self)
lazy var detailVC =
storyboard?.instantiateViewController(withIdentifier: "DetailViewController") as! DetailViewController
@IBOutlet weak var mapView: MKMapView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
// Initialize FloatingPanelController
fpc = FloatingPanelController()
fpc.delegate = self
// Initialize FloatingPanelController and add the view
fpc.surfaceView.backgroundColor = .clear
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
// Set a content view controller
fpc.contentMode = .fitToBounds
fpc.delegate = fpcDelegate
fpc.set(contentViewController: searchVC)
fpc.track(scrollView: searchVC.tableView)
detailFpc.isRemovalInteractionEnabled = true
detailFpc.set(contentViewController: detailVC)
switch traitCollection.userInterfaceIdiom {
case .pad:
layoutPanelForPad()
default:
layoutPanelForPhone()
}
setupMapView()
setUpSearchView()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// Add FloatingPanel to a view with animation.
fpc.addPanel(toParent: self, animated: true)
// Must be here
searchVC.searchBar.delegate = self
@@ -51,6 +56,327 @@ class ViewController: UIViewController, MKMapViewDelegate, UISearchBarDelegate,
teardownMapView()
}
func layoutPanelForPad() {
fpc.behavior = SearchPaneliPadBehavior()
fpc.panGestureRecognizer.delegateProxy = fpcDelegate
// Not use addPanel(toParent:) because of the Auto Layout configuration of fpc.view.
view.addSubview(fpc.view)
addChild(fpc)
fpc.view.frame = view.bounds // Needed for a correct safe area configuration
fpc.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
fpc.view.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 0.0),
fpc.view.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 0.0 ),
fpc.view.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0.0),
fpc.view.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor, constant: 0.0),
])
fpc.show(animated: false) { [weak self] in
guard let self = self else { return }
self.didMove(toParent: self)
}
fpc.setAppearanceForPad()
detailFpc.setAppearanceForPad()
}
func layoutPanelForPhone() {
fpc.track(scrollView: searchVC.tableView) // Only track the tabvle view on iPhone
fpc.addPanel(toParent: self, animated: true)
fpc.setApearanceForPhone()
detailFpc.setApearanceForPhone()
}
}
extension FloatingPanelController {
func setApearanceForPhone() {
let appearance = SurfaceAppearance()
appearance.cornerRadius = 8.0
appearance.backgroundColor = .clear
surfaceView.appearance = appearance
}
func setAppearanceForPad() {
view.clipsToBounds = false
let appearance = SurfaceAppearance()
appearance.cornerRadius = 8.0
let shadow = SurfaceAppearance.Shadow()
shadow.color = UIColor.black
shadow.offset = CGSize(width: 0, height: 16)
shadow.radius = 16
shadow.spread = 8
appearance.shadows = [shadow]
appearance.backgroundColor = .clear
surfaceView.appearance = appearance
}
}
// MARK: - UISearchBarDelegate
extension ViewController: UISearchBarDelegate {
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
searchBar.resignFirstResponder()
searchBar.showsCancelButton = false
searchVC.hideHeader(animated: true)
UIView.animate(withDuration: 0.25) {
self.fpc.move(to: .half, animated: false)
}
}
func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
searchBar.showsCancelButton = true
searchVC.showHeader(animated: true)
searchVC.tableView.alpha = 1.0
UIView.animate(withDuration: 0.25) { [weak self] in
self?.fpc.move(to: .full, animated: false)
}
}
}
// MARK: - iPhone
class SearchPanelPhoneDelegate: NSObject, FloatingPanelControllerDelegate, UIGestureRecognizerDelegate {
unowned let owner: ViewController
init(owner: ViewController) {
self.owner = owner
}
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout {
switch newCollection.verticalSizeClass {
case .compact:
let appearance = vc.surfaceView.appearance
appearance.borderWidth = 1.0 / owner.traitCollection.displayScale
appearance.borderColor = UIColor.black.withAlphaComponent(0.2)
vc.surfaceView.appearance = appearance
return SearchPanelLandscapeLayout()
default:
let appearance = vc.surfaceView.appearance
appearance.borderWidth = 0.0
appearance.borderColor = nil
vc.surfaceView.appearance = appearance
return FloatingPanelBottomLayout()
}
}
func floatingPanelDidMove(_ vc: FloatingPanelController) {
debugPrint("surfaceLocation: ", vc.surfaceLocation)
let loc = vc.surfaceLocation
if vc.isAttracting == false {
let minY = vc.surfaceLocation(for: .full).y - 6.0
let maxY = vc.surfaceLocation(for: .tip).y + 6.0
vc.surfaceLocation = CGPoint(x: loc.x, y: min(max(loc.y, minY), maxY))
}
let tipY = vc.surfaceLocation(for: .tip).y
if loc.y > tipY - 44.0 {
let progress = max(0.0, min((tipY - loc.y) / 44.0, 1.0))
owner.searchVC.tableView.alpha = progress
} else {
owner.searchVC.tableView.alpha = 1.0
}
debugPrint("NearbyState : ",vc.nearbyState)
}
func floatingPanelWillBeginDragging(_ vc: FloatingPanelController) {
if vc.state == .full {
owner.searchVC.searchBar.showsCancelButton = false
owner.searchVC.searchBar.resignFirstResponder()
}
}
func floatingPanelWillEndDragging(_ vc: FloatingPanelController, withVelocity velocity: CGPoint, targetState: UnsafeMutablePointer<FloatingPanelState>) {
if targetState.pointee != .full {
owner.searchVC.hideHeader(animated: true)
}
if targetState.pointee == .tip {
vc.contentMode = .static
}
}
func floatingPanelDidEndAttracting(_ fpc: FloatingPanelController) {
fpc.contentMode = .fitToBounds
}
}
class SearchPanelLandscapeLayout: FloatingPanelLayout {
let position: FloatingPanelPosition = .bottom
let initialState: FloatingPanelState = .tip
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
return [
.full: FloatingPanelLayoutAnchor(absoluteInset: 16.0, edge: .top, referenceGuide: .safeArea),
.tip: FloatingPanelLayoutAnchor(absoluteInset: 69.0, edge: .bottom, referenceGuide: .safeArea),
]
}
func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint] {
if #available(iOS 11.0, *) {
return [
surfaceView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 8.0),
surfaceView.widthAnchor.constraint(equalToConstant: 291),
]
} else {
return [
surfaceView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 8.0),
surfaceView.widthAnchor.constraint(equalToConstant: 291),
]
}
}
func backdropAlpha(for state: FloatingPanelState) -> CGFloat {
return 0.0
}
}
class DetailPanelPhoneDelegate: NSObject, FloatingPanelControllerDelegate, UIGestureRecognizerDelegate {
unowned let owner: ViewController
init(owner: ViewController) {
self.owner = owner
}
}
class DetailPanelPhoneLayout: FloatingPanelLayout {
let position: FloatingPanelPosition = .bottom
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
return [
.full: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .safeArea),
]
}
let initialState: FloatingPanelState = .full
func backdropAlpha(for state: FloatingPanelState) -> CGFloat {
return 0.0
}
}
// MARK: - iPad
class SearchPanelPadDelegate: NSObject, FloatingPanelControllerDelegate, UIGestureRecognizerDelegate {
unowned let owner: ViewController
init(owner: ViewController) {
self.owner = owner
}
func floatingPanel(_ fpc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout {
if newCollection.horizontalSizeClass == .compact {
fpc.surfaceView.containerMargins = .zero
return FloatingPanelBottomLayout()
}
fpc.surfaceView.containerMargins = UIEdgeInsets(top: .leastNonzeroMagnitude, // For top left/right rounding corners
left: 16,
bottom: 0.0,
right: 0.0)
return SearchPanelPadLayout()
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return false
}
func floatingPanelWillBeginDragging(_ vc: FloatingPanelController) {
if vc.state == .full {
owner.searchVC.searchBar.showsCancelButton = false
owner.searchVC.searchBar.resignFirstResponder()
}
}
func floatingPanelWillEndDragging(_ vc: FloatingPanelController, withVelocity velocity: CGPoint, targetState: UnsafeMutablePointer<FloatingPanelState>) {
if targetState.pointee != .full {
owner.searchVC.hideHeader(animated: true)
}
}
}
class SearchPanelPadLayout: FloatingPanelLayout {
let position: FloatingPanelPosition = .top
let initialState: FloatingPanelState = .tip
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
return [
.tip: FloatingPanelLayoutAnchor(absoluteInset: 80.0, edge: .top, referenceGuide: .superview),
.half: FloatingPanelLayoutAnchor(absoluteInset: 200.0, edge: .top, referenceGuide: .superview),
.full: FloatingPanelLayoutAnchor(absoluteInset: 60.0, edge: .bottom, referenceGuide: .superview),
]
}
func backdropAlpha(for state: FloatingPanelState) -> CGFloat {
return 0.0
}
func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint] {
return [
surfaceView.leftAnchor.constraint(equalTo: view.leftAnchor),
surfaceView.widthAnchor.constraint(equalToConstant: 375),
]
}
}
class SearchPaneliPadBehavior: FloatingPanelBehavior {
var springDecelerationRate: CGFloat {
return UIScrollView.DecelerationRate.fast.rawValue - 0.003
}
var springResponseTime: CGFloat {
return 0.3
}
var momentumProjectionRate: CGFloat {
return UIScrollView.DecelerationRate.fast.rawValue
}
func shouldProjectMomentum(_ fpc: FloatingPanelController, to proposedTargetPosition: FloatingPanelState) -> Bool {
return true
}
}
class DetailPanelPadDelegate: NSObject, FloatingPanelControllerDelegate, UIGestureRecognizerDelegate {
unowned let owner: ViewController
init(owner: ViewController) {
self.owner = owner
}
func floatingPanel(_ fpc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout {
if newCollection.horizontalSizeClass == .compact {
fpc.surfaceView.containerMargins = .zero
return FloatingPanelBottomLayout()
}
if let item = owner.detailVC.item, item.title.contains("Right") {
fpc.surfaceView.containerMargins = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: .leastNonzeroMagnitude)
return DetailPanelPadRightLayout()
}
fpc.surfaceView.containerMargins = UIEdgeInsets(top: 0.0, left: .leastNonzeroMagnitude, bottom: 0.0, right: 0.0)
return DetailPanelPadLeftLayout()
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return false
}
}
class DetailPanelPadLeftLayout: FloatingPanelLayout {
let position: FloatingPanelPosition = .left
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
return [
.full: FloatingPanelLayoutAnchor(absoluteInset: 375, edge: .left, referenceGuide: .superview)
]
}
let initialState: FloatingPanelState = .full
func backdropAlpha(for state: FloatingPanelState) -> CGFloat {
return 0.0
}
}
class DetailPanelPadRightLayout: FloatingPanelLayout {
let position: FloatingPanelPosition = .right
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
return [
.full: FloatingPanelLayoutAnchor(absoluteInset: 375, edge: .right, referenceGuide: .superview)
]
}
let initialState: FloatingPanelState = .full
func backdropAlpha(for state: FloatingPanelState) -> CGFloat {
return 0.0
}
}
// MARK: - MKMapViewDelegate
extension ViewController: MKMapViewDelegate {
func setupMapView() {
let center = CLLocationCoordinate2D(latitude: 37.623198015869235,
longitude: -122.43066818432008)
@@ -68,219 +394,4 @@ class ViewController: UIViewController, MKMapViewDelegate, UISearchBarDelegate,
mapView.delegate = nil
mapView = nil
}
// MARK: UISearchBarDelegate
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
searchBar.resignFirstResponder()
searchBar.showsCancelButton = false
searchVC.hideHeader()
fpc.move(to: .half, animated: true)
}
func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
searchBar.showsCancelButton = true
searchVC.showHeader()
searchVC.tableView.alpha = 1.0
fpc.move(to: .full, animated: true)
}
// MARK: FloatingPanelControllerDelegate
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? {
switch newCollection.verticalSizeClass {
case .compact:
fpc.surfaceView.borderWidth = 1.0 / traitCollection.displayScale
fpc.surfaceView.borderColor = UIColor.black.withAlphaComponent(0.2)
return SearchPanelLandscapeLayout()
default:
fpc.surfaceView.borderWidth = 0.0
fpc.surfaceView.borderColor = nil
return nil
}
}
func floatingPanelDidMove(_ vc: FloatingPanelController) {
let y = vc.surfaceView.frame.origin.y
let tipY = vc.originYOfSurface(for: .tip)
if y > tipY - 44.0 {
let progress = max(0.0, min((tipY - y) / 44.0, 1.0))
self.searchVC.tableView.alpha = progress
}
}
func floatingPanelWillBeginDragging(_ vc: FloatingPanelController) {
if vc.position == .full {
searchVC.searchBar.showsCancelButton = false
searchVC.searchBar.resignFirstResponder()
}
}
func floatingPanelDidEndDragging(_ vc: FloatingPanelController, withVelocity velocity: CGPoint, targetPosition: FloatingPanelPosition) {
if targetPosition != .full {
searchVC.hideHeader()
}
UIView.animate(withDuration: 0.25,
delay: 0.0,
options: .allowUserInteraction,
animations: {
if targetPosition == .tip {
self.searchVC.tableView.alpha = 0.0
} else {
self.searchVC.tableView.alpha = 1.0
}
}, completion: nil)
}
}
class SearchPanelViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
@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
tableView.delegate = self
searchBar.placeholder = "Search for a place or address"
searchBar.setSearchText(fontSize: 15.0)
hideHeader()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
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
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 2
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
if let cell = cell as? SearchCell {
switch indexPath.row {
case 0:
cell.iconImageView.image = UIImage(named: "mark")
cell.titleLabel.text = "Marked Location"
cell.subTitleLabel.text = "Golden Gate Bridge, San Francisco"
case 1:
cell.iconImageView.image = UIImage(named: "like")
cell.titleLabel.text = "Favorites"
cell.subTitleLabel.text = "0 Places"
default:
break
}
}
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: false)
}
func showHeader() {
changeHeader(height: 116.0)
}
func hideHeader() {
changeHeader(height: 0.0)
}
func changeHeader(height: CGFloat) {
tableView.beginUpdates()
if let headerView = tableView.tableHeaderView {
UIView.animate(withDuration: 0.25) {
var frame = headerView.frame
frame.size.height = height
self.tableView.tableHeaderView?.frame = frame
}
}
tableView.endUpdates()
}
}
public class SearchPanelLandscapeLayout: FloatingPanelLayout {
public var initialPosition: FloatingPanelPosition {
return .tip
}
public var supportedPositions: Set<FloatingPanelPosition> {
return [.full, .tip]
}
public func insetFor(position: FloatingPanelPosition) -> CGFloat? {
switch position {
case .full: return 16.0
case .tip: return 69.0
default: return nil
}
}
public func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint] {
if #available(iOS 11.0, *) {
return [
surfaceView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 8.0),
surfaceView.widthAnchor.constraint(equalToConstant: 291),
]
} else {
return [
surfaceView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 8.0),
surfaceView.widthAnchor.constraint(equalToConstant: 291),
]
}
}
public func backdropAlphaFor(position: FloatingPanelPosition) -> CGFloat {
return 0.0
}
}
class SearchCell: UITableViewCell {
@IBOutlet weak var iconImageView: UIImageView!
@IBOutlet weak var titleLabel: UILabel!
@IBOutlet weak var subTitleLabel: UILabel!
}
class SearchHeaderView: UIView {
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.clipsToBounds = true
}
}
extension UISearchBar {
func setSearchText(fontSize: CGFloat) {
#if swift(>=5.1) // Xcode 11 or later
let font = searchTextField.font
searchTextField.font = font?.withSize(fontSize)
#else
let textField = value(forKey: "_searchField") as! UITextField
textField.font = textField.font?.withSize(fontSize)
#endif
}
}
@@ -294,7 +294,7 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/usr/libexec/PlistBuddy -c \"Set :CFBundleVersion $(git rev-parse --abbrev-ref HEAD)($(git rev-parse --short HEAD))\" $SRCROOT/$INFOPLIST_FILE\n";
shellScript = "/usr/libexec/PlistBuddy -c \"Set :CFBundleVersion $(git rev-parse --abbrev-ref HEAD)($(git rev-parse --short HEAD))\" \"$SRCROOT/$INFOPLIST_FILE\"\n";
};
/* End PBXShellScriptBuildPhase section */
+1 -4
View File
@@ -1,7 +1,4 @@
//
// Created by Shin Yamamoto on 2018/09/18.
// Copyright © 2018 Shin Yamamoto. All rights reserved.
//
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
import UIKit
@@ -1,19 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14460.31" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="RoN-h0-uBD">
<device id="retina5_9" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17156" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="RoN-h0-uBD">
<device id="retina5_9" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14460.20"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17125"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="Stack View standard spacing" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Navigation Controller-->
<scene sceneID="Cjh-iX-VQw">
<objects>
<navigationController id="RoN-h0-uBD" sceneMemberID="viewController">
<navigationController storyboardIdentifier="RootNavigationController" id="RoN-h0-uBD" sceneMemberID="viewController">
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="hNW-5m-Omi">
<rect key="frame" x="0.0" y="44" width="375" height="96"/>
<autoresizingMask key="autoresizingMask"/>
@@ -46,7 +45,7 @@
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="M0G-C8-hAO">
<rect key="frame" x="15" y="0.0" width="345" height="43.666667938232422"/>
<rect key="frame" x="16" y="0.0" width="343" height="43.666667938232422"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
@@ -58,6 +57,7 @@
</prototypes>
</tableView>
</subviews>
<viewLayoutGuide key="safeArea" id="39L-Nq-qfp"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="7IS-PU-x0P" firstAttribute="top" secondItem="Smh-Bd-AAc" secondAttribute="top" id="6yd-jv-ey3"/>
@@ -65,7 +65,6 @@
<constraint firstItem="7IS-PU-x0P" firstAttribute="bottom" secondItem="Smh-Bd-AAc" secondAttribute="bottom" id="fNW-DP-lhV"/>
<constraint firstItem="7IS-PU-x0P" firstAttribute="trailing" secondItem="39L-Nq-qfp" secondAttribute="trailing" id="vfY-Rc-FOI"/>
</constraints>
<viewLayoutGuide key="safeArea" id="39L-Nq-qfp"/>
</view>
<navigationItem key="navigationItem" title="Samples" id="wCF-su-7up">
<barButtonItem key="rightBarButtonItem" title="Settings" id="rbH-U3-XyA">
@@ -90,66 +89,76 @@
<rect key="frame" x="0.0" y="0.0" width="375" height="197.33333333333334"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" distribution="fillProportionally" alignment="center" spacing="16" translatesAutoresizingMaskIntoConstraints="NO" id="n93-ZL-fmC">
<rect key="frame" x="32" y="16" width="311" height="181.33333333333334"/>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" distribution="fillProportionally" spacing="16" translatesAutoresizingMaskIntoConstraints="NO" id="n93-ZL-fmC">
<rect key="frame" x="32" y="16" width="311" height="149.33333333333334"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Version: 1.0" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="WmC-Tq-NDN">
<rect key="frame" x="118.33333333333334" y="0.0" width="74.333333333333343" height="21"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="UINavigationBar" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Ulg-gS-ah0">
<rect key="frame" x="90.666666666666686" y="37" width="130" height="25"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleHeadline"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="126" translatesAutoresizingMaskIntoConstraints="NO" id="uEf-g4-CeU">
<rect key="frame" x="23.333333333333343" y="78" width="264.66666666666663" height="38"/>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" distribution="fillProportionally" alignment="center" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="DCi-Iv-o6d">
<rect key="frame" x="0.0" y="0.0" width="311" height="52.666666666666664"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Large Titles" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ogl-S5-4tJ">
<rect key="frame" x="0.0" y="8.9999999999999982" width="89.666666666666671" height="20.333333333333329"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Version: 1.0" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="WmC-Tq-NDN">
<rect key="frame" x="118.33333333333334" y="0.0" width="74.333333333333343" height="17.333333333333332"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="UINavigationBar" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Ulg-gS-ah0">
<rect key="frame" x="78.333333333333329" y="25.333333333333336" width="154.66666666666669" height="27.333333333333336"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleTitle2"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="js8-Qv-lUC">
<rect key="frame" x="215.66666666666666" y="3.6666666666666714" width="50.999999999999972" height="31"/>
<connections>
<action selector="toggleLargeTitle:" destination="C1X-9Z-TyQ" eventType="valueChanged" id="FJS-Ty-mCY"/>
</connections>
</switch>
</subviews>
</stackView>
<stackView opaque="NO" contentMode="scaleToFill" spacing="126" translatesAutoresizingMaskIntoConstraints="NO" id="ZtZ-Dz-4cC">
<rect key="frame" x="23.333333333333343" y="132" width="264.66666666666663" height="49.333333333333343"/>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" distribution="fillProportionally" spacing="16" translatesAutoresizingMaskIntoConstraints="NO" id="J8j-7w-yCZ">
<rect key="frame" x="0.0" y="68.666666666666657" width="311" height="80.666666666666657"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Translucent" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Z5i-rm-QgL">
<rect key="frame" x="0.0" y="0.0" width="89.666666666666671" height="49.333333333333336"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="s6b-j9-8Kw">
<rect key="frame" x="215.66666666666666" y="0.0" width="50.999999999999972" height="49.333333333333336"/>
<connections>
<action selector="toggleTranslucent:" destination="C1X-9Z-TyQ" eventType="valueChanged" id="nL4-3L-9hh"/>
</connections>
</switch>
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacingType="standard" translatesAutoresizingMaskIntoConstraints="NO" id="uEf-g4-CeU">
<rect key="frame" x="0.0" y="0.0" width="311" height="32"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Large Titles" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ogl-S5-4tJ">
<rect key="frame" x="0.0" y="5.9999999999999982" width="254" height="20.333333333333329"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleHeadline"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="js8-Qv-lUC">
<rect key="frame" x="262" y="0.66666666666665719" width="51" height="31"/>
<connections>
<action selector="toggleLargeTitle:" destination="C1X-9Z-TyQ" eventType="valueChanged" id="FJS-Ty-mCY"/>
</connections>
</switch>
</subviews>
</stackView>
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacingType="standard" translatesAutoresizingMaskIntoConstraints="NO" id="ZtZ-Dz-4cC">
<rect key="frame" x="0.0" y="47.999999999999986" width="311" height="32.666666666666671"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Translucent" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Z5i-rm-QgL">
<rect key="frame" x="0.0" y="6.333333333333341" width="254" height="20.333333333333329"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleHeadline"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="s6b-j9-8Kw">
<rect key="frame" x="262" y="1" width="51" height="31"/>
<connections>
<action selector="toggleTranslucent:" destination="C1X-9Z-TyQ" eventType="valueChanged" id="nL4-3L-9hh"/>
</connections>
</switch>
</subviews>
</stackView>
</subviews>
</stackView>
</subviews>
</stackView>
</subviews>
<viewLayoutGuide key="safeArea" id="0hr-ty-yWm"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="0hr-ty-yWm" firstAttribute="bottom" secondItem="n93-ZL-fmC" secondAttribute="bottom" id="2Ey-ou-E1M"/>
<constraint firstAttribute="bottom" secondItem="n93-ZL-fmC" secondAttribute="bottom" constant="32" id="2Ey-ou-E1M"/>
<constraint firstAttribute="trailing" secondItem="n93-ZL-fmC" secondAttribute="trailing" constant="32" id="DdZ-eB-F5s"/>
<constraint firstItem="n93-ZL-fmC" firstAttribute="leading" secondItem="af9-Zr-Ppc" secondAttribute="leading" constant="32" id="TyK-GP-Ari"/>
<constraint firstItem="n93-ZL-fmC" firstAttribute="top" secondItem="af9-Zr-Ppc" secondAttribute="topMargin" constant="16" id="mbC-6H-z9M"/>
</constraints>
<viewLayoutGuide key="safeArea" id="0hr-ty-yWm"/>
</view>
<size key="freeformSize" width="375" height="197.33000000000001"/>
<connections>
@@ -170,7 +179,7 @@
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="IvG-yp-yzI">
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="IvG-yp-yzI">
<rect key="frame" x="20" y="44" width="39" height="30"/>
<state key="normal" title="Close"/>
<connections>
@@ -179,13 +188,13 @@
</connections>
</button>
</subviews>
<viewLayoutGuide key="safeArea" id="954-Dk-zvc"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="IvG-yp-yzI" firstAttribute="top" secondItem="954-Dk-zvc" secondAttribute="top" id="18k-sV-PgT"/>
<constraint firstItem="954-Dk-zvc" firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="IvG-yp-yzI" secondAttribute="trailing" id="mpr-u5-MZu"/>
<constraint firstItem="IvG-yp-yzI" firstAttribute="leading" secondItem="954-Dk-zvc" secondAttribute="leading" constant="20" id="pYt-jE-CTF"/>
</constraints>
<viewLayoutGuide key="safeArea" id="954-Dk-zvc"/>
</view>
<tabBarItem key="tabBarItem" tag="1" title="Layout 2" id="qb3-RB-B28"/>
</viewController>
@@ -201,7 +210,7 @@
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="NbG-e8-HdI">
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="NbG-e8-HdI">
<rect key="frame" x="20" y="44" width="39" height="30"/>
<state key="normal" title="Close"/>
<connections>
@@ -210,13 +219,13 @@
</connections>
</button>
</subviews>
<viewLayoutGuide key="safeArea" id="0ao-SI-QZW"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="0ao-SI-QZW" firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="NbG-e8-HdI" secondAttribute="trailing" id="K9F-6x-KWn"/>
<constraint firstItem="NbG-e8-HdI" firstAttribute="top" secondItem="0ao-SI-QZW" secondAttribute="top" id="nsE-so-rTl"/>
<constraint firstItem="NbG-e8-HdI" firstAttribute="leading" secondItem="0ao-SI-QZW" secondAttribute="leading" constant="20" id="sF4-Dm-aoY"/>
</constraints>
<viewLayoutGuide key="safeArea" id="0ao-SI-QZW"/>
</view>
<tabBarItem key="tabBarItem" tag="2" title="Layout 3" id="RJD-TF-Sdh"/>
</viewController>
@@ -232,7 +241,7 @@
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="eFN-tN-4Ct">
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="eFN-tN-4Ct">
<rect key="frame" x="20" y="44" width="39" height="30"/>
<state key="normal" title="Close"/>
<connections>
@@ -241,13 +250,13 @@
</connections>
</button>
</subviews>
<viewLayoutGuide key="safeArea" id="5Ns-4l-Ufg"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="eFN-tN-4Ct" firstAttribute="leading" secondItem="5Ns-4l-Ufg" secondAttribute="leading" constant="20" id="5BT-yZ-EKe"/>
<constraint firstItem="5Ns-4l-Ufg" firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="eFN-tN-4Ct" secondAttribute="trailing" id="OzZ-Dz-RNF"/>
<constraint firstItem="eFN-tN-4Ct" firstAttribute="top" secondItem="5Ns-4l-Ufg" secondAttribute="top" id="hUV-3a-XkY"/>
</constraints>
<viewLayoutGuide key="safeArea" id="5Ns-4l-Ufg"/>
</view>
<tabBarItem key="tabBarItem" title="Layout 1" id="HEV-kf-jxH"/>
</viewController>
@@ -264,20 +273,19 @@
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="1000" verticalHuggingPriority="1000" horizontalCompressionResistancePriority="1000" verticalCompressionResistancePriority="1000" text="Change this text" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ge4-RW-Gmz">
<rect key="frame" x="24" y="24" width="327" height="20.333333333333329"/>
<rect key="frame" x="125.66666666666669" y="24" width="124" height="20.333333333333329"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<viewLayoutGuide key="safeArea" id="ouu-g9-OiX"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="trailing" secondItem="ge4-RW-Gmz" secondAttribute="trailing" constant="24" id="V59-MD-Lcg"/>
<constraint firstItem="ge4-RW-Gmz" firstAttribute="leading" secondItem="eLM-xc-d9e" secondAttribute="leading" constant="24" id="hAO-P0-7Kw"/>
<constraint firstItem="ge4-RW-Gmz" firstAttribute="top" secondItem="eLM-xc-d9e" secondAttribute="top" constant="24" id="j0s-fd-MYj"/>
<constraint firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="ge4-RW-Gmz" secondAttribute="bottom" constant="24" id="tEn-PO-nVD"/>
<constraint firstItem="ge4-RW-Gmz" firstAttribute="centerX" secondItem="eLM-xc-d9e" secondAttribute="centerX" id="vh3-l7-uY8"/>
</constraints>
<viewLayoutGuide key="safeArea" id="ouu-g9-OiX"/>
</view>
<size key="freeformSize" width="375" height="778"/>
</viewController>
@@ -316,7 +324,7 @@
<rect key="frame" x="0.0" y="724" width="375" height="0.0"/>
<color key="backgroundColor" white="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</view>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="sbF-Az-7sy">
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="sbF-Az-7sy">
<rect key="frame" x="20" y="0.0" width="39" height="30"/>
<state key="normal" title="Close"/>
<connections>
@@ -326,35 +334,35 @@
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="top" spacing="44" translatesAutoresizingMaskIntoConstraints="NO" id="9p4-06-y2T">
<rect key="frame" x="134.66666666666666" y="88" width="106" height="326"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="i9x-x5-n1q">
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="i9x-x5-n1q">
<rect key="frame" x="0.0" y="0.0" width="80" height="30"/>
<state key="normal" title="Move to full"/>
<connections>
<action selector="moveToFullWithSender:" destination="bYI-y3-Rzb" eventType="touchUpInside" id="TDe-3J-gIR"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="2u5-cH-RAN">
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="2u5-cH-RAN">
<rect key="frame" x="0.0" y="74" width="85" height="30"/>
<state key="normal" title="Move to half"/>
<connections>
<action selector="moveToHalfWithSender:" destination="bYI-y3-Rzb" eventType="touchUpInside" id="12s-o7-Et5"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="M4A-iO-RIE">
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="M4A-iO-RIE">
<rect key="frame" x="0.0" y="148" width="77" height="30"/>
<state key="normal" title="Move to tip"/>
<connections>
<action selector="moveToTipWithSender:" destination="bYI-y3-Rzb" eventType="touchUpInside" id="BmL-91-9ai"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="swr-XM-GzZ">
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="swr-XM-GzZ">
<rect key="frame" x="0.0" y="222" width="106" height="30"/>
<state key="normal" title="Move to hidden"/>
<connections>
<action selector="moveToHiddenWithSender:" destination="bYI-y3-Rzb" eventType="touchUpInside" id="jfJ-0f-fdk"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="szf-HE-QTk">
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="szf-HE-QTk">
<rect key="frame" x="0.0" y="296" width="96" height="30"/>
<state key="normal" title="Update layout"/>
<connections>
@@ -364,6 +372,7 @@
</subviews>
</stackView>
</subviews>
<viewLayoutGuide key="safeArea" id="kjr-TP-fcM"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="sbF-Az-7sy" firstAttribute="top" secondItem="kjr-TP-fcM" secondAttribute="top" id="3VR-hj-zeQ"/>
@@ -375,7 +384,6 @@
<constraint firstItem="9p4-06-y2T" firstAttribute="centerX" secondItem="kjr-TP-fcM" secondAttribute="centerX" id="l8t-p3-ETf"/>
<constraint firstItem="vut-mK-Y4t" firstAttribute="top" secondItem="kjr-TP-fcM" secondAttribute="bottom" id="rMy-JT-t4B"/>
</constraints>
<viewLayoutGuide key="safeArea" id="kjr-TP-fcM"/>
</view>
<size key="freeformSize" width="375" height="778"/>
<connections>
@@ -468,6 +476,7 @@
</constraints>
</scrollView>
</subviews>
<viewLayoutGuide key="safeArea" id="ufS-Rf-F2F"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<gestureRecognizers/>
<constraints>
@@ -476,7 +485,6 @@
<constraint firstAttribute="bottom" secondItem="sBe-tN-uMi" secondAttribute="bottom" id="jzB-47-P7e"/>
<constraint firstItem="ufS-Rf-F2F" firstAttribute="trailing" secondItem="sBe-tN-uMi" secondAttribute="trailing" id="nHG-wg-pLP"/>
</constraints>
<viewLayoutGuide key="safeArea" id="ufS-Rf-F2F"/>
<connections>
<outletCollection property="gestureRecognizers" destination="tOa-bf-zGz" appends="YES" id="zle-Sz-M3U"/>
<outletCollection property="gestureRecognizers" destination="SCk-hG-weZ" appends="YES" id="OcK-FK-Lac"/>
@@ -527,7 +535,7 @@
<rect key="frame" x="0.0" y="44" width="375" height="734"/>
<color key="backgroundColor" red="0.0078431372550000003" green="0.72156862749999995" blue="0.45882352939999999" alpha="1" colorSpace="calibratedRGB"/>
</view>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="noi-1a-5bZ" customClass="CloseButton" customModule="Samples" customModuleProvider="target">
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="noi-1a-5bZ" customClass="CloseButton" customModule="Samples" customModuleProvider="target">
<rect key="frame" x="319" y="44" width="44" height="44"/>
<constraints>
<constraint firstAttribute="height" constant="44" id="0jg-5D-A1F"/>
@@ -537,17 +545,34 @@
<action selector="closeWithSender:" destination="YC8-ae-15L" eventType="touchUpInside" id="Z2v-19-S5k"/>
</connections>
</button>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="22" translatesAutoresizingMaskIntoConstraints="NO" id="tP3-oJ-4EB">
<rect key="frame" x="130.66666666666666" y="132" width="114" height="134"/>
<stackView opaque="NO" contentMode="scaleToFill" distribution="fillProportionally" alignment="center" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="qux-uG-4o2">
<rect key="frame" x="8" y="52" width="148" height="31"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="c5r-jU-haj">
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="fitToBounds" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="7lq-d3-PKi">
<rect key="frame" x="0.0" y="5.3333333333333357" width="91" height="20.333333333333332"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="0MA-lV-KjS">
<rect key="frame" x="99" y="0.0" width="51" height="31"/>
<connections>
<action selector="modeChanged:" destination="YC8-ae-15L" eventType="valueChanged" id="IQ8-u2-Rib"/>
</connections>
</switch>
</subviews>
</stackView>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="22" translatesAutoresizingMaskIntoConstraints="NO" id="tP3-oJ-4EB">
<rect key="frame" x="130.66666666666666" y="88" width="114" height="134"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="c5r-jU-haj">
<rect key="frame" x="0.0" y="0.0" width="114" height="30"/>
<state key="normal" title="Show"/>
<connections>
<action selector="buttonPressed:" destination="YC8-ae-15L" eventType="touchUpInside" id="Mi1-o6-TWt"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="wmd-ab-Nz3">
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="wmd-ab-Nz3">
<rect key="frame" x="0.0" y="52" width="114" height="30"/>
<state key="normal" title="Present Modallly"/>
<connections>
@@ -555,7 +580,7 @@
<segue destination="bYI-y3-Rzb" kind="presentation" identifier="PresentModallySegue" id="3yq-HE-Tgn"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="01L-lp-oy6">
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="01L-lp-oy6">
<rect key="frame" x="0.0" y="104" width="114" height="30"/>
<state key="normal" title="Update Layout"/>
<connections>
@@ -565,6 +590,7 @@
</subviews>
</stackView>
</subviews>
<viewLayoutGuide key="safeArea" id="aOK-7l-cA6"/>
<color key="backgroundColor" red="1" green="0.83234566450000003" blue="0.47320586440000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<gestureRecognizers/>
<constraints>
@@ -574,14 +600,15 @@
<constraint firstItem="8yw-OC-Ubk" firstAttribute="bottom" secondItem="g7l-kO-y7q" secondAttribute="bottom" id="JOL-wC-w74"/>
<constraint firstItem="8yw-OC-Ubk" firstAttribute="leading" secondItem="aOK-7l-cA6" secondAttribute="leading" id="RiJ-Hb-OOZ"/>
<constraint firstItem="8yw-OC-Ubk" firstAttribute="trailing" secondItem="aOK-7l-cA6" secondAttribute="trailing" id="Sof-yL-mwK"/>
<constraint firstItem="tP3-oJ-4EB" firstAttribute="top" secondItem="aOK-7l-cA6" secondAttribute="top" constant="88" id="Zhb-Ss-epe"/>
<constraint firstItem="tP3-oJ-4EB" firstAttribute="top" secondItem="g7l-kO-y7q" secondAttribute="top" constant="88" id="Zhb-Ss-epe"/>
<constraint firstItem="Kva-Z7-0qY" firstAttribute="trailing" secondItem="aOK-7l-cA6" secondAttribute="trailing" id="kkp-Yo-FQW"/>
<constraint firstItem="aOK-7l-cA6" firstAttribute="trailing" secondItem="noi-1a-5bZ" secondAttribute="trailing" constant="12" id="lv9-Nf-HNB"/>
<constraint firstItem="qux-uG-4o2" firstAttribute="top" secondItem="aOK-7l-cA6" secondAttribute="top" constant="8" id="naa-cf-ZIc"/>
<constraint firstItem="Kva-Z7-0qY" firstAttribute="leading" secondItem="aOK-7l-cA6" secondAttribute="leading" id="oVC-i1-TwS"/>
<constraint firstItem="aOK-7l-cA6" firstAttribute="bottom" secondItem="Kva-Z7-0qY" secondAttribute="bottom" id="rW2-mF-5DR"/>
<constraint firstItem="8yw-OC-Ubk" firstAttribute="top" relation="greaterThanOrEqual" secondItem="tP3-oJ-4EB" secondAttribute="bottom" constant="88" id="vKQ-h9-uKt"/>
<constraint firstItem="qux-uG-4o2" firstAttribute="leading" secondItem="g7l-kO-y7q" secondAttribute="leading" constant="8" id="zXb-R9-bMO"/>
</constraints>
<viewLayoutGuide key="safeArea" id="aOK-7l-cA6"/>
<connections>
<outletCollection property="gestureRecognizers" destination="6Ca-p8-7uF" appends="YES" id="xOy-f1-NZE"/>
<outletCollection property="gestureRecognizers" destination="SPY-Vr-XDT" appends="YES" id="vgS-Am-jhQ"/>
@@ -591,6 +618,8 @@
<size key="freeformSize" width="375" height="778"/>
<connections>
<outlet property="closeButton" destination="noi-1a-5bZ" id="eWQ-ha-8y7"/>
<outlet property="intrinsicHeightConstraint" destination="vKQ-h9-uKt" id="QpA-WD-b17"/>
<outlet property="modeChangeView" destination="qux-uG-4o2" id="1Nq-fE-dXw"/>
<segue destination="bYI-y3-Rzb" kind="show" identifier="ShowSegue" id="r1P-2i-NDe"/>
</connections>
</viewController>
@@ -611,7 +640,7 @@
</connections>
</pongPressGestureRecognizer>
</objects>
<point key="canvasLocation" x="655" y="734"/>
<point key="canvasLocation" x="653.60000000000002" y="733.74384236453204"/>
</scene>
<!--Debug Text View Controller-->
<scene sceneID="Bkq-O7-q4A">
@@ -667,6 +696,7 @@ Section 1.10.33 of "de Finibus Bonorum et Malorum", written by Cicero in 45 BC
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
</textView>
</subviews>
<viewLayoutGuide key="safeArea" id="5ET-zC-lCb"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="rN1-HL-YHv" firstAttribute="leading" secondItem="5ET-zC-lCb" secondAttribute="leading" id="7V3-KL-vXd"/>
@@ -674,7 +704,6 @@ Section 1.10.33 of "de Finibus Bonorum et Malorum", written by Cicero in 45 BC
<constraint firstItem="rN1-HL-YHv" firstAttribute="top" secondItem="9YG-0j-Zzg" secondAttribute="top" constant="17" id="fiO-LL-nSC"/>
<constraint firstItem="rN1-HL-YHv" firstAttribute="trailing" secondItem="5ET-zC-lCb" secondAttribute="trailing" id="lfg-EE-euw"/>
</constraints>
<viewLayoutGuide key="safeArea" id="5ET-zC-lCb"/>
</view>
<size key="freeformSize" width="375" height="778"/>
<connections>
@@ -688,6 +717,11 @@ Section 1.10.33 of "de Finibus Bonorum et Malorum", written by Cicero in 45 BC
<point key="canvasLocation" x="-1" y="734"/>
</scene>
</scenes>
<designables>
<designable name="noi-1a-5bZ">
<size key="intrinsicContentSize" width="30" height="30"/>
</designable>
</designables>
<inferredMetricsTieBreakers>
<segue reference="r1P-2i-NDe"/>
</inferredMetricsTieBreakers>
+1 -4
View File
@@ -1,7 +1,4 @@
//
// Created by Shin Yamamoto on 2018/09/19.
// Copyright © 2018 Shin Yamamoto. All rights reserved.
//
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
import UIKit
+1 -4
View File
@@ -1,7 +1,4 @@
//
// Created by Shin Yamamoto on 2018/10/08.
// Copyright © 2018 Shin Yamamoto. All rights reserved.
//
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
import UIKit
File diff suppressed because it is too large Load Diff
+1 -7
View File
@@ -1,10 +1,4 @@
//
// FloatingModalSampleTests.swift
// FloatingModalSampleTests
//
// Created by Shin Yamamoto on 2018/09/18.
// Copyright © 2018 Shin Yamamoto. All rights reserved.
//
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
import XCTest
@testable import FloatingPanelSample
+1 -4
View File
@@ -1,7 +1,4 @@
//
// Created by Shin Yamamoto on 2018/09/18.
// Copyright © 2018 Shin Yamamoto. All rights reserved.
//
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
import XCTest
@@ -0,0 +1,375 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 50;
objects = {
/* Begin PBXBuildFile section */
545BA70621BA3214007F7846 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 545BA70521BA3214007F7846 /* AppDelegate.m */; };
545BA70921BA3214007F7846 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 545BA70821BA3214007F7846 /* ViewController.m */; };
545BA70C21BA3214007F7846 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 545BA70A21BA3214007F7846 /* Main.storyboard */; };
545BA70E21BA3217007F7846 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 545BA70D21BA3217007F7846 /* Assets.xcassets */; };
545BA71121BA3217007F7846 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 545BA70F21BA3217007F7846 /* LaunchScreen.storyboard */; };
545BA71421BA3217007F7846 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 545BA71321BA3217007F7846 /* main.m */; };
545BA72621BA3BAF007F7846 /* FloatingPanel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 545BA72521BA3BAF007F7846 /* FloatingPanel.framework */; };
545BA72721BA3BAF007F7846 /* FloatingPanel.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 545BA72521BA3BAF007F7846 /* FloatingPanel.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
545BA72821BA3BAF007F7846 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
545BA72721BA3BAF007F7846 /* FloatingPanel.framework in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
545BA70121BA3214007F7846 /* SamplesObjC.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SamplesObjC.app; sourceTree = BUILT_PRODUCTS_DIR; };
545BA70421BA3214007F7846 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = "<group>"; };
545BA70521BA3214007F7846 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = "<group>"; };
545BA70721BA3214007F7846 /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = "<group>"; };
545BA70821BA3214007F7846 /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = "<group>"; };
545BA70B21BA3214007F7846 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
545BA70D21BA3217007F7846 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
545BA71021BA3217007F7846 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
545BA71221BA3217007F7846 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
545BA71321BA3217007F7846 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
545BA72221BA3867007F7846 /* SamplesObjC-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SamplesObjC-Bridging-Header.h"; sourceTree = "<group>"; };
545BA72521BA3BAF007F7846 /* FloatingPanel.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = FloatingPanel.framework; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
545BA6FE21BA3214007F7846 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
545BA72621BA3BAF007F7846 /* FloatingPanel.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
545BA6F821BA3214007F7846 = {
isa = PBXGroup;
children = (
545BA72521BA3BAF007F7846 /* FloatingPanel.framework */,
545BA70321BA3214007F7846 /* SamplesObjC */,
545BA70221BA3214007F7846 /* Products */,
);
sourceTree = "<group>";
};
545BA70221BA3214007F7846 /* Products */ = {
isa = PBXGroup;
children = (
545BA70121BA3214007F7846 /* SamplesObjC.app */,
);
name = Products;
sourceTree = "<group>";
};
545BA70321BA3214007F7846 /* SamplesObjC */ = {
isa = PBXGroup;
children = (
545BA70421BA3214007F7846 /* AppDelegate.h */,
545BA70521BA3214007F7846 /* AppDelegate.m */,
545BA70721BA3214007F7846 /* ViewController.h */,
545BA70821BA3214007F7846 /* ViewController.m */,
545BA70A21BA3214007F7846 /* Main.storyboard */,
545BA70D21BA3217007F7846 /* Assets.xcassets */,
545BA70F21BA3217007F7846 /* LaunchScreen.storyboard */,
545BA71221BA3217007F7846 /* Info.plist */,
545BA71321BA3217007F7846 /* main.m */,
545BA72221BA3867007F7846 /* SamplesObjC-Bridging-Header.h */,
);
path = SamplesObjC;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
545BA70021BA3214007F7846 /* SamplesObjC */ = {
isa = PBXNativeTarget;
buildConfigurationList = 545BA71721BA3217007F7846 /* Build configuration list for PBXNativeTarget "SamplesObjC" */;
buildPhases = (
545BA6FD21BA3214007F7846 /* Sources */,
545BA6FE21BA3214007F7846 /* Frameworks */,
545BA6FF21BA3214007F7846 /* Resources */,
545BA72821BA3BAF007F7846 /* Embed Frameworks */,
);
buildRules = (
);
dependencies = (
);
name = SamplesObjC;
productName = SamplesObjC;
productReference = 545BA70121BA3214007F7846 /* SamplesObjC.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
545BA6F921BA3214007F7846 /* Project object */ = {
isa = PBXProject;
attributes = {
LastUpgradeCheck = 1110;
ORGANIZATIONNAME = "Shin Yamamoto";
TargetAttributes = {
545BA70021BA3214007F7846 = {
CreatedOnToolsVersion = 10.1;
LastSwiftMigration = 1010;
};
};
};
buildConfigurationList = 545BA6FC21BA3214007F7846 /* Build configuration list for PBXProject "SamplesObjC" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 545BA6F821BA3214007F7846;
productRefGroup = 545BA70221BA3214007F7846 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
545BA70021BA3214007F7846 /* SamplesObjC */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
545BA6FF21BA3214007F7846 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
545BA71121BA3217007F7846 /* LaunchScreen.storyboard in Resources */,
545BA70E21BA3217007F7846 /* Assets.xcassets in Resources */,
545BA70C21BA3214007F7846 /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
545BA6FD21BA3214007F7846 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
545BA70921BA3214007F7846 /* ViewController.m in Sources */,
545BA71421BA3217007F7846 /* main.m in Sources */,
545BA70621BA3214007F7846 /* AppDelegate.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXVariantGroup section */
545BA70A21BA3214007F7846 /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
545BA70B21BA3214007F7846 /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
545BA70F21BA3217007F7846 /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
545BA71021BA3217007F7846 /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
545BA71521BA3217007F7846 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_IDENTITY = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.1;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
};
name = Debug;
};
545BA71621BA3217007F7846 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_IDENTITY = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.1;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
VALIDATE_PRODUCT = YES;
};
name = Release;
};
545BA71821BA3217007F7846 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
DEFINES_MODULE = NO;
DEVELOPMENT_TEAM = J3D7L9FHSS;
INFOPLIST_FILE = SamplesObjC/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.SamplesObjC;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "SamplesObjC/SamplesObjC-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 4.2;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
545BA71921BA3217007F7846 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
DEFINES_MODULE = NO;
DEVELOPMENT_TEAM = J3D7L9FHSS;
INFOPLIST_FILE = SamplesObjC/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.SamplesObjC;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "SamplesObjC/SamplesObjC-Bridging-Header.h";
SWIFT_VERSION = 4.2;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
545BA6FC21BA3214007F7846 /* Build configuration list for PBXProject "SamplesObjC" */ = {
isa = XCConfigurationList;
buildConfigurations = (
545BA71521BA3217007F7846 /* Debug */,
545BA71621BA3217007F7846 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
545BA71721BA3217007F7846 /* Build configuration list for PBXNativeTarget "SamplesObjC" */ = {
isa = XCConfigurationList;
buildConfigurations = (
545BA71821BA3217007F7846 /* Debug */,
545BA71921BA3217007F7846 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 545BA6F921BA3214007F7846 /* Project object */;
}
@@ -2,6 +2,6 @@
<Workspace
version = "1.0">
<FileRef
location = "self:FloatingModalController.xcodeproj">
location = "self:SamplesObjC.xcodeproj">
</FileRef>
</Workspace>
@@ -0,0 +1,87 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1110"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "545BA70021BA3214007F7846"
BuildableName = "SamplesObjC.app"
BlueprintName = "SamplesObjC"
ReferencedContainer = "container:SamplesObjC.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "545BA70021BA3214007F7846"
BuildableName = "SamplesObjC.app"
BlueprintName = "SamplesObjC"
ReferencedContainer = "container:SamplesObjC.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "545BA70021BA3214007F7846"
BuildableName = "SamplesObjC.app"
BlueprintName = "SamplesObjC"
ReferencedContainer = "container:SamplesObjC.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "545BA70021BA3214007F7846"
BuildableName = "SamplesObjC.app"
BlueprintName = "SamplesObjC"
ReferencedContainer = "container:SamplesObjC.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
@@ -0,0 +1,9 @@
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
#import <UIKit/UIKit.h>
@interface AppDelegate : UIResponder <UIApplicationDelegate>
@property (strong, nonatomic) UIWindow* window;
@end
@@ -0,0 +1,9 @@
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
#import "AppDelegate.h"
@interface AppDelegate ()
@end
@implementation AppDelegate
@end
@@ -0,0 +1,98 @@
{
"images" : [
{
"idiom" : "iphone",
"size" : "20x20",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "20x20",
"scale" : "3x"
},
{
"idiom" : "iphone",
"size" : "29x29",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "29x29",
"scale" : "3x"
},
{
"idiom" : "iphone",
"size" : "40x40",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "40x40",
"scale" : "3x"
},
{
"idiom" : "iphone",
"size" : "60x60",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "60x60",
"scale" : "3x"
},
{
"idiom" : "ipad",
"size" : "20x20",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "20x20",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "29x29",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "29x29",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "40x40",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "40x40",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "76x76",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "76x76",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "83.5x83.5",
"scale" : "2x"
},
{
"idiom" : "ios-marketing",
"size" : "1024x1024",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
@@ -0,0 +1,6 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
}
}
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" 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="13104.12"/>
<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"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
</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,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
<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="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="ViewController" customModuleProvider="" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>
@@ -0,0 +1,45 @@
<?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>UIMainStoryboardFile</key>
<string>Main</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,3 @@
//
// Use this file to import your target's public headers that you would like to expose to Swift.
//
@@ -0,0 +1,13 @@
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
#import <UIKit/UIKit.h>
@import FloatingPanel;
@interface ViewController : UIViewController
@end
@interface MyFloatingPanelLayout : NSObject <FloatingPanelLayout>
@end
@interface MyFloatingPanelBehavior : NSObject <FloatingPanelBehavior>
@end
@@ -0,0 +1,78 @@
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
#import "ViewController.h"
@import FloatingPanel;
@interface ViewController()<FloatingPanelControllerDelegate>
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
FloatingPanelController *fpc = [[FloatingPanelController alloc] init];
[fpc setContentViewController:nil];
[fpc trackScrollView:nil];
[fpc setDelegate:self];
[fpc setLayout: [MyFloatingPanelLayout new]];
[fpc setBehavior:[MyFloatingPanelBehavior new]];
[fpc setRemovalInteractionEnabled:NO];
[fpc addPanelToParent:self at:self.view.subviews.count animated:NO];
[fpc moveToState:FloatingPanelState.Tip animated:true completion:nil];
[self updateAppearance: fpc];
}
- (id<FloatingPanelLayout>)floatingPanel:(FloatingPanelController *)vc layoutFor:(UITraitCollection *)newCollection {
FloatingPanelBottomLayout *layout = [FloatingPanelBottomLayout new];
return layout;
}
- (void)updateAppearance: (FloatingPanelController*)fpc
{
FloatingPanelSurfaceAppearance *appearance = [[FloatingPanelSurfaceAppearance alloc] init];
appearance.backgroundColor = [UIColor clearColor];
appearance.cornerRadius = 23.0;
if (@available(iOS 13.0, *)) {
fpc.surfaceView.containerView.layer.cornerCurve = kCACornerCurveContinuous;
}
FloatingPanelSurfaceAppearanceShadow *shadow = [[FloatingPanelSurfaceAppearanceShadow alloc] init];
shadow.color = [UIColor redColor];
shadow.radius = 10.0;
shadow.spread = 10.0;
FloatingPanelSurfaceAppearanceShadow *shadow2 = [[FloatingPanelSurfaceAppearanceShadow alloc] init];
shadow2.color = [UIColor blueColor];
shadow2.radius = 10.0;
shadow2.spread = 10.0;
appearance.shadows = @[shadow, shadow2];
fpc.surfaceView.appearance = appearance;
}
@end
@implementation MyFloatingPanelLayout
- (FloatingPanelState *)initialState {
return FloatingPanelState.Half;
}
- (NSDictionary<FloatingPanelState *, id<FloatingPanelLayoutAnchoring>> *)anchors {
return @{
FloatingPanelState.Half: [[FloatingPanelLayoutAnchor alloc] initWithFractionalInset:0.5
edge:FloatingPanelReferenceEdgeTop
referenceGuide:FloatingPanelLayoutReferenceGuideSafeArea],
FloatingPanelState.Tip: [[FloatingPanelLayoutAnchor alloc] initWithAbsoluteInset:44.0
edge:FloatingPanelReferenceEdgeBottom
referenceGuide:FloatingPanelLayoutReferenceGuideSafeArea],
};
}
- (enum FloatingPanelPosition)position {
return FloatingPanelPositionBottom;
}
@end
@implementation MyFloatingPanelBehavior
@end
+10
View File
@@ -0,0 +1,10 @@
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
+1 -7
View File
@@ -1,10 +1,4 @@
//
// AppDelegate.swift
// Stocks
//
// Created by Shin Yamamoto on 2018/10/12.
// Copyright © 2018 Shin Yamamoto. All rights reserved.
//
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
import UIKit
+51 -83
View File
@@ -1,10 +1,4 @@
//
// ViewController.swift
// Stocks
//
// Created by Shin Yamamoto on 2018/10/12.
// Copyright © 2018 Shin Yamamoto. All rights reserved.
//
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
import UIKit
import FloatingPanel
@@ -18,19 +12,21 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate {
var newsVC: NewsViewController!
var initialColor: UIColor = .black
override func viewDidLoad() {
super.viewDidLoad()
initialColor = view.backgroundColor!
// Initialize FloatingPanelController
fpc = FloatingPanelController()
fpc.delegate = self
fpc.behavior = FloatingPanelStocksBehavior()
// Initialize FloatingPanelController and add the view
fpc.surfaceView.backgroundColor = UIColor(displayP3Red: 30.0/255.0, green: 30.0/255.0, blue: 30.0/255.0, alpha: 1.0)
fpc.surfaceView.cornerRadius = 24.0
fpc.surfaceView.shadowHidden = true
fpc.surfaceView.borderWidth = 1.0 / traitCollection.displayScale
fpc.surfaceView.borderColor = UIColor.black.withAlphaComponent(0.2)
fpc.surfaceView.appearance.cornerRadius = 24.0
fpc.surfaceView.appearance.shadows = []
fpc.surfaceView.appearance.borderWidth = 1.0 / traitCollection.displayScale
fpc.surfaceView.appearance.borderColor = UIColor.black.withAlphaComponent(0.2)
newsVC = storyboard?.instantiateViewController(withIdentifier: "News") as? NewsViewController
@@ -38,7 +34,7 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate {
fpc.set(contentViewController: newsVC)
fpc.track(scrollView: newsVC.scrollView)
fpc.addPanel(toParent: self, belowView: bottomToolView, animated: false)
fpc.addPanel(toParent: self, at: view.subviews.firstIndex(of: bottomToolView) ?? -1 , animated: false)
topBannerView.frame = .zero
topBannerView.alpha = 0.0
@@ -50,46 +46,46 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate {
])
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
}
override var preferredStatusBarStyle: UIStatusBarStyle {
return .lightContent
}
// MARK: FloatingPanelControllerDelegate
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? {
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout {
return FloatingPanelStocksLayout()
}
func floatingPanel(_ vc: FloatingPanelController, behaviorFor newCollection: UITraitCollection) -> FloatingPanelBehavior? {
return FloatingPanelStocksBehavior()
}
func floatingPanelDidMove(_ vc: FloatingPanelController) {
if vc.isAttracting == false {
let loc = vc.surfaceLocation
let minY = vc.surfaceLocation(for: .full).y
let maxY = vc.surfaceLocation(for: .tip).y
vc.surfaceLocation = CGPoint(x: loc.x, y: min(max(loc.y, minY), maxY))
}
func floatingPanelWillBeginDragging(_ vc: FloatingPanelController) {
if vc.position == .full {
// Dimiss top bar with dissolve animation
UIView.animate(withDuration: 0.25) {
self.topBannerView.alpha = 0.0
self.labelStackView.alpha = 1.0
self.view.backgroundColor = self.initialColor
}
if vc.surfaceLocation.y <= vc.surfaceLocation(for: .full).y + 100 {
showStockTickerBanner()
} else {
hideStockTickerBanner()
}
}
func floatingPanelDidEndDragging(_ vc: FloatingPanelController, withVelocity velocity: CGPoint, targetPosition: FloatingPanelPosition) {
if targetPosition == .full {
// Present top bar with dissolve animation
UIView.animate(withDuration: 0.25) {
self.topBannerView.alpha = 1.0
self.labelStackView.alpha = 0.0
self.view.backgroundColor = .black
}
private func showStockTickerBanner() {
// Present top bar with dissolve animation
UIView.animate(withDuration: 0.25) {
self.topBannerView.alpha = 1.0
self.labelStackView.alpha = 0.0
self.view.backgroundColor = .black
}
}
private func hideStockTickerBanner() {
// Dimiss top bar with dissolve animation
UIView.animate(withDuration: 0.25) {
self.topBannerView.alpha = 0.0
self.labelStackView.alpha = 1.0
self.view.backgroundColor = .black
}
}
}
@@ -99,57 +95,29 @@ class NewsViewController: UIViewController {
}
// MARK: My custom layout
// MARK: - FloatingPanelLayout
class FloatingPanelStocksLayout: FloatingPanelLayout {
var initialPosition: FloatingPanelPosition {
return .tip
let position: FloatingPanelPosition = .bottom
let initialState: FloatingPanelState = .tip
var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] {
return [
.full: FloatingPanelLayoutAnchor(absoluteInset: 56.0, edge: .top, referenceGuide: .safeArea),
.half: FloatingPanelLayoutAnchor(absoluteInset: 262.0, edge: .bottom, referenceGuide: .safeArea),
/* Visible + ToolView */
.tip: FloatingPanelLayoutAnchor(absoluteInset: 85.0 + 44.0, edge: .bottom, referenceGuide: .safeArea),
]
}
var topInteractionBuffer: CGFloat { return 0.0 }
var bottomInteractionBuffer: CGFloat { return 0.0 }
func insetFor(position: FloatingPanelPosition) -> CGFloat? {
switch position {
case .full: return 56.0
case .half: return 262.0
case .tip: return 85.0 + 44.0 // Visible + ToolView
default: return nil
}
}
func backdropAlphaFor(position: FloatingPanelPosition) -> CGFloat {
func backdropAlpha(for state: FloatingPanelState) -> CGFloat {
return 0.0
}
}
// MARK: My custom behavior
// MARK: - FloatingPanelBehavior
class FloatingPanelStocksBehavior: FloatingPanelBehavior {
var velocityThreshold: CGFloat {
return 15.0
}
func interactionAnimator(_ fpc: FloatingPanelController, to targetPosition: FloatingPanelPosition, with velocity: CGVector) -> UIViewPropertyAnimator {
let timing = timeingCurve(to: targetPosition, with: velocity)
return UIViewPropertyAnimator(duration: 0, timingParameters: timing)
}
private func timeingCurve(to: FloatingPanelPosition, with velocity: CGVector) -> UITimingCurveProvider {
let damping = self.damping(with: velocity)
return UISpringTimingParameters(dampingRatio: damping,
frequencyResponse: 0.4,
initialVelocity: velocity)
}
private func damping(with velocity: CGVector) -> CGFloat {
switch velocity.dy {
case ...(-velocityThreshold):
return 0.7
case velocityThreshold...:
return 0.7
default:
return 1.0
}
}
let springDecelerationRate: CGFloat = UIScrollView.DecelerationRate.fast.rawValue
let springResponseTime: CGFloat = 0.25
}
+6 -7
View File
@@ -1,23 +1,22 @@
Pod::Spec.new do |s|
s.name = "FloatingPanel"
s.version = "1.6.6"
s.version = "2.0.0"
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.
The new interface displays the related contents and utilities in parallel as a user wants.
DESC
s.homepage = "https://github.com/SCENEE/FloatingPanel"
# s.screenshots = ""
s.author = "Shin Yamamoto"
s.social_media_url = "https://twitter.com/scenee"
s.platform = :ios, "10.0"
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.source = { :git => "https://github.com/SCENEE/FloatingPanel.git", :tag => s.version.to_s }
s.source_files = "Sources/*.swift"
s.swift_versions = ['5.1', '5.2', '5.3']
s.framework = "UIKit"
s.author = { "Shin Yamamoto" => "shin@scenee.com" }
s.license = { :type => "MIT", :file => "LICENSE" }
s.social_media_url = "https://twitter.com/scenee"
end
@@ -7,27 +7,32 @@
objects = {
/* Begin PBXBuildFile section */
542753C622C49A6E00D17955 /* FloatingPanelLayoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 542753C522C49A6E00D17955 /* FloatingPanelLayoutTests.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 */; };
542753C622C49A6E00D17955 /* LayoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 542753C522C49A6E00D17955 /* LayoutTests.swift */; };
54352E9621A51A2500CBCA08 /* Transitioning.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54352E9521A51A2500CBCA08 /* Transitioning.swift */; };
54352E9821A521CA00CBCA08 /* PassThroughView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54352E9721A521CA00CBCA08 /* PassThroughView.swift */; };
5450EEE421646DF500135936 /* Behavior.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5450EEE321646DF500135936 /* Behavior.swift */; };
545DB9CB2151169500CA77B8 /* FloatingPanel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 545DB9C12151169500CA77B8 /* FloatingPanel.framework */; };
545DB9D02151169500CA77B8 /* FloatingPanelControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DB9CF2151169500CA77B8 /* FloatingPanelControllerTests.swift */; };
545DB9D02151169500CA77B8 /* ControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DB9CF2151169500CA77B8 /* ControllerTests.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 */; };
546055BF2333C4740069F400 /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 542753C722C49A8F00D17955 /* Utils.swift */; };
549E944522CF295D0050AECF /* FloatingPanelPositionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 549E944422CF295D0050AECF /* FloatingPanelPositionTests.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 */; };
545DB9E021511AC100CA77B8 /* Controller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DB9DF21511AC100CA77B8 /* Controller.swift */; };
545DBA2B2152383100CA77B8 /* GrabberView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DBA2A2152383100CA77B8 /* GrabberView.swift */; };
546055BF2333C4740069F400 /* TestSupports.swift in Sources */ = {isa = PBXBuildFile; fileRef = 542753C722C49A8F00D17955 /* TestSupports.swift */; };
5469F4A224B003EF00537F8A /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5469F49F24B003EF00537F8A /* LaunchScreen.storyboard */; };
5469F4A324B003EF00537F8A /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5469F4A024B003EF00537F8A /* AppDelegate.swift */; };
5469F4AE24B30D7E00537F8A /* State.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5469F4AD24B30D7E00537F8A /* State.swift */; };
5469F4B024B30E1500537F8A /* LayoutAnchoring.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5469F4AF24B30E1500537F8A /* LayoutAnchoring.swift */; };
5469F4B224B30F1100537F8A /* Position.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5469F4B124B30F1100537F8A /* Position.swift */; };
5469F4B424B30F3500537F8A /* LayoutReferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5469F4B324B30F3500537F8A /* LayoutReferences.swift */; };
549C371F2361E15E007D8058 /* UtilTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 549C371E2361E15D007D8058 /* UtilTests.swift */; };
549E944522CF295D0050AECF /* StateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 549E944422CF295D0050AECF /* StateTests.swift */; };
54A6B6B122968B530077F348 /* CoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54A6B6B022968B530077F348 /* CoreTests.swift */; };
54A6B6B82296A8520077F348 /* SurfaceViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54A6B6B72296A8520077F348 /* SurfaceViewTests.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 */; };
54CDC5D3215B6D5A007D205C /* SurfaceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CDC5D2215B6D5A007D205C /* SurfaceView.swift */; };
54CDC5D5215B6D8D007D205C /* BackdropView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CDC5D4215B6D8D007D205C /* BackdropView.swift */; };
54CFBFC3215CD045006B5735 /* Layout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CFBFC2215CD045006B5735 /* Layout.swift */; };
54CFBFC5215CD09C006B5735 /* Core.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CFBFC4215CD09C006B5735 /* Core.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -48,32 +53,37 @@
/* 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>"; };
542753C522C49A6E00D17955 /* LayoutTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutTests.swift; sourceTree = "<group>"; };
542753C722C49A8F00D17955 /* TestSupports.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestSupports.swift; sourceTree = "<group>"; };
54352E9521A51A2500CBCA08 /* Transitioning.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Transitioning.swift; sourceTree = "<group>"; };
54352E9721A521CA00CBCA08 /* PassThroughView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PassThroughView.swift; sourceTree = "<group>"; };
5450EEE321646DF500135936 /* Behavior.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Behavior.swift; sourceTree = "<group>"; };
545DB9C12151169500CA77B8 /* FloatingPanel.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = FloatingPanel.framework; sourceTree = BUILT_PRODUCTS_DIR; };
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 /* FloatingPanelControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelControllerTests.swift; sourceTree = "<group>"; };
545DB9CF2151169500CA77B8 /* ControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControllerTests.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>"; };
549E944422CF295D0050AECF /* FloatingPanelPositionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelPositionTests.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>"; };
545DB9DF21511AC100CA77B8 /* Controller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Controller.swift; sourceTree = "<group>"; };
545DBA2A2152383100CA77B8 /* GrabberView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GrabberView.swift; sourceTree = "<group>"; };
5469F49F24B003EF00537F8A /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = "<group>"; };
5469F4A024B003EF00537F8A /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
5469F4A124B003EF00537F8A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
5469F4AD24B30D7E00537F8A /* State.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = State.swift; sourceTree = "<group>"; };
5469F4AF24B30E1500537F8A /* LayoutAnchoring.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutAnchoring.swift; sourceTree = "<group>"; };
5469F4B124B30F1100537F8A /* Position.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Position.swift; sourceTree = "<group>"; };
5469F4B324B30F3500537F8A /* LayoutReferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutReferences.swift; sourceTree = "<group>"; };
549C371E2361E15D007D8058 /* UtilTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UtilTests.swift; sourceTree = "<group>"; };
549E944422CF295D0050AECF /* StateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateTests.swift; sourceTree = "<group>"; };
54A6B6B022968B530077F348 /* CoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreTests.swift; sourceTree = "<group>"; };
54A6B6B72296A8520077F348 /* SurfaceViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SurfaceViewTests.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>"; };
54CDC5D2215B6D5A007D205C /* SurfaceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SurfaceView.swift; sourceTree = "<group>"; };
54CDC5D4215B6D8D007D205C /* BackdropView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackdropView.swift; sourceTree = "<group>"; };
54CFBFC2215CD045006B5735 /* Layout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Layout.swift; sourceTree = "<group>"; };
54CFBFC4215CD09C006B5735 /* Core.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Core.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 */
@@ -107,7 +117,6 @@
children = (
545DB9C32151169500CA77B8 /* Sources */,
545DB9CE2151169500CA77B8 /* Tests */,
54E740CB218AFD67005C1A34 /* TestingApp */,
545DB9C22151169500CA77B8 /* Products */,
);
sourceTree = "<group>";
@@ -125,19 +134,23 @@
545DB9C32151169500CA77B8 /* Sources */ = {
isa = PBXGroup;
children = (
545DB9C52151169500CA77B8 /* Info.plist */,
545DB9C42151169500CA77B8 /* FloatingPanel.h */,
545DB9DF21511AC100CA77B8 /* FloatingPanelController.swift */,
54352E9521A51A2500CBCA08 /* FloatingPanelTransitioning.swift */,
54CFBFC4215CD09C006B5735 /* FloatingPanel.swift */,
54CFBFC2215CD045006B5735 /* FloatingPanelLayout.swift */,
5450EEE321646DF500135936 /* FloatingPanelBehavior.swift */,
54352E9721A521CA00CBCA08 /* FloatingPanelView.swift */,
54CDC5D2215B6D5A007D205C /* FloatingPanelSurfaceView.swift */,
54CDC5D4215B6D8D007D205C /* FloatingPanelBackdropView.swift */,
545DBA2A2152383100CA77B8 /* GrabberHandleView.swift */,
545DB9DF21511AC100CA77B8 /* Controller.swift */,
5469F4AD24B30D7E00537F8A /* State.swift */,
5469F4B124B30F1100537F8A /* Position.swift */,
54CFBFC4215CD09C006B5735 /* Core.swift */,
54CFBFC2215CD045006B5735 /* Layout.swift */,
5469F4B324B30F3500537F8A /* LayoutReferences.swift */,
5469F4AF24B30E1500537F8A /* LayoutAnchoring.swift */,
5450EEE321646DF500135936 /* Behavior.swift */,
54352E9721A521CA00CBCA08 /* PassThroughView.swift */,
54CDC5D2215B6D5A007D205C /* SurfaceView.swift */,
54CDC5D4215B6D8D007D205C /* BackdropView.swift */,
545DBA2A2152383100CA77B8 /* GrabberView.swift */,
54352E9521A51A2500CBCA08 /* Transitioning.swift */,
545DB9DD215118C800CA77B8 /* UIExtensions.swift */,
54ABD7AE216CCFF7002E6C13 /* Logger.swift */,
545DB9C42151169500CA77B8 /* FloatingPanel.h */,
545DB9C52151169500CA77B8 /* Info.plist */,
);
path = Sources;
sourceTree = "<group>";
@@ -145,23 +158,25 @@
545DB9CE2151169500CA77B8 /* Tests */ = {
isa = PBXGroup;
children = (
54A6B6B022968B530077F348 /* FloatingPanelTests.swift */,
545DB9CF2151169500CA77B8 /* FloatingPanelControllerTests.swift */,
542753C522C49A6E00D17955 /* FloatingPanelLayoutTests.swift */,
54A6B6B72296A8520077F348 /* FloatingPanelSurfaceViewTests.swift */,
549E944422CF295D0050AECF /* FloatingPanelPositionTests.swift */,
542753C722C49A8F00D17955 /* Utils.swift */,
5469F49E24B003EF00537F8A /* TestingApp */,
54A6B6B022968B530077F348 /* CoreTests.swift */,
545DB9CF2151169500CA77B8 /* ControllerTests.swift */,
542753C522C49A6E00D17955 /* LayoutTests.swift */,
54A6B6B72296A8520077F348 /* SurfaceViewTests.swift */,
549E944422CF295D0050AECF /* StateTests.swift */,
549C371E2361E15D007D8058 /* UtilTests.swift */,
542753C722C49A8F00D17955 /* TestSupports.swift */,
545DB9D12151169500CA77B8 /* Info.plist */,
);
path = Tests;
sourceTree = "<group>";
};
54E740CB218AFD67005C1A34 /* TestingApp */ = {
5469F49E24B003EF00537F8A /* TestingApp */ = {
isa = PBXGroup;
children = (
54E740CC218AFD67005C1A34 /* AppDelegate.swift */,
54E740D8218AFD6A005C1A34 /* Info.plist */,
54A6B6B522968F710077F348 /* LaunchScreen.storyboard */,
5469F49F24B003EF00537F8A /* LaunchScreen.storyboard */,
5469F4A024B003EF00537F8A /* AppDelegate.swift */,
5469F4A124B003EF00537F8A /* Info.plist */,
);
path = TestingApp;
sourceTree = "<group>";
@@ -246,7 +261,7 @@
TargetAttributes = {
545DB9C02151169500CA77B8 = {
CreatedOnToolsVersion = 10.0;
LastSwiftMigration = 1000;
LastSwiftMigration = 1110;
};
545DB9C92151169500CA77B8 = {
CreatedOnToolsVersion = 10.0;
@@ -296,7 +311,7 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
54A6B6B622968F710077F348 /* LaunchScreen.storyboard in Resources */,
5469F4A224B003EF00537F8A /* LaunchScreen.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -307,17 +322,21 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
54CDC5D3215B6D5A007D205C /* FloatingPanelSurfaceView.swift in Sources */,
54CFBFC3215CD045006B5735 /* FloatingPanelLayout.swift in Sources */,
54CDC5D5215B6D8D007D205C /* FloatingPanelBackdropView.swift in Sources */,
54352E9821A521CA00CBCA08 /* FloatingPanelView.swift in Sources */,
54CFBFC5215CD09C006B5735 /* FloatingPanel.swift in Sources */,
5469F4B224B30F1100537F8A /* Position.swift in Sources */,
5469F4B024B30E1500537F8A /* LayoutAnchoring.swift in Sources */,
54CDC5D3215B6D5A007D205C /* SurfaceView.swift in Sources */,
54CFBFC3215CD045006B5735 /* Layout.swift in Sources */,
5469F4B424B30F3500537F8A /* LayoutReferences.swift in Sources */,
54CDC5D5215B6D8D007D205C /* BackdropView.swift in Sources */,
54352E9821A521CA00CBCA08 /* PassThroughView.swift in Sources */,
54CFBFC5215CD09C006B5735 /* Core.swift in Sources */,
54ABD7AF216CCFF7002E6C13 /* Logger.swift in Sources */,
545DB9E021511AC100CA77B8 /* FloatingPanelController.swift in Sources */,
5450EEE421646DF500135936 /* FloatingPanelBehavior.swift in Sources */,
545DBA2B2152383100CA77B8 /* GrabberHandleView.swift in Sources */,
54352E9621A51A2500CBCA08 /* FloatingPanelTransitioning.swift in Sources */,
545DB9E021511AC100CA77B8 /* Controller.swift in Sources */,
5450EEE421646DF500135936 /* Behavior.swift in Sources */,
545DBA2B2152383100CA77B8 /* GrabberView.swift in Sources */,
54352E9621A51A2500CBCA08 /* Transitioning.swift in Sources */,
545DB9DE215118C800CA77B8 /* UIExtensions.swift in Sources */,
5469F4AE24B30D7E00537F8A /* State.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -325,12 +344,13 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
54A6B6B122968B530077F348 /* FloatingPanelTests.swift in Sources */,
545DB9D02151169500CA77B8 /* FloatingPanelControllerTests.swift in Sources */,
549E944522CF295D0050AECF /* FloatingPanelPositionTests.swift in Sources */,
542753C622C49A6E00D17955 /* FloatingPanelLayoutTests.swift in Sources */,
54A6B6B82296A8520077F348 /* FloatingPanelSurfaceViewTests.swift in Sources */,
546055BF2333C4740069F400 /* Utils.swift in Sources */,
54A6B6B122968B530077F348 /* CoreTests.swift in Sources */,
549C371F2361E15E007D8058 /* UtilTests.swift in Sources */,
545DB9D02151169500CA77B8 /* ControllerTests.swift in Sources */,
549E944522CF295D0050AECF /* StateTests.swift in Sources */,
542753C622C49A6E00D17955 /* LayoutTests.swift in Sources */,
54A6B6B82296A8520077F348 /* SurfaceViewTests.swift in Sources */,
546055BF2333C4740069F400 /* TestSupports.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -338,7 +358,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
54E740CD218AFD67005C1A34 /* AppDelegate.swift in Sources */,
5469F4A324B003EF00537F8A /* AppDelegate.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -417,6 +437,7 @@
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
VERSION_INFO_PREFIX = "";
};
@@ -474,6 +495,7 @@
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
SWIFT_VERSION = 5.0;
VALIDATE_PRODUCT = YES;
VERSIONING_SYSTEM = "apple-generic";
VERSION_INFO_PREFIX = "";
@@ -484,6 +506,7 @@
isa = XCBuildConfiguration;
buildSettings = {
APPLICATION_EXTENSION_API_ONLY = YES;
BUILD_LIBRARY_FOR_DISTRIBUTION = YES;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "";
CODE_SIGN_STYLE = Automatic;
@@ -505,7 +528,7 @@
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 4.0;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
@@ -514,6 +537,7 @@
isa = XCBuildConfiguration;
buildSettings = {
APPLICATION_EXTENSION_API_ONLY = YES;
BUILD_LIBRARY_FOR_DISTRIBUTION = YES;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "";
CODE_SIGN_STYLE = Automatic;
@@ -533,7 +557,7 @@
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SKIP_INSTALL = YES;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_VERSION = 4.0;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
@@ -585,7 +609,7 @@
buildSettings = {
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = "";
INFOPLIST_FILE = TestingApp/Info.plist;
INFOPLIST_FILE = Tests/TestingApp/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@@ -603,7 +627,7 @@
buildSettings = {
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = "";
INFOPLIST_FILE = TestingApp/Info.plist;
INFOPLIST_FILE = Tests/TestingApp/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@@ -675,6 +699,7 @@
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
VERSION_INFO_PREFIX = "";
};
@@ -703,9 +728,9 @@
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "TEST DEBUG __FP_LOG";
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_COMPILATION_MODE = singlefile;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 4.0;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Test;
@@ -735,7 +760,7 @@
isa = XCBuildConfiguration;
buildSettings = {
DEVELOPMENT_TEAM = "";
INFOPLIST_FILE = TestingApp/Info.plist;
INFOPLIST_FILE = Tests/TestingApp/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingPanelTesting;
@@ -0,0 +1,8 @@
<?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>FILEHEADER</key>
<string> Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.</string>
</dict>
</plist>
+6 -7
View File
@@ -4,13 +4,9 @@
<FileRef
location = "group:README.md">
</FileRef>
<Group
location = "group:Framework"
name = "Framework">
<FileRef
location = "group:FloatingPanel.xcodeproj">
</FileRef>
</Group>
<FileRef
location = "group:FloatingPanel.xcodeproj">
</FileRef>
<Group
location = "group:Examples"
name = "Examples">
@@ -23,5 +19,8 @@
<FileRef
location = "group:Samples/Samples.xcodeproj">
</FileRef>
<FileRef
location = "group:SamplesObjC/SamplesObjC.xcodeproj">
</FileRef>
</Group>
</Workspace>
-9
View File
@@ -1,9 +0,0 @@
//
// Created by Shin Yamamoto on 2018/09/18.
// Copyright © 2018 Shin Yamamoto. All rights reserved.
//
#import <UIKit/UIKit.h>
FOUNDATION_EXPORT double FloatingPanelVersionNumber;
FOUNDATION_EXPORT const unsigned char FloatingPanelVersionString[];
-868
View File
@@ -1,868 +0,0 @@
//
// Copyright © 2018 Shin Yamamoto. All rights reserved.
//
import UIKit
import UIKit.UIGestureRecognizerSubclass // For Xcode 9.4.1
///
/// FloatingPanel presentation model
///
class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
// MUST be a weak reference to prevent UI freeze on the presentation modally
weak var viewcontroller: FloatingPanelController?
let surfaceView: FloatingPanelSurfaceView
let backdropView: FloatingPanelBackdropView
var layoutAdapter: FloatingPanelLayoutAdapter
var behavior: FloatingPanelBehavior
weak var scrollView: UIScrollView? {
didSet {
oldValue?.panGestureRecognizer.removeTarget(self, action: nil)
scrollView?.panGestureRecognizer.addTarget(self, action: #selector(handle(panGesture:)))
}
}
private(set) var state: FloatingPanelPosition = .hidden {
didSet {
if let vc = viewcontroller {
vc.delegate?.floatingPanelDidChangePosition(vc)
}
}
}
private var isBottomState: Bool {
let remains = layoutAdapter.supportedPositions.filter { $0.rawValue > state.rawValue }
return remains.count == 0
}
let panGestureRecognizer: FloatingPanelPanGestureRecognizer
var isRemovalInteractionEnabled: Bool = false
fileprivate var animator: UIViewPropertyAnimator?
private var initialFrame: CGRect = .zero
private var initialTranslationY: CGFloat = 0
private var initialLocation: CGPoint = .nan
var interactionInProgress: Bool = false
var isDecelerating: Bool = false
// Scroll handling
private var initialScrollOffset: CGPoint = .zero
private var stopScrollDeceleration: Bool = false
private var scrollBouncable = false
private var scrollIndictorVisible = false
// MARK: - Interface
init(_ vc: FloatingPanelController, layout: FloatingPanelLayout, behavior: FloatingPanelBehavior) {
viewcontroller = vc
surfaceView = FloatingPanelSurfaceView()
surfaceView.backgroundColor = .white
backdropView = FloatingPanelBackdropView()
backdropView.backgroundColor = .black
backdropView.alpha = 0.0
self.layoutAdapter = FloatingPanelLayoutAdapter(surfaceView: surfaceView,
backdropView: backdropView,
layout: layout)
self.behavior = behavior
panGestureRecognizer = FloatingPanelPanGestureRecognizer()
if #available(iOS 11.0, *) {
panGestureRecognizer.name = "FloatingPanelSurface"
}
super.init()
panGestureRecognizer.floatingPanel = self
surfaceView.addGestureRecognizer(panGestureRecognizer)
panGestureRecognizer.addTarget(self, action: #selector(handle(panGesture:)))
panGestureRecognizer.delegate = self
}
func move(to: FloatingPanelPosition, animated: Bool, completion: (() -> Void)? = nil) {
move(from: state, to: to, animated: animated, completion: completion)
}
private func move(from: FloatingPanelPosition, to: FloatingPanelPosition, animated: Bool, completion: (() -> Void)? = nil) {
assert(layoutAdapter.isValid(to), "Can't move to '\(to)' position because it's not valid in the layout")
guard let vc = viewcontroller else {
completion?()
return
}
if state != layoutAdapter.topMostState {
lockScrollView()
}
tearDownActiveInteraction()
if animated {
let animator: UIViewPropertyAnimator
switch (from, to) {
case (.hidden, let to):
animator = behavior.addAnimator(vc, to: to)
case (let from, .hidden):
animator = behavior.removeAnimator(vc, from: from)
case (let from, let to):
animator = behavior.moveAnimator(vc, from: from, to: to)
}
animator.addAnimations { [weak self] in
guard let `self` = self else { return }
self.state = to
self.updateLayout(to: to)
}
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
animator.startAnimation()
} else {
self.state = to
self.updateLayout(to: to)
if self.state == self.layoutAdapter.topMostState {
self.unlockScrollView()
} else {
self.lockScrollView()
}
completion?()
}
}
// MARK: - Layout update
private func updateLayout(to target: FloatingPanelPosition) {
self.layoutAdapter.activateLayout(of: target)
}
func getBackdropAlpha(at currentY: CGFloat, with translation: CGPoint) -> CGFloat {
let forwardY = (translation.y >= 0)
let segment = layoutAdapter.segument(at: currentY, forward: forwardY)
let lowerPos = segment.lower ?? layoutAdapter.topMostState
let upperPos = segment.upper ?? layoutAdapter.bottomMostState
let pre = forwardY ? lowerPos : upperPos
let next = forwardY ? upperPos : lowerPos
let nextY = layoutAdapter.positionY(for: next)
let preY = layoutAdapter.positionY(for: pre)
let nextAlpha = layoutAdapter.layout.backdropAlphaFor(position: next)
let preAlpha = layoutAdapter.layout.backdropAlphaFor(position: pre)
if preY == nextY {
return preAlpha
} else {
return preAlpha + max(min(1.0, 1.0 - (nextY - currentY) / (nextY - preY) ), 0.0) * (nextAlpha - preAlpha)
}
}
// MARK: - UIGestureRecognizerDelegate
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
guard gestureRecognizer == panGestureRecognizer else { return false }
/* log.debug("shouldRecognizeSimultaneouslyWith", otherGestureRecognizer) */
if let vc = viewcontroller,
vc.delegate?.floatingPanel(vc, shouldRecognizeSimultaneouslyWith: otherGestureRecognizer) ?? false {
return true
}
switch otherGestureRecognizer {
case is UIPanGestureRecognizer,
is UISwipeGestureRecognizer,
is UIRotationGestureRecognizer,
is UIScreenEdgePanGestureRecognizer,
is UIPinchGestureRecognizer:
// all gestures of the tracking scroll view should be recognized in parallel
// and handle them in self.handle(panGesture:)
return scrollView?.gestureRecognizers?.contains(otherGestureRecognizer) ?? false
default:
// 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)
}
}
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
guard gestureRecognizer == panGestureRecognizer else { return false }
/* log.debug("shouldBeRequiredToFailBy", otherGestureRecognizer) */
return false
}
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool {
guard gestureRecognizer == panGestureRecognizer else { return false }
/* log.debug("shouldRequireFailureOf", otherGestureRecognizer) */
// Should begin the pan gesture without waiting for the tracking scroll view's gestures.
// `scrollView.gestureRecognizers` can contains the following gestures
// * UIScrollViewDelayedTouchesBeganGestureRecognizer
// * UIScrollViewPanGestureRecognizer (scrollView.panGestureRecognizer)
// * _UIDragAutoScrollGestureRecognizer
// * _UISwipeActionPanGestureRecognizer
// * UISwipeDismissalGestureRecognizer
if let scrollView = scrollView {
// On short contents scroll, `_UISwipeActionPanGestureRecognizer` blocks
// the panel's pan gesture if not returns false
if let scrollGestureRecognizers = scrollView.gestureRecognizers,
scrollGestureRecognizers.contains(otherGestureRecognizer) {
return false
}
}
if let vc = viewcontroller,
vc.delegate?.floatingPanel(vc, shouldRecognizeSimultaneouslyWith: otherGestureRecognizer) ?? false {
return false
}
switch otherGestureRecognizer {
case is UIPanGestureRecognizer,
is UISwipeGestureRecognizer,
is UIRotationGestureRecognizer,
is UIScreenEdgePanGestureRecognizer,
is UIPinchGestureRecognizer:
// Do not begin the pan gesture until these gestures fail
return true
default:
// Should begin the pan gesture without waiting tap/long press gestures fail
return false
}
}
var grabberAreaFrame: CGRect {
let grabberAreaFrame = CGRect(x: surfaceView.bounds.origin.x,
y: surfaceView.bounds.origin.y,
width: surfaceView.bounds.width,
height: surfaceView.topGrabberBarHeight * 2)
return grabberAreaFrame
}
// MARK: - Gesture handling
@objc func handle(panGesture: UIPanGestureRecognizer) {
let velocity = panGesture.velocity(in: panGesture.view)
switch panGesture {
case scrollView?.panGestureRecognizer:
guard let scrollView = scrollView else { return }
let location = panGesture.location(in: surfaceView)
let belowTop = surfaceView.presentationFrame.minY > layoutAdapter.topY
let offset = scrollView.contentOffset.y - scrollView.contentOffsetZero.y
log.debug("scroll gesture(\(state):\(panGesture.state)) --",
"belowTop = \(belowTop),",
"interactionInProgress = \(interactionInProgress),",
"scroll offset = \(offset),",
"location = \(location.y), velocity = \(velocity.y)")
if belowTop {
// Scroll offset pinning
if state == layoutAdapter.topMostState {
if interactionInProgress {
log.debug("settle offset --", initialScrollOffset.y)
scrollView.setContentOffset(initialScrollOffset, animated: false)
} else {
if grabberAreaFrame.contains(location) {
// Preserve the current content offset in moving from full.
scrollView.setContentOffset(initialScrollOffset, animated: false)
}
}
} else {
scrollView.setContentOffset(initialScrollOffset, animated: false)
}
// Hide a scroll indicator at the non-top in dragging.
if interactionInProgress {
lockScrollView()
} else {
if state == layoutAdapter.topMostState, self.animator == nil,
offset > 0, velocity.y < 0 {
unlockScrollView()
}
}
} else {
if interactionInProgress {
// Show a scroll indicator at the top in dragging.
if offset >= 0, velocity.y <= 0 {
unlockScrollView()
} else {
if state == layoutAdapter.topMostState {
// Adjust a small gap of the scroll offset just after swiping down starts in the grabber area.
if grabberAreaFrame.contains(location), grabberAreaFrame.contains(initialLocation) {
scrollView.setContentOffset(initialScrollOffset, animated: false)
}
}
}
} else {
if state == layoutAdapter.topMostState {
// Hide a scroll indicator just before starting an interaction by swiping a panel down.
if offset < 0, velocity.y > 0 {
lockScrollView()
}
// Show a scroll indicator when an animation is interrupted at the top and content is scrolled up
if offset > 0, velocity.y < 0 {
unlockScrollView()
}
// Adjust a small gap of the scroll offset just before swiping down starts in the grabber area,
if grabberAreaFrame.contains(location), grabberAreaFrame.contains(initialLocation) {
scrollView.setContentOffset(initialScrollOffset, animated: false)
}
}
}
}
case panGestureRecognizer:
let translation = panGesture.translation(in: panGestureRecognizer.view!.superview)
let location = panGesture.location(in: panGesture.view)
log.debug("panel gesture(\(state):\(panGesture.state)) --",
"translation = \(translation.y), location = \(location.y), velocity = \(velocity.y)")
if interactionInProgress == false, isDecelerating == false,
let vc = viewcontroller, vc.delegate?.floatingPanelShouldBeginDragging(vc) == false {
return
}
if let animator = self.animator {
guard surfaceView.presentationFrame.minY >= layoutAdapter.topMaxY else { return }
log.debug("panel animation(interruptible: \(animator.isInterruptible)) interrupted!!!")
if animator.isInterruptible {
animator.stopAnimation(false)
// A user can stop a panel at the nearest Y of a target position so this fine-tunes
// the a small gap between the presentation layer frame and model layer frame
// to unlock scroll view properly at finishAnimation(at:)
if abs(surfaceView.frame.minY - layoutAdapter.topY) <= 1.0 {
surfaceView.frame.origin.y = layoutAdapter.topY
}
animator.finishAnimation(at: .current)
} else {
self.animator = nil
}
}
if panGesture.state == .began {
panningBegan(at: location)
return
}
if shouldScrollViewHandleTouch(scrollView, point: location, velocity: velocity) {
return
}
switch panGesture.state {
case .changed:
if interactionInProgress == false {
startInteraction(with: translation, at: location)
}
panningChange(with: translation)
case .ended, .cancelled, .failed:
if interactionInProgress == false {
startInteraction(with: translation, at: location)
// Workaround: Prevent stopping the surface view b/w anchors if the pan gesture
// doesn't pass through .changed state after an interruptible animator is interrupted.
let dy = translation.y - .leastNonzeroMagnitude
layoutAdapter.updateInteractiveTopConstraint(diff: dy,
allowsTopBuffer: true,
with: behavior)
}
panningEnd(with: translation, velocity: velocity)
default:
break
}
default:
return
}
}
private func shouldScrollViewHandleTouch(_ scrollView: UIScrollView?, point: CGPoint, velocity: CGPoint) -> Bool {
// When no scrollView, nothing to handle.
guard let scrollView = scrollView else { return false }
// For _UISwipeActionPanGestureRecognizer
if let scrollGestureRecognizers = scrollView.gestureRecognizers {
for gesture in scrollGestureRecognizers {
guard gesture.state == .began || gesture.state == .changed
else { continue }
if gesture != scrollView.panGestureRecognizer {
return true
}
}
}
guard
state == layoutAdapter.topMostState, // When not top most(i.e. .full), don't scroll.
interactionInProgress == false, // When interaction already in progress, don't scroll.
surfaceView.frame.minY == layoutAdapter.topY
else {
return false
}
// When the current and initial point within grabber area, do scroll.
if grabberAreaFrame.contains(point), !grabberAreaFrame.contains(initialLocation) {
return true
}
guard
scrollView.frame.contains(initialLocation), // When initialLocation not in scrollView, don't scroll.
!grabberAreaFrame.contains(point) // When point within grabber area, don't scroll.
else {
return false
}
let offset = scrollView.contentOffset.y - scrollView.contentOffsetZero.y
// The zero offset must be excluded because the offset is usually zero
// after a panel moves from half/tip to full.
if offset > 0.0 {
return true
}
if scrollView.isDecelerating {
return true
}
if velocity.y <= 0 {
return true
}
return false
}
private func panningBegan(at location: CGPoint) {
// A user interaction does not always start from Began state of the pan gesture
// because it can be recognized in scrolling a content in a content view controller.
// So here just preserve the current state if needed.
log.debug("panningBegan -- location = \(location.y)")
initialLocation = location
guard let scrollView = scrollView else { return }
if state == layoutAdapter.topMostState {
if grabberAreaFrame.contains(location) {
initialScrollOffset = scrollView.contentOffset
}
} else {
initialScrollOffset = scrollView.contentOffset
}
}
private func panningChange(with translation: CGPoint) {
log.debug("panningChange -- translation = \(translation.y)")
let preY = surfaceView.frame.minY
let dy = translation.y - initialTranslationY
layoutAdapter.updateInteractiveTopConstraint(diff: dy,
allowsTopBuffer: allowsTopBuffer(for: dy),
with: behavior)
let currentY = surfaceView.frame.minY
backdropView.alpha = getBackdropAlpha(at: currentY, with: translation)
preserveContentVCLayoutIfNeeded()
let didMove = (preY != currentY)
guard didMove else { return }
if let vc = viewcontroller {
vc.delegate?.floatingPanelDidMove(vc)
}
}
private func allowsTopBuffer(for translationY: CGFloat) -> Bool {
let preY = surfaceView.frame.minY
let nextY = initialFrame.offsetBy(dx: 0.0, dy: translationY).minY
if let scrollView = scrollView, scrollView.panGestureRecognizer.state == .changed,
preY > 0 && preY > nextY {
return false
} else {
return true
}
}
private var disabledBottomAutoLayout = false
private var disabledAutoLayoutItems: Set<NSLayoutConstraint> = []
// 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 {
disabledAutoLayoutItems.removeAll()
vc.contentViewController?.view?.constraints.forEach({ (const) in
switch vc.contentViewController?.layoutGuide.bottomAnchor {
case const.firstAnchor:
(const.secondItem as? UIView)?.disableAutoLayout()
const.isActive = false
disabledAutoLayoutItems.insert(const)
case const.secondAnchor:
(const.firstItem as? UIView)?.disableAutoLayout()
const.isActive = false
disabledAutoLayoutItems.insert(const)
default:
break
}
})
}
disabledBottomAutoLayout = true
} else {
if disabledBottomAutoLayout {
disabledAutoLayoutItems.forEach({ (const) in
switch vc.contentViewController?.layoutGuide.bottomAnchor {
case const.firstAnchor:
(const.secondItem as? UIView)?.enableAutoLayout()
const.isActive = true
case const.secondAnchor:
(const.firstItem as? UIView)?.enableAutoLayout()
const.isActive = true
default:
break
}
})
disabledAutoLayoutItems.removeAll()
}
disabledBottomAutoLayout = false
}
}
private func panningEnd(with translation: CGPoint, velocity: CGPoint) {
log.debug("panningEnd -- translation = \(translation.y), velocity = \(velocity.y)")
if state == .hidden {
log.debug("Already hidden")
return
}
stopScrollDeceleration = (surfaceView.frame.minY > layoutAdapter.topY) // Projecting the dragging to the scroll dragging or not
if stopScrollDeceleration {
DispatchQueue.main.async { [weak self] in
guard let `self` = self else { return }
self.stopScrollingWithDeceleration(at: self.initialScrollOffset)
}
}
let currentY = surfaceView.frame.minY
let targetPosition = self.targetPosition(from: currentY, with: velocity)
let distance = self.distance(to: targetPosition)
endInteraction(for: targetPosition)
if isRemovalInteractionEnabled, isBottomState {
let velocityVector = (distance != 0) ? CGVector(dx: 0, dy: min(velocity.y/distance, behavior.removalVelocity)) : .zero
// `velocityVector` will be replaced by just a velocity(not vector) when FloatingPanelRemovalInteraction will be added.
if shouldStartRemovalAnimation(with: velocityVector), let vc = viewcontroller {
vc.delegate?.floatingPanelDidEndDraggingToRemove(vc, withVelocity: velocity)
let animationVector = CGVector(dx: abs(velocityVector.dx), dy: abs(velocityVector.dy))
startRemovalAnimation(vc, with: animationVector) { [weak self] in
self?.finishRemovalAnimation()
}
return
}
}
if let vc = viewcontroller {
vc.delegate?.floatingPanelDidEndDragging(vc, withVelocity: velocity, targetPosition: targetPosition)
}
if scrollView != nil, !stopScrollDeceleration,
surfaceView.frame.minY == layoutAdapter.topY,
targetPosition == layoutAdapter.topMostState {
self.state = targetPosition
self.updateLayout(to: targetPosition)
self.unlockScrollView()
return
}
// Workaround: Disable a tracking scroll to prevent bouncing a scroll content in a panel animating
let isScrollEnabled = scrollView?.isScrollEnabled
if let scrollView = scrollView, targetPosition != .full {
scrollView.isScrollEnabled = false
}
startAnimation(to: targetPosition, at: distance, with: velocity)
// Workaround: Reset `self.scrollView.isScrollEnabled`
if let scrollView = scrollView, targetPosition != .full,
let isScrollEnabled = isScrollEnabled {
scrollView.isScrollEnabled = isScrollEnabled
}
}
private func shouldStartRemovalAnimation(with velocityVector: CGVector) -> Bool {
let posY = layoutAdapter.positionY(for: state)
let currentY = surfaceView.frame.minY
let hiddenY = layoutAdapter.positionY(for: .hidden)
let vth = behavior.removalVelocity
let pth = max(min(behavior.removalProgress, 1.0), 0.0)
let num = (currentY - posY)
let den = (hiddenY - posY)
guard num >= 0, den != 0, (num / den >= pth || velocityVector.dy == vth)
else { return false }
return true
}
private func startRemovalAnimation(_ vc: FloatingPanelController, with velocityVector: CGVector, completion: (() -> Void)?) {
let animator = behavior.removalInteractionAnimator(vc, with: velocityVector)
animator.addAnimations { [weak self] in
self?.state = .hidden
self?.updateLayout(to: .hidden)
}
animator.addCompletion({ _ in
self.animator = nil
completion?()
})
self.animator = animator
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)")
guard interactionInProgress == false else { return }
var offset: CGPoint = .zero
initialFrame = surfaceView.frame
if state == layoutAdapter.topMostState, let scrollView = scrollView {
if grabberAreaFrame.contains(location) {
initialScrollOffset = scrollView.contentOffset
} else {
// Fit the surface bounds to a scroll offset content by startInteraction(at:offset:)
offset = CGPoint(x: -scrollView.contentOffset.x, y: -scrollView.contentOffset.y)
initialScrollOffset = scrollView.contentOffsetZero
}
log.debug("initial scroll offset --", initialScrollOffset)
}
initialTranslationY = translation.y
if let vc = viewcontroller {
vc.delegate?.floatingPanelWillBeginDragging(vc)
}
layoutAdapter.startInteraction(at: state, offset: offset)
interactionInProgress = true
lockScrollView()
}
private func endInteraction(for targetPosition: FloatingPanelPosition) {
log.debug("endInteraction to \(targetPosition)")
if let scrollView = scrollView {
log.debug("endInteraction -- scroll offset = \(scrollView.contentOffset)")
}
interactionInProgress = false
// Prevent to keep a scroll view indicator visible at the half/tip position
if targetPosition != layoutAdapter.topMostState {
lockScrollView()
}
layoutAdapter.endInteraction(at: targetPosition)
}
private func tearDownActiveInteraction() {
// Cancel the pan gesture so that panningEnd(with:velocity:) is called
panGestureRecognizer.isEnabled = false
panGestureRecognizer.isEnabled = true
}
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
vc.delegate?.floatingPanelWillBeginDecelerating(vc)
let velocityVector = (distance != 0) ? CGVector(dx: 0, dy: abs(velocity.y)/distance) : .zero
let animator = behavior.interactionAnimator(vc, to: targetPosition, with: velocityVector)
animator.addAnimations { [weak self] in
guard let `self` = self else { return }
self.state = targetPosition
self.updateLayout(to: targetPosition)
}
animator.addCompletion { [weak self] pos in
// Prevent calling `finishAnimation(at:)` by the old animator whose `isInterruptive` is false
// when a new animator has been started after the old one is interrupted.
guard let `self` = self, self.animator == animator else { return }
self.finishAnimation(at: targetPosition)
}
self.animator = animator
animator.startAnimation()
}
private func finishAnimation(at targetPosition: FloatingPanelPosition) {
log.debug("finishAnimation to \(targetPosition)")
self.isDecelerating = false
self.animator = nil
if let vc = viewcontroller {
vc.delegate?.floatingPanelDidEndDecelerating(vc)
}
if let scrollView = scrollView {
log.debug("finishAnimation -- scroll offset = \(scrollView.contentOffset)")
}
stopScrollDeceleration = false
log.debug("finishAnimation -- state = \(state) surface.minY = \(surfaceView.presentationFrame.minY) topY = \(layoutAdapter.topY)")
if state == layoutAdapter.topMostState, abs(surfaceView.presentationFrame.minY - layoutAdapter.topY) <= 1.0 {
unlockScrollView()
}
}
private func distance(to targetPosition: FloatingPanelPosition) -> CGFloat {
let currentY = surfaceView.frame.minY
let targetY = layoutAdapter.positionY(for: targetPosition)
return CGFloat(abs(currentY - targetY))
}
// Distance travelled after decelerating to zero velocity at a constant rate.
// Refer to the slides p176 of [Designing Fluid Interfaces](https://developer.apple.com/videos/play/wwdc2018/803/)
private func project(initialVelocity: CGFloat, decelerationRate: CGFloat) -> CGFloat {
return (initialVelocity / 1000.0) * decelerationRate / (1.0 - decelerationRate)
}
func targetPosition(from currentY: CGFloat, with velocity: CGPoint) -> (FloatingPanelPosition) {
guard let vc = viewcontroller else { return state }
let supportedPositions = layoutAdapter.supportedPositions
guard supportedPositions.count > 1 else {
return state
}
let sortedPositions = Array(supportedPositions).sorted(by: { $0.rawValue < $1.rawValue })
// Projection
let decelerationRate = behavior.momentumProjectionRate(vc)
let baseY = abs(layoutAdapter.positionY(for: layoutAdapter.bottomMostState) - layoutAdapter.positionY(for: layoutAdapter.topMostState))
let vecY = velocity.y / baseY
var pY = project(initialVelocity: vecY, decelerationRate: decelerationRate) * baseY + currentY
let forwardY = velocity.y == 0 ? (currentY - layoutAdapter.positionY(for: state) > 0) : velocity.y > 0
let segment = layoutAdapter.segument(at: pY, forward: forwardY)
var fromPos: FloatingPanelPosition
var toPos: FloatingPanelPosition
let (lowerPos, upperPos) = (segment.lower ?? sortedPositions.first!, segment.upper ?? sortedPositions.last!)
(fromPos, toPos) = forwardY ? (lowerPos, upperPos) : (upperPos, lowerPos)
if behavior.shouldProjectMomentum(vc, for: toPos) == false {
let segment = layoutAdapter.segument(at: currentY, forward: forwardY)
var (lowerPos, upperPos) = (segment.lower ?? sortedPositions.first!, segment.upper ?? sortedPositions.last!)
// Equate the segment out of {top,bottom} most state to the {top,bottom} most segment
if lowerPos == upperPos {
if forwardY {
upperPos = lowerPos.next(in: sortedPositions)
} else {
lowerPos = upperPos.pre(in: sortedPositions)
}
}
(fromPos, toPos) = forwardY ? (lowerPos, upperPos) : (upperPos, lowerPos)
// Block a projection to a segment over the next from the current segment
// (= Trim pY with the current segment)
if forwardY {
pY = max(min(pY, layoutAdapter.positionY(for: toPos.next(in: sortedPositions))), layoutAdapter.positionY(for: fromPos))
} else {
pY = max(min(pY, layoutAdapter.positionY(for: fromPos)), layoutAdapter.positionY(for: toPos.pre(in: sortedPositions)))
}
}
// Redirection
let redirectionalProgress = max(min(behavior.redirectionalProgress(vc, from: fromPos, to: toPos), 1.0), 0.0)
let progress = abs(pY - layoutAdapter.positionY(for: fromPos)) / abs(layoutAdapter.positionY(for: fromPos) - layoutAdapter.positionY(for: toPos))
return progress > redirectionalProgress ? toPos : fromPos
}
// MARK: - ScrollView handling
private func lockScrollView() {
guard let scrollView = scrollView else { return }
if scrollView.isLocked {
log.debug("Already scroll locked.")
return
}
log.debug("lock scroll view")
scrollBouncable = scrollView.bounces
scrollIndictorVisible = scrollView.showsVerticalScrollIndicator
scrollView.isDirectionalLockEnabled = true
scrollView.bounces = false
scrollView.showsVerticalScrollIndicator = false
}
private func unlockScrollView() {
guard let scrollView = scrollView, scrollView.isLocked else { return }
log.debug("unlock scroll view")
scrollView.isDirectionalLockEnabled = false
scrollView.bounces = scrollBouncable
scrollView.showsVerticalScrollIndicator = scrollIndictorVisible
}
private func stopScrollingWithDeceleration(at contentOffset: CGPoint) {
// Must use setContentOffset(_:animated) to force-stop deceleration
scrollView?.setContentOffset(contentOffset, animated: false)
}
}
class FloatingPanelPanGestureRecognizer: UIPanGestureRecognizer {
fileprivate weak var floatingPanel: FloatingPanel?
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event)
if floatingPanel?.animator != nil {
self.state = .began
}
}
override weak var delegate: UIGestureRecognizerDelegate? {
get {
return super.delegate
}
set {
guard newValue is FloatingPanel else {
let exception = NSException(name: .invalidArgumentException,
reason: "FloatingPanelController's built-in pan gesture recognizer must have its controller as its delegate.",
userInfo: nil)
exception.raise()
return
}
super.delegate = newValue
}
}
}
@@ -1,9 +0,0 @@
//
// Created by Shin Yamamoto on 2018/09/26.
// Copyright © 2018 Shin Yamamoto. All rights reserved.
//
import UIKit
/// A view that presents a backdrop interface behind a floating panel.
public class FloatingPanelBackdropView: UIView { }
@@ -1,156 +0,0 @@
//
// Created by Shin Yamamoto on 2018/10/03.
// Copyright © 2018 Shin Yamamoto. All rights reserved.
//
import UIKit
public protocol FloatingPanelBehavior {
/// Asks the behavior if the floating panel should project a momentum of a user interaction to move the proposed position.
///
/// The default implementation of this method returns true. This method is called for a layout to support all positions(tip, half and full).
/// Therefore, `proposedTargetPosition` can only be `FloatingPanelPosition.tip` or `FloatingPanelPosition.full`.
func shouldProjectMomentum(_ fpc: FloatingPanelController, for proposedTargetPosition: FloatingPanelPosition) -> Bool
/// Returns a deceleration rate to calculate a target position projected a dragging momentum.
///
/// The default implementation of this method returns the normal deceleration rate of UIScrollView.
func momentumProjectionRate(_ fpc: FloatingPanelController) -> CGFloat
/// 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 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.
///
/// - Attention:
/// By default, it returns a non-interruptible animator to prevent a propagation of the animation to a content view.
/// However returning an interruptible animator is working well depending on a content view and it can be better
/// than using a non-interruptible one.
func interactionAnimator(_ fpc: FloatingPanelController, to targetPosition: FloatingPanelPosition, with velocity: CGVector) -> UIViewPropertyAnimator
/// Returns a UIViewPropertyAnimator object to add a floating panel to a position.
///
/// Its animator instance will be used to animate the surface view in `FloatingPanelController.addPanel(toParent:belowView:animated:)`.
/// Default is an animator with ease-in-out curve and 0.25 sec duration.
func addAnimator(_ fpc: FloatingPanelController, to: FloatingPanelPosition) -> UIViewPropertyAnimator
/// Returns a UIViewPropertyAnimator object to remove a floating panel from a position.
///
/// Its animator instance will be used to animate the surface view in `FloatingPanelController.removePanelFromParent(animated:completion:)`.
/// Default is an animator with ease-in-out curve and 0.25 sec duration.
func removeAnimator(_ fpc: FloatingPanelController, from: FloatingPanelPosition) -> UIViewPropertyAnimator
/// Returns a UIViewPropertyAnimator object to move a floating panel from a position to a position.
///
/// Its animator instance will be used to animate the surface view in `FloatingPanelController.move(to:animated:completion:)`.
/// Default is an animator with ease-in-out curve and 0.25 sec duration.
func moveAnimator(_ fpc: FloatingPanelController, from: FloatingPanelPosition, to: FloatingPanelPosition) -> UIViewPropertyAnimator
/// Returns a y-axis velocity to invoke a removal interaction at the bottom position.
///
/// Default is 10.0. This method is called when FloatingPanelController.isRemovalInteractionEnabled is true.
var removalVelocity: CGFloat { get }
/// Returns the threshold of the transition to invoke a removal interaction at the bottom 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 invoke the removal interaction. The default value is 0.5. Values less than 0.0 and greater than 1.0 are pinned to those limits. This method is called when FloatingPanelController.isRemovalInteractionEnabled is true.
var removalProgress: CGFloat { get }
/// Returns a UIViewPropertyAnimator object to remove a floating panel with a velocity interactively at the bottom position.
///
/// 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 {
func shouldProjectMomentum(_ fpc: FloatingPanelController, for proposedTargetPosition: FloatingPanelPosition) -> Bool {
return false
}
func momentumProjectionRate(_ fpc: FloatingPanelController) -> CGFloat {
#if swift(>=4.2)
return UIScrollView.DecelerationRate.normal.rawValue
#else
return UIScrollViewDecelerationRateNormal
#endif
}
func redirectionalProgress(_ fpc: FloatingPanelController, from: FloatingPanelPosition, to: FloatingPanelPosition) -> CGFloat {
return 0.5
}
func interactionAnimator(_ fpc: FloatingPanelController, to targetPosition: FloatingPanelPosition, with velocity: CGVector) -> UIViewPropertyAnimator {
return defaultBehavior.interactionAnimator(fpc, to: targetPosition, with: velocity)
}
func addAnimator(_ fpc: FloatingPanelController, to: FloatingPanelPosition) -> UIViewPropertyAnimator {
return UIViewPropertyAnimator(duration: 0.25, curve: .easeInOut)
}
func removeAnimator(_ fpc: FloatingPanelController, from: FloatingPanelPosition) -> UIViewPropertyAnimator {
return UIViewPropertyAnimator(duration: 0.25, curve: .easeInOut)
}
func moveAnimator(_ fpc: FloatingPanelController, from: FloatingPanelPosition, to: FloatingPanelPosition) -> UIViewPropertyAnimator {
return UIViewPropertyAnimator(duration: 0.25, curve: .easeInOut)
}
var removalVelocity: CGFloat {
return 10.0
}
var removalProgress: CGFloat {
return 0.5
}
func removalInteractionAnimator(_ fpc: FloatingPanelController, with velocity: CGVector) -> UIViewPropertyAnimator {
log.debug("velocity", velocity)
let timing = UISpringTimingParameters(dampingRatio: 1.0,
frequencyResponse: 0.3,
initialVelocity: velocity)
return UIViewPropertyAnimator(duration: 0, timingParameters: timing)
}
func allowsRubberBanding(for edge: UIRectEdge) -> Bool {
return false
}
}
private let defaultBehavior = FloatingPanelDefaultBehavior()
public class FloatingPanelDefaultBehavior: FloatingPanelBehavior {
public init() { }
public func interactionAnimator(_ fpc: FloatingPanelController, to targetPosition: FloatingPanelPosition, with velocity: CGVector) -> UIViewPropertyAnimator {
let timing = timeingCurve(with: velocity)
let animator = UIViewPropertyAnimator(duration: 0, timingParameters: timing)
animator.isInterruptible = false // Prevent a propagation of the animation(spring etc) to a content view
return animator
}
private func timeingCurve(with velocity: CGVector) -> UITimingCurveProvider {
log.debug("velocity", velocity)
let damping = self.getDamping(with: velocity)
return UISpringTimingParameters(dampingRatio: damping,
frequencyResponse: 0.3,
initialVelocity: velocity)
}
private let velocityThreshold: CGFloat = 8.0
private func getDamping(with velocity: CGVector) -> CGFloat {
let dy = abs(velocity.dy)
if dy > velocityThreshold {
return 0.7
} else {
return 1.0
}
}
}
@@ -1,608 +0,0 @@
//
// Created by Shin Yamamoto on 2018/09/18.
// Copyright © 2018 Shin Yamamoto. All rights reserved.
//
import UIKit
public protocol FloatingPanelControllerDelegate: class {
// if it returns nil, FloatingPanelController uses the default layout
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout?
// if it returns nil, FloatingPanelController uses the default behavior
func floatingPanel(_ vc: FloatingPanelController, behaviorFor newCollection: UITraitCollection) -> FloatingPanelBehavior?
/// Called when the floating panel has changed to a new position. Can be called inside an animation block, so any
/// view properties set inside this function will be automatically animated alongside the panel.
func floatingPanelDidChangePosition(_ vc: FloatingPanelController)
/// Asks the delegate if dragging should begin by the pan gesture recognizer.
func floatingPanelShouldBeginDragging(_ vc: FloatingPanelController) -> Bool
func floatingPanelDidMove(_ vc: FloatingPanelController) // any surface frame changes in dragging
// called on start of dragging (may require some time and or distance to move)
func floatingPanelWillBeginDragging(_ vc: FloatingPanelController)
// called on finger up if the user dragged. velocity is in points/second.
func floatingPanelDidEndDragging(_ vc: FloatingPanelController, withVelocity velocity: CGPoint, targetPosition: FloatingPanelPosition)
func floatingPanelWillBeginDecelerating(_ vc: FloatingPanelController) // called on finger up as we are moving
func floatingPanelDidEndDecelerating(_ vc: FloatingPanelController) // called when scroll view grinds to a halt
// called on start of dragging to remove its views from a parent view controller
func floatingPanelDidEndDraggingToRemove(_ vc: FloatingPanelController, withVelocity velocity: CGPoint)
// called when its views are removed from a parent view controller
func floatingPanelDidEndRemove(_ vc: FloatingPanelController)
/// Asks the delegate if the other gesture recognizer should be allowed to recognize the gesture in parallel.
///
/// By default, any tap and long gesture recognizers are allowed to recognize gestures simultaneously.
func floatingPanel(_ vc: FloatingPanelController, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool
}
public extension FloatingPanelControllerDelegate {
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? {
return nil
}
func floatingPanel(_ vc: FloatingPanelController, behaviorFor newCollection: UITraitCollection) -> FloatingPanelBehavior? {
return nil
}
func floatingPanelDidChangePosition(_ vc: FloatingPanelController) {}
func floatingPanelShouldBeginDragging(_ vc: FloatingPanelController) -> Bool {
return true
}
func floatingPanelDidMove(_ vc: FloatingPanelController) {}
func floatingPanelWillBeginDragging(_ vc: FloatingPanelController) {}
func floatingPanelDidEndDragging(_ vc: FloatingPanelController, withVelocity velocity: CGPoint, targetPosition: FloatingPanelPosition) {}
func floatingPanelWillBeginDecelerating(_ vc: FloatingPanelController) {}
func floatingPanelDidEndDecelerating(_ vc: FloatingPanelController) {}
func floatingPanelDidEndDraggingToRemove(_ vc: FloatingPanelController, withVelocity velocity: CGPoint) {}
func floatingPanelDidEndRemove(_ vc: FloatingPanelController) {}
func floatingPanel(_ vc: FloatingPanelController, shouldRecognizeSimultaneouslyWith gestureRecognizer: UIGestureRecognizer) -> Bool {
return false
}
}
public enum FloatingPanelPosition: Int {
case full
case half
case tip
case hidden
static var allCases: [FloatingPanelPosition] {
return [.full, .half, .tip, .hidden]
}
func next(in positions: [FloatingPanelPosition]) -> FloatingPanelPosition {
#if swift(>=4.2)
guard
let index = positions.firstIndex(of: self),
positions.indices.contains(index + 1)
else { return self }
#else
guard
let index = positions.index(of: self),
positions.indices.contains(index + 1)
else { return self }
#endif
return positions[index + 1]
}
func pre(in positions: [FloatingPanelPosition]) -> FloatingPanelPosition {
#if swift(>=4.2)
guard
let index = positions.firstIndex(of: self),
positions.indices.contains(index - 1)
else { return self }
#else
guard
let index = positions.index(of: self),
positions.indices.contains(index - 1)
else { return self }
#endif
return positions[index - 1]
}
}
///
/// A container view controller to display a floating panel to present contents in parallel as a user wants.
///
open class FloatingPanelController: UIViewController {
/// Constants indicating how safe area insets are added to the adjusted content inset.
public enum ContentInsetAdjustmentBehavior: Int {
case always
case never
}
/// The delegate of the floating panel controller object.
public weak var delegate: FloatingPanelControllerDelegate?{
didSet{
didUpdateDelegate()
}
}
/// Returns the surface view managed by the controller object. It's the same as `self.view`.
public var surfaceView: FloatingPanelSurfaceView! {
return floatingPanel.surfaceView
}
/// Returns the backdrop view managed by the controller object.
public var backdropView: FloatingPanelBackdropView! {
return floatingPanel.backdropView
}
/// Returns the scroll view that the controller tracks.
public weak var scrollView: UIScrollView? {
return floatingPanel.scrollView
}
// The underlying gesture recognizer for pan gestures
public var panGestureRecognizer: UIPanGestureRecognizer {
return floatingPanel.panGestureRecognizer
}
/// The current position of the floating panel controller's contents.
public var position: FloatingPanelPosition {
return floatingPanel.state
}
/// The layout object managed by the controller
public var layout: FloatingPanelLayout {
return floatingPanel.layoutAdapter.layout
}
/// The behavior object managed by the controller
public var behavior: FloatingPanelBehavior {
return floatingPanel.behavior
}
/// The content insets of the tracking scroll view derived from this safe area
public var adjustedContentInsets: UIEdgeInsets {
return floatingPanel.layoutAdapter.adjustedContentInsets
}
/// The behavior for determining the adjusted content offsets.
///
/// This property specifies how the content area of the tracking scroll view is modified using `adjustedContentInsets`. The default value of this property is FloatingPanelController.ContentInsetAdjustmentBehavior.always.
public var contentInsetAdjustmentBehavior: ContentInsetAdjustmentBehavior = .always
/// A Boolean value that determines whether the removal interaction is enabled.
public var isRemovalInteractionEnabled: Bool {
set { floatingPanel.isRemovalInteractionEnabled = newValue }
get { return floatingPanel.isRemovalInteractionEnabled }
}
/// The view controller responsible for the content portion of the floating panel.
public var contentViewController: UIViewController? {
set { set(contentViewController: newValue) }
get { return _contentViewController }
}
private var _contentViewController: UIViewController?
private(set) var floatingPanel: FloatingPanel!
private var preSafeAreaInsets: UIEdgeInsets = .zero // Capture the latest one
private var safeAreaInsetsObservation: NSKeyValueObservation?
private let modalTransition = FloatingPanelModalTransition()
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setUp()
}
/// Initialize a newly created floating panel controller.
public init(delegate: FloatingPanelControllerDelegate? = nil) {
super.init(nibName: nil, bundle: nil)
self.delegate = delegate
setUp()
}
private func setUp() {
_ = FloatingPanelController.dismissSwizzling
modalPresentationStyle = .custom
transitioningDelegate = modalTransition
floatingPanel = FloatingPanel(self,
layout: fetchLayout(for: self.traitCollection),
behavior: fetchBehavior(for: self.traitCollection))
}
private func didUpdateDelegate(){
floatingPanel.layoutAdapter.layout = fetchLayout(for: traitCollection)
floatingPanel.behavior = fetchBehavior(for: self.traitCollection)
}
// MARK:- Overrides
/// Creates the view that the controller manages.
open override func loadView() {
assert(self.storyboard == nil, "Storyboard isn't supported")
let view = FloatingPanelPassThroughView()
view.backgroundColor = .clear
backdropView.frame = view.bounds
view.addSubview(backdropView)
surfaceView.frame = view.bounds
view.addSubview(surfaceView)
self.view = view as UIView
}
open override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if #available(iOS 11.0, *) {}
else {
// Because {top,bottom}LayoutGuide is managed as a view
if preSafeAreaInsets != layoutInsets,
floatingPanel.isDecelerating == false {
self.update(safeAreaInsets: layoutInsets)
}
}
}
open override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
if view.translatesAutoresizingMaskIntoConstraints {
view.frame.size = size
view.layoutIfNeeded()
}
}
open override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) {
super.willTransition(to: newCollection, with: coordinator)
self.prepare(for: newCollection)
}
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 {
switch traitCollection.verticalSizeClass {
case .compact:
return self.delegate?.floatingPanel(self, layoutFor: traitCollection) ?? FloatingPanelDefaultLandscapeLayout()
default:
return self.delegate?.floatingPanel(self, layoutFor: traitCollection) ?? FloatingPanelDefaultLayout()
}
}
private func fetchBehavior(for traitCollection: UITraitCollection) -> FloatingPanelBehavior {
return self.delegate?.floatingPanel(self, behaviorFor: traitCollection) ?? FloatingPanelDefaultBehavior()
}
private func update(safeAreaInsets: UIEdgeInsets) {
guard
preSafeAreaInsets != safeAreaInsets
else { return }
log.debug("Update safeAreaInsets", safeAreaInsets)
// Prevent an infinite loop on iOS 10: setUpLayout() -> viewDidLayoutSubviews() -> setUpLayout()
preSafeAreaInsets = safeAreaInsets
activateLayout()
switch contentInsetAdjustmentBehavior {
case .always:
scrollView?.contentInset = adjustedContentInsets
scrollView?.scrollIndicatorInsets = adjustedContentInsets
default:
break
}
}
private func reloadLayout(for traitCollection: UITraitCollection) {
floatingPanel.layoutAdapter.layout = fetchLayout(for: traitCollection)
floatingPanel.layoutAdapter.prepareLayout(in: self)
if let parent = self.parent {
if let layout = layout as? UIViewController, layout == parent {
log.warning("A memory leak will occur by a retain cycle because \(self) owns the parent view controller(\(parent)) as the layout object. Don't let the parent adopt FloatingPanelLayout.")
}
if let behavior = behavior as? UIViewController, behavior == parent {
log.warning("A memory leak will occur by a retain cycle because \(self) owns the parent view controller(\(parent)) as the behavior object. Don't let the parent adopt FloatingPanelBehavior.")
}
}
}
private func activateLayout() {
// preserve the current content offset
let contentOffset = scrollView?.contentOffset
floatingPanel.layoutAdapter.updateHeight()
floatingPanel.layoutAdapter.activateLayout(of: floatingPanel.state)
scrollView?.contentOffset = contentOffset ?? .zero
}
// MARK: - Container view controller interface
/// Shows the surface view at the initial position defined by the current layout
public func show(animated: Bool = false, completion: (() -> Void)? = nil) {
// Must apply the current layout here
reloadLayout(for: traitCollection)
activateLayout()
if #available(iOS 11.0, *) {
// Must track the safeAreaInsets of `self.view` to update the layout.
// There are 2 reasons.
// 1. This or the parent VC doesn't call viewSafeAreaInsetsDidChange() on the bottom
// inset's update expectedly.
// 2. The safe area top inset can be variable on the large title navigation bar(iOS11+).
// That's why it needs the observation to keep `adjustedContentInsets` correct.
safeAreaInsetsObservation = self.observe(\.view.safeAreaInsets, options: [.initial, .new, .old]) { [weak self] (vc, change) in
guard change.oldValue != change.newValue else { return }
self?.update(safeAreaInsets: vc.layoutInsets)
}
} else {
// KVOs for topLayoutGuide & bottomLayoutGuide are not effective.
// Instead, update(safeAreaInsets:) is called at `viewDidLayoutSubviews()`
}
move(to: floatingPanel.layoutAdapter.layout.initialPosition,
animated: animated,
completion: completion)
}
/// Hides the surface view to the hidden position
public func hide(animated: Bool = false, completion: (() -> Void)? = nil) {
move(to: .hidden,
animated: animated,
completion: completion)
}
/// Adds the view managed by the controller as a child of the specified view controller.
/// - Parameters:
/// - parent: A parent view controller object that displays FloatingPanelController's view. A container view controller object isn't applicable.
/// - belowView: Insert the surface view managed by the controller below the specified view. By default, the surface view will be added to the end of the parent list of subviews.
/// - animated: Pass true to animate the presentation; otherwise, pass false.
public func addPanel(toParent parent: UIViewController, belowView: UIView? = nil, animated: Bool = false) {
guard self.parent == nil else {
log.warning("Already added to a parent(\(parent))")
return
}
precondition((parent is UINavigationController) == false, "UINavigationController displays only one child view controller at a time.")
precondition((parent is UITabBarController) == false, "UITabBarController displays child view controllers with a radio-style selection interface")
precondition((parent is UISplitViewController) == false, "UISplitViewController manages two child view controllers in a master-detail interface")
precondition((parent is UITableViewController) == false, "UITableViewController should not be the parent because the view is a table view so that a floating panel doens't work well")
precondition((parent is UICollectionViewController) == false, "UICollectionViewController should not be the parent because the view is a collection view so that a floating panel doens't work well")
if let belowView = belowView {
parent.view.insertSubview(self.view, belowSubview: belowView)
} else {
parent.view.addSubview(self.view)
}
#if swift(>=4.2)
parent.addChild(self)
#else
parent.addChildViewController(self)
#endif
view.frame = parent.view.bounds // Needed for a correct safe area configuration
view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
self.view.topAnchor.constraint(equalTo: parent.view.topAnchor, constant: 0.0),
self.view.leftAnchor.constraint(equalTo: parent.view.leftAnchor, constant: 0.0),
self.view.rightAnchor.constraint(equalTo: parent.view.rightAnchor, constant: 0.0),
self.view.bottomAnchor.constraint(equalTo: parent.view.bottomAnchor, constant: 0.0),
])
show(animated: animated) { [weak self] in
guard let `self` = self else { return }
#if swift(>=4.2)
self.didMove(toParent: self)
#else
self.didMove(toParentViewController: self)
#endif
}
}
/// Removes the controller and the managed view from its parent view controller
/// - Parameters:
/// - animated: Pass true to animate the presentation; otherwise, pass false.
/// - completion: The block to execute after the view controller is dismissed. This block has no return value and takes no parameters. You may specify nil for this parameter.
public func removePanelFromParent(animated: Bool, completion: (() -> Void)? = nil) {
guard self.parent != nil else {
completion?()
return
}
hide(animated: animated) { [weak self] in
guard let `self` = self else { return }
#if swift(>=4.2)
self.willMove(toParent: nil)
#else
self.willMove(toParentViewController: nil)
#endif
self.view.removeFromSuperview()
#if swift(>=4.2)
self.removeFromParent()
#else
self.removeFromParentViewController()
#endif
completion?()
}
}
/// Moves the position to the specified position.
/// - Parameters:
/// - to: Pass a FloatingPanelPosition value to move the surface view to the position.
/// - animated: Pass true to animate the presentation; otherwise, pass false.
/// - completion: The block to execute after the view controller has finished moving. This block has no return value and takes no parameters. You may specify nil for this parameter.
public func move(to: FloatingPanelPosition, animated: Bool, completion: (() -> Void)? = nil) {
precondition(floatingPanel.layoutAdapter.vc != nil, "Use show(animated:completion)")
floatingPanel.move(to: to, animated: animated, completion: completion)
}
/// Sets the view controller responsible for the content portion of the floating panel.
public func set(contentViewController: UIViewController?) {
if let vc = _contentViewController {
#if swift(>=4.2)
vc.willMove(toParent: nil)
#else
vc.willMove(toParentViewController: nil)
#endif
vc.view.removeFromSuperview()
#if swift(>=4.2)
vc.removeFromParent()
#else
vc.removeFromParentViewController()
#endif
}
if let vc = contentViewController {
#if swift(>=4.2)
addChild(vc)
#else
addChildViewController(vc)
#endif
let surfaceView = floatingPanel.surfaceView
surfaceView.add(contentView: vc.view)
#if swift(>=4.2)
vc.didMove(toParent: self)
#else
vc.didMove(toParentViewController: self)
#endif
}
_contentViewController = contentViewController
}
@available(*, unavailable, renamed: "set(contentViewController:)")
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:)")
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)
}
}
// MARK: - Scroll view tracking
/// Tracks the specified scroll view to correspond with the scroll.
///
/// - Parameters:
/// - scrollView: Specify a scroll view to continuously and seamlessly work in concert with interactions of the surface view or nil to cancel it.
public func track(scrollView: UIScrollView?) {
guard let scrollView = scrollView else {
floatingPanel.scrollView = nil
return
}
floatingPanel.scrollView = scrollView
switch contentInsetAdjustmentBehavior {
case .always:
if #available(iOS 11.0, *) {
scrollView.contentInsetAdjustmentBehavior = .never
} else {
#if swift(>=4.2)
children.forEach { (vc) in
vc.automaticallyAdjustsScrollViewInsets = false
}
#else
childViewControllers.forEach { (vc) in
vc.automaticallyAdjustsScrollViewInsets = false
}
#endif
}
default:
break
}
}
// MARK: - Utilities
/// Updates the layout object from the delegate and lays out the views managed
/// by the controller immediately.
///
/// This method updates the `FloatingPanelLayout` object from the delegate and
/// then it calls `layoutIfNeeded()` of the root view to force the view
/// to update the floating panel's layout immediately. It can be called in an
/// animation block.
public func updateLayout() {
reloadLayout(for: traitCollection)
activateLayout()
}
/// Returns the y-coordinate of the point at the origin of the surface view.
public func originYOfSurface(for pos: FloatingPanelPosition) -> CGFloat {
return floatingPanel.layoutAdapter.positionY(for: pos)
}
}
extension FloatingPanelController {
private static let dismissSwizzling: Any? = {
let aClass: AnyClass! = UIViewController.self //object_getClass(vc)
if let imp = class_getMethodImplementation(aClass, #selector(dismiss(animated:completion:))),
let originalAltMethod = class_getInstanceMethod(aClass, #selector(fp_original_dismiss(animated:completion:))) {
method_setImplementation(originalAltMethod, imp)
}
let originalMethod = class_getInstanceMethod(aClass, #selector(dismiss(animated:completion:)))
let swizzledMethod = class_getInstanceMethod(aClass, #selector(fp_dismiss(animated:completion:)))
if let originalMethod = originalMethod, let swizzledMethod = swizzledMethod {
// switch implementation..
method_exchangeImplementations(originalMethod, swizzledMethod)
}
return nil
}()
}
public extension UIViewController {
@objc func fp_original_dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
// Implementation will be replaced by IMP of self.dismiss(animated:completion:)
}
@objc func fp_dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
// Call dismiss(animated:completion:) to a content view controller
if let fpc = parent as? FloatingPanelController {
if fpc.presentingViewController != nil {
self.fp_original_dismiss(animated: flag, completion: completion)
} else {
fpc.removePanelFromParent(animated: flag, completion: completion)
}
return
}
// Call dismiss(animated:completion:) to FloatingPanelController directly
if let fpc = self as? FloatingPanelController {
if fpc.presentingViewController != nil {
self.fp_original_dismiss(animated: flag, completion: completion)
} else {
fpc.removePanelFromParent(animated: flag, completion: completion)
}
return
}
// For other view controllers
self.fp_original_dismiss(animated: flag, completion: completion)
}
}
-563
View File
@@ -1,563 +0,0 @@
//
// Created by Shin Yamamoto on 2018/09/27.
// Copyright © 2018 Shin Yamamoto. All rights reserved.
//
import UIKit
/// FloatingPanelFullScreenLayout
///
/// Use the layout protocol if you configure full, half and tip insets from the superview, not the safe area.
/// It can't be used with FloatingPanelIntrinsicLayout.
public protocol FloatingPanelFullScreenLayout: FloatingPanelLayout { }
/// FloatingPanelIntrinsicLayout
///
/// Use the layout protocol if you want to layout a panel using the intrinsic height.
/// It can't be used with FloatingPanelFullScreenLayout.
///
/// - Attention:
/// `insetFor(position:)` must return `nil` for the full position. Because
/// the inset is determined automatically by the intrinsic height.
/// You can customize insets only for the half, tip and hidden positions.
public protocol FloatingPanelIntrinsicLayout: FloatingPanelLayout { }
public extension FloatingPanelIntrinsicLayout {
var initialPosition: FloatingPanelPosition {
return .full
}
var supportedPositions: Set<FloatingPanelPosition> {
return [.full]
}
func insetFor(position: FloatingPanelPosition) -> CGFloat? {
return nil
}
}
public protocol FloatingPanelLayout: class {
/// Returns the initial position of a floating panel.
var initialPosition: FloatingPanelPosition { get }
/// Returns a set of FloatingPanelPosition objects to tell the applicable
/// positions of the floating panel controller.
///
/// By default, it returns full, half and tip positions.
var supportedPositions: Set<FloatingPanelPosition> { get }
/// Return the interaction buffer to the top from the top position. Default is 6.0.
var topInteractionBuffer: CGFloat { get }
/// Return the interaction buffer to the bottom from the bottom position. Default is 6.0.
///
/// - Important:
/// The specified buffer is ignored when `FloatingPanelController.isRemovalInteractionEnabled` is set to true.
var bottomInteractionBuffer: CGFloat { get }
/// Returns a CGFloat value to determine a Y coordinate of a floating panel for each position(full, half, tip and hidden).
///
/// Its returning value indicates a different inset for each position.
/// For full position, a top inset from a safe area in `FloatingPanelController.view`.
/// For half or tip position, a bottom inset from the safe area.
/// For hidden position, a bottom inset from `FloatingPanelController.view`.
/// If a position isn't supported or the default value is used, return nil.
func insetFor(position: FloatingPanelPosition) -> CGFloat?
/// Returns X-axis and width layout constraints of the surface view of a floating panel.
/// You must not include any Y-axis and height layout constraints of the surface view
/// because their constraints will be configured by the floating panel controller.
/// By default, the width of a surface view fits a safe area.
func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint]
/// Returns a CGFloat value to determine the backdrop view's alpha for a position.
///
/// Default is 0.3 at full position, otherwise 0.0.
func backdropAlphaFor(position: FloatingPanelPosition) -> CGFloat
}
public extension FloatingPanelLayout {
var topInteractionBuffer: CGFloat { return 6.0 }
var bottomInteractionBuffer: CGFloat { return 6.0 }
var supportedPositions: Set<FloatingPanelPosition> {
return Set([.full, .half, .tip])
}
func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint] {
return [
surfaceView.leftAnchor.constraint(equalTo: view.sideLayoutGuide.leftAnchor, constant: 0.0),
surfaceView.rightAnchor.constraint(equalTo: view.sideLayoutGuide.rightAnchor, constant: 0.0),
]
}
func backdropAlphaFor(position: FloatingPanelPosition) -> CGFloat {
return position == .full ? 0.3 : 0.0
}
}
public class FloatingPanelDefaultLayout: FloatingPanelLayout {
public init() { }
public var initialPosition: FloatingPanelPosition {
return .half
}
public func insetFor(position: FloatingPanelPosition) -> CGFloat? {
switch position {
case .full: return 18.0
case .half: return 262.0
case .tip: return 69.0
case .hidden: return nil
}
}
}
public class FloatingPanelDefaultLandscapeLayout: FloatingPanelLayout {
public init() { }
public var initialPosition: FloatingPanelPosition {
return .tip
}
public var supportedPositions: Set<FloatingPanelPosition> {
return [.full, .tip]
}
public func insetFor(position: FloatingPanelPosition) -> CGFloat? {
switch position {
case .full: return 16.0
case .tip: return 69.0
default: return nil
}
}
}
struct LayoutSegment {
let lower: FloatingPanelPosition?
let upper: FloatingPanelPosition?
}
class FloatingPanelLayoutAdapter {
weak var vc: FloatingPanelController!
private weak var surfaceView: FloatingPanelSurfaceView!
private weak var backdropView: FloatingPanelBackdropView!
var layout: FloatingPanelLayout {
didSet {
checkLayoutConsistance()
}
}
private var safeAreaInsets: UIEdgeInsets {
return vc?.layoutInsets ?? .zero
}
private var initialConst: CGFloat = 0.0
private var fixedConstraints: [NSLayoutConstraint] = []
private var fullConstraints: [NSLayoutConstraint] = []
private var halfConstraints: [NSLayoutConstraint] = []
private var tipConstraints: [NSLayoutConstraint] = []
private var offConstraints: [NSLayoutConstraint] = []
private var interactiveTopConstraint: NSLayoutConstraint?
private var heightConstraint: NSLayoutConstraint?
private var fullInset: CGFloat {
if layout is FloatingPanelIntrinsicLayout {
return intrinsicHeight
} else {
return layout.insetFor(position: .full) ?? 0.0
}
}
private var halfInset: CGFloat {
return layout.insetFor(position: .half) ?? 0.0
}
private var tipInset: CGFloat {
return layout.insetFor(position: .tip) ?? 0.0
}
private var hiddenInset: CGFloat {
return layout.insetFor(position: .hidden) ?? 0.0
}
var supportedPositions: Set<FloatingPanelPosition> {
return layout.supportedPositions
}
var topMostState: FloatingPanelPosition {
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 {
return positionY(for: topMostState)
}
var bottomY: CGFloat {
return positionY(for: bottomMostState)
}
var topMaxY: CGFloat {
return topY - layout.topInteractionBuffer
}
var bottomMaxY: CGFloat {
return bottomY + layout.bottomInteractionBuffer
}
var adjustedContentInsets: UIEdgeInsets {
return UIEdgeInsets(top: 0.0,
left: 0.0,
bottom: safeAreaInsets.bottom,
right: 0.0)
}
func positionY(for pos: FloatingPanelPosition) -> CGFloat {
switch pos {
case .full:
switch layout {
case is FloatingPanelIntrinsicLayout:
return surfaceView.superview!.bounds.height - surfaceView.bounds.height
case is FloatingPanelFullScreenLayout:
return fullInset
default:
return (safeAreaInsets.top + fullInset)
}
case .half:
switch layout {
case is FloatingPanelFullScreenLayout:
return surfaceView.superview!.bounds.height - halfInset
default:
return surfaceView.superview!.bounds.height - (safeAreaInsets.bottom + halfInset)
}
case .tip:
switch layout {
case is FloatingPanelFullScreenLayout:
return surfaceView.superview!.bounds.height - tipInset
default:
return surfaceView.superview!.bounds.height - (safeAreaInsets.bottom + tipInset)
}
case .hidden:
return surfaceView.superview!.bounds.height - hiddenInset
}
}
var intrinsicHeight: CGFloat = 0.0
init(surfaceView: FloatingPanelSurfaceView, backdropView: FloatingPanelBackdropView, layout: FloatingPanelLayout) {
self.layout = layout
self.surfaceView = surfaceView
self.backdropView = backdropView
}
func updateIntrinsicHeight() {
#if swift(>=4.2)
let fittingSize = UIView.layoutFittingCompressedSize
#else
let fittingSize = UILayoutFittingCompressedSize
#endif
var intrinsicHeight = surfaceView.contentView?.systemLayoutSizeFitting(fittingSize).height ?? 0.0
var safeAreaBottom: CGFloat = 0.0
if #available(iOS 11.0, *) {
safeAreaBottom = surfaceView.contentView?.safeAreaInsets.bottom ?? 0.0
if safeAreaBottom > 0 {
intrinsicHeight -= safeAreaInsets.bottom
}
}
self.intrinsicHeight = max(intrinsicHeight, 0.0)
log.debug("Update intrinsic height =", intrinsicHeight,
", surface(height) =", surfaceView.frame.height,
", content(height) =", surfaceView.contentView?.frame.height ?? 0.0,
", content safe area(bottom) =", safeAreaBottom)
}
func prepareLayout(in vc: FloatingPanelController) {
self.vc = vc
NSLayoutConstraint.deactivate(fixedConstraints + fullConstraints + halfConstraints + tipConstraints + offConstraints)
surfaceView.translatesAutoresizingMaskIntoConstraints = false
backdropView.translatesAutoresizingMaskIntoConstraints = false
// Fixed constraints of surface and backdrop views
let surfaceConstraints = layout.prepareLayout(surfaceView: surfaceView, in: vc.view!)
let backdropConstraints = [
backdropView.topAnchor.constraint(equalTo: vc.view.topAnchor, constant: 0.0),
backdropView.leftAnchor.constraint(equalTo: vc.view.leftAnchor,constant: 0.0),
backdropView.rightAnchor.constraint(equalTo: vc.view.rightAnchor, constant: 0.0),
backdropView.bottomAnchor.constraint(equalTo: vc.view.bottomAnchor, constant: 0.0),
]
fixedConstraints = surfaceConstraints + backdropConstraints
// Flexible surface constraints for full, half, tip and off
let topAnchor: NSLayoutYAxisAnchor = {
if layout is FloatingPanelFullScreenLayout {
return vc.view.topAnchor
} else {
return vc.layoutGuide.topAnchor
}
}()
switch layout {
case is FloatingPanelIntrinsicLayout:
// Set up on updateHeight()
break
default:
fullConstraints = [
surfaceView.topAnchor.constraint(equalTo: topAnchor,
constant: fullInset),
]
}
let bottomAnchor: NSLayoutYAxisAnchor = {
if layout is FloatingPanelFullScreenLayout {
return vc.view.bottomAnchor
} else {
return vc.layoutGuide.bottomAnchor
}
}()
halfConstraints = [
surfaceView.topAnchor.constraint(equalTo: bottomAnchor,
constant: -halfInset),
]
tipConstraints = [
surfaceView.topAnchor.constraint(equalTo: bottomAnchor,
constant: -tipInset),
]
offConstraints = [
surfaceView.topAnchor.constraint(equalTo:vc.view.bottomAnchor,
constant: -hiddenInset),
]
}
func startInteraction(at state: FloatingPanelPosition, offset: CGPoint = .zero) {
guard self.interactiveTopConstraint == nil else { return }
NSLayoutConstraint.deactivate(fullConstraints + halfConstraints + tipConstraints + offConstraints)
let interactiveTopConstraint: NSLayoutConstraint
switch layout {
case is FloatingPanelIntrinsicLayout,
is FloatingPanelFullScreenLayout:
initialConst = surfaceView.frame.minY + offset.y
interactiveTopConstraint = surfaceView.topAnchor.constraint(equalTo: vc.view.topAnchor,
constant: initialConst)
default:
initialConst = surfaceView.frame.minY - safeAreaInsets.top + offset.y
interactiveTopConstraint = surfaceView.topAnchor.constraint(equalTo: vc.layoutGuide.topAnchor,
constant: initialConst)
}
NSLayoutConstraint.activate([interactiveTopConstraint])
self.interactiveTopConstraint = interactiveTopConstraint
}
func endInteraction(at state: FloatingPanelPosition) {
// Don't deactivate `interactiveTopConstraint` here because it leads to
// unsatisfiable constraints
}
// The method is separated from prepareLayout(to:) for the rotation support
// It must be called in FloatingPanelController.traitCollectionDidChange(_:)
func updateHeight() {
guard let vc = vc else { return }
if let const = self.heightConstraint {
NSLayoutConstraint.deactivate([const])
}
let heightConstraint: NSLayoutConstraint
switch layout {
case is FloatingPanelIntrinsicLayout:
updateIntrinsicHeight()
heightConstraint = surfaceView.heightAnchor.constraint(equalToConstant: intrinsicHeight + safeAreaInsets.bottom)
default:
let const = -(positionY(for: topMostState))
heightConstraint = surfaceView.heightAnchor.constraint(equalTo: vc.view.heightAnchor,
constant: const)
}
NSLayoutConstraint.activate([heightConstraint])
self.heightConstraint = heightConstraint
surfaceView.bottomOverflow = vc.view.bounds.height + layout.topInteractionBuffer
if layout is FloatingPanelIntrinsicLayout {
NSLayoutConstraint.deactivate(fullConstraints)
fullConstraints = [
surfaceView.topAnchor.constraint(equalTo: vc.layoutGuide.bottomAnchor,
constant: -fullInset),
]
}
}
func updateInteractiveTopConstraint(diff: CGFloat, allowsTopBuffer: Bool, with behavior: FloatingPanelBehavior) {
defer {
layoutSurfaceIfNeeded() // MUST be called to update `surfaceView.frame`
}
let topMostConst: CGFloat = {
var ret: CGFloat = 0.0
switch layout {
case is FloatingPanelIntrinsicLayout, is FloatingPanelFullScreenLayout:
ret = topY
default:
ret = topY - safeAreaInsets.top
}
return max(ret, 0.0) // The top boundary is equal to the related topAnchor.
}()
let bottomMostConst: CGFloat = {
var ret: CGFloat = 0.0
let _bottomY = vc.isRemovalInteractionEnabled ? positionY(for: .hidden) : bottomY
switch layout {
case is FloatingPanelIntrinsicLayout, is FloatingPanelFullScreenLayout:
ret = _bottomY
default:
ret = _bottomY - safeAreaInsets.top
}
return min(ret, surfaceView.superview!.bounds.height)
}()
let minConst = allowsTopBuffer ? topMostConst - layout.topInteractionBuffer : topMostConst
let maxConst = bottomMostConst + layout.bottomInteractionBuffer
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) {
defer {
layoutSurfaceIfNeeded()
log.debug("activateLayout -- surface.presentation = \(self.surfaceView.presentationFrame) surface.frame = \(self.surfaceView.frame)")
}
var state = state
setBackdropAlpha(of: state)
// Must deactivate `interactiveTopConstraint` here
if let interactiveTopConstraint = interactiveTopConstraint {
NSLayoutConstraint.deactivate([interactiveTopConstraint])
self.interactiveTopConstraint = nil
}
NSLayoutConstraint.activate(fixedConstraints)
if isValid(state) == false {
state = layout.initialPosition
}
NSLayoutConstraint.deactivate(fullConstraints + halfConstraints + tipConstraints + offConstraints)
switch state {
case .full:
NSLayoutConstraint.activate(fullConstraints)
case .half:
NSLayoutConstraint.activate(halfConstraints)
case .tip:
NSLayoutConstraint.activate(tipConstraints)
case .hidden:
NSLayoutConstraint.activate(offConstraints)
}
}
func isValid(_ state: FloatingPanelPosition) -> Bool {
return supportedPositions.union([.hidden]).contains(state)
}
private func layoutSurfaceIfNeeded() {
#if !TEST
guard surfaceView.window != nil else { return }
#endif
surfaceView.superview?.layoutIfNeeded()
}
private func setBackdropAlpha(of target: FloatingPanelPosition) {
if target == .hidden {
self.backdropView.alpha = 0.0
} else {
self.backdropView.alpha = layout.backdropAlphaFor(position: target)
}
}
private func checkLayoutConsistance() {
// Verify layout configurations
assert(supportedPositions.count > 0)
assert(supportedPositions.contains(layout.initialPosition),
"Does not include an initial position (\(layout.initialPosition)) in supportedPositions (\(supportedPositions))")
if layout is FloatingPanelIntrinsicLayout {
assert(layout.insetFor(position: .full) == nil, "Return `nil` for full position on FloatingPanelIntrinsicLayout")
}
if halfInset > 0 {
assert(halfInset > tipInset, "Invalid half and tip insets")
}
// The verification isn't working on orientation change(portrait -> landscape)
// of a floating panel in tab bar. Because the `safeAreaInsets.bottom` is
// updated in delay so that it can be 83.0(not 53.0) even after the surface
// and the super view's frame is fit to landscape already.
/*if fullInset > 0 {
assert(middleY > topY, "Invalid insets { topY: \(topY), middleY: \(middleY) }")
assert(bottomY > topY, "Invalid insets { topY: \(topY), bottomY: \(bottomY) }")
}*/
}
func segument(at posY: CGFloat, forward: Bool) -> LayoutSegment {
/// ----------------------->Y
/// --> forward <-- backward
/// |-------|===o===|-------| |-------|-------|===o===|
/// |-------|-------x=======| |-------|=======x-------|
/// |-------|-------|===o===| |-------|===o===|-------|
/// pos: o/x, seguement: =
let sortedPositions = supportedPositions.sorted(by: { $0.rawValue < $1.rawValue })
let upperIndex: Int?
if forward {
#if swift(>=4.2)
upperIndex = sortedPositions.firstIndex(where: { posY < positionY(for: $0) })
#else
upperIndex = sortedPositions.index(where: { posY < positionY(for: $0) })
#endif
} else {
#if swift(>=4.2)
upperIndex = sortedPositions.firstIndex(where: { posY <= positionY(for: $0) })
#else
upperIndex = sortedPositions.index(where: { posY <= positionY(for: $0) })
#endif
}
switch upperIndex {
case 0:
return LayoutSegment(lower: nil, upper: sortedPositions.first)
case let upperIndex?:
return LayoutSegment(lower: sortedPositions[upperIndex - 1], upper: sortedPositions[upperIndex])
default:
return LayoutSegment(lower: sortedPositions[sortedPositions.endIndex - 1], upper: nil)
}
}
}
@@ -1,236 +0,0 @@
//
// Created by Shin Yamamoto on 2018/09/26.
// Copyright © 2018 Shin Yamamoto. All rights reserved.
//
import UIKit
/// A view that presents a surface interface in a floating panel.
public class FloatingPanelSurfaceView: UIView {
/// A GrabberHandleView object displayed at the top of the surface view.
///
/// To use a custom grabber handle, hide this and then add the custom one
/// to the surface view at appropriate coordinates.
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 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.
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()
public override var backgroundColor: UIColor? {
get { return color }
set { color = newValue }
}
/// The radius to use when drawing top rounded corners.
///
/// `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 {
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() } }
/// The color of the surface shadow.
public var shadowColor: UIColor = .black { didSet { setNeedsLayout() } }
/// The offset (in points) of the surface shadow.
public var shadowOffset: CGSize = CGSize(width: 0.0, height: 1.0) { didSet { setNeedsLayout() } }
/// The opacity of the surface shadow.
public var shadowOpacity: Float = 0.2 { didSet { setNeedsLayout() } }
/// The blur radius (in points) used to render the surface shadow.
public var shadowRadius: CGFloat = 3 { didSet { setNeedsLayout() } }
/// The width of the surface border.
public var borderColor: UIColor? { didSet { setNeedsLayout() } }
/// The color of the surface border.
public var borderWidth: CGFloat = 0.0 { didSet { setNeedsLayout() } }
/// Offset of the container view from the top
public var containerTopInset: CGFloat = 0.0 { didSet {
setNeedsUpdateConstraints()
} }
/// The view presents an actual surface shape.
///
/// 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)
public override class var requiresConstraintBasedLayout: Bool { return true }
override init(frame: CGRect) {
super.init(frame: frame)
addSubViews()
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
addSubViews()
}
private func addSubViews() {
super.backgroundColor = .clear
self.clipsToBounds = false
addSubview(containerView)
containerView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
containerViewTopInsetConstraint,
containerView.leftAnchor.constraint(equalTo: leftAnchor, constant: 0.0),
containerView.rightAnchor.constraint(equalTo: rightAnchor, constant: 0.0),
containerViewHeightConstraint,
])
addSubview(grabberHandle)
grabberHandle.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
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 + contentInsets.top + contentInsets.bottom)
grabberHandleTopConstraint.constant = grabberTopPadding
grabberHandleWidthConstraint.constant = grabberHandleWidth
grabberHandleHeightConstraint.constant = grabberHandleHeight
super.updateConstraints()
}
public override func layoutSubviews() {
super.layoutSubviews()
log.debug("surface view frame = \(frame)")
containerView.backgroundColor = color
updateShadow()
updateCornerRadius()
updateBorder()
}
private func updateShadow() {
if shadowHidden == false {
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 updateCornerRadius() {
guard containerView.layer.cornerRadius != 0.0 else {
containerView.layer.masksToBounds = false
return
}
containerView.layer.masksToBounds = true
if #available(iOS 11, *) {
// Don't use `contentView.clipToBounds` because it prevents content view from expanding the height of a subview of it
// for the bottom overflow like Auto Layout settings of UIVisualEffectView in Main.storyboard of Example/Maps.
// Because the bottom of contentView must be fit to the bottom of a screen to work the `safeLayoutGuide` of a content VC.
containerView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
} else {
// 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) {
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 + contentInsets.top + contentInsets.bottom))
NSLayoutConstraint.activate([
topConstraint,
leftConstraint,
rightConstraint,
heightConstraint,
])
self.contentViewTopConstraint = topConstraint
self.contentViewLeftConstraint = leftConstraint
self.contentViewRightConstraint = rightConstraint
self.contentViewHeightConstraint = heightConstraint
}
}
-142
View File
@@ -1,142 +0,0 @@
//
// Created by Shin Yamamoto on 2018/09/18.
// Copyright © 2018 Shin Yamamoto. All rights reserved.
//
import UIKit
protocol LayoutGuideProvider {
var topAnchor: NSLayoutYAxisAnchor { get }
var bottomAnchor: NSLayoutYAxisAnchor { get }
}
extension UILayoutGuide: LayoutGuideProvider {}
class CustomLayoutGuide: LayoutGuideProvider {
let topAnchor: NSLayoutYAxisAnchor
let bottomAnchor: NSLayoutYAxisAnchor
init(topAnchor: NSLayoutYAxisAnchor, bottomAnchor: NSLayoutYAxisAnchor) {
self.topAnchor = topAnchor
self.bottomAnchor = bottomAnchor
}
}
extension UIViewController {
var layoutInsets: UIEdgeInsets {
if #available(iOS 11.0, *) {
return view.safeAreaInsets
} else {
return UIEdgeInsets(top: topLayoutGuide.length,
left: 0.0,
bottom: bottomLayoutGuide.length,
right: 0.0)
}
}
var layoutGuide: LayoutGuideProvider {
if #available(iOS 11.0, *) {
return view!.safeAreaLayoutGuide
} else {
return CustomLayoutGuide(topAnchor: topLayoutGuide.bottomAnchor,
bottomAnchor: bottomLayoutGuide.topAnchor)
}
}
}
protocol SideLayoutGuideProvider {
var leftAnchor: NSLayoutXAxisAnchor { get }
var rightAnchor: NSLayoutXAxisAnchor { get }
}
extension UIView: SideLayoutGuideProvider {}
extension UILayoutGuide: SideLayoutGuideProvider {}
// The reason why UIView has no extensions of safe area insets and top/bottom guides
// is for iOS10 compat.
extension UIView {
var sideLayoutGuide: SideLayoutGuideProvider {
if #available(iOS 11.0, *) {
return safeAreaLayoutGuide
} else {
return self
}
}
var presentationFrame: CGRect {
return layer.presentation()?.frame ?? frame
}
}
extension UIView {
func disableAutoLayout() {
let frame = self.frame
translatesAutoresizingMaskIntoConstraints = true
self.frame = frame
}
func enableAutoLayout() {
translatesAutoresizingMaskIntoConstraints = false
}
}
#if __FP_LOG
#if swift(>=4.2)
extension UIGestureRecognizer.State: CustomDebugStringConvertible {
public var debugDescription: String {
switch self {
case .began: return "began"
case .changed: return "changed"
case .failed: return "failed"
case .cancelled: return "cancelled"
case .ended: return "endeded"
case .possible: return "possible"
}
}
}
#else
extension UIGestureRecognizerState: CustomDebugStringConvertible {
public var debugDescription: String {
switch self {
case .began: return "began"
case .changed: return "changed"
case .failed: return "failed"
case .cancelled: return "cancelled"
case .ended: return "endeded"
case .possible: return "possible"
}
}
}
#endif
#endif
extension UIScrollView {
var contentOffsetZero: CGPoint {
return CGPoint(x: 0.0, y: 0.0 - contentInset.top)
}
var isLocked: Bool {
return !showsVerticalScrollIndicator && !bounces && isDirectionalLockEnabled
}
}
extension UISpringTimingParameters {
public convenience init(dampingRatio: CGFloat, frequencyResponse: CGFloat, initialVelocity: CGVector = .zero) {
let mass = 1 as CGFloat
let stiffness = pow(2 * .pi / frequencyResponse, 2) * mass
let damp = 4 * .pi * dampingRatio * mass / frequencyResponse
self.init(mass: mass, stiffness: stiffness, damping: damp, initialVelocity: initialVelocity)
}
}
extension CGPoint {
static var nan: CGPoint {
return CGPoint(x: CGFloat.nan,
y: CGFloat.nan)
}
}
extension UITraitCollection {
func shouldUpdateLayout(from previous: UITraitCollection) -> Bool {
return previous.horizontalSizeClass != horizontalSizeClass
|| previous.verticalSizeClass != verticalSizeClass
|| previous.preferredContentSizeCategory != preferredContentSizeCategory
|| previous.layoutDirection != layoutDirection
}
}
@@ -1,147 +0,0 @@
//
// 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)
}
func test_moveTo() {
let delegate = FloatingPanelTestDelegate()
let fpc = FloatingPanelController(delegate: delegate)
XCTAssertEqual(delegate.position, .hidden)
fpc.showForTest()
XCTAssertEqual(delegate.position, .half)
fpc.hide()
XCTAssertEqual(delegate.position, .hidden)
fpc.move(to: .full, animated: false)
XCTAssertEqual(fpc.position, .full)
XCTAssertEqual(delegate.position, .full)
XCTAssertEqual(fpc.surfaceView.frame.minY, fpc.originYOfSurface(for: .full))
fpc.move(to: .half, animated: false)
XCTAssertEqual(fpc.position, .half)
XCTAssertEqual(delegate.position, .half)
XCTAssertEqual(fpc.surfaceView.frame.minY, fpc.originYOfSurface(for: .half))
fpc.move(to: .tip, animated: false)
XCTAssertEqual(fpc.position, .tip)
XCTAssertEqual(delegate.position, .tip)
XCTAssertEqual(fpc.surfaceView.frame.minY, fpc.originYOfSurface(for: .tip))
fpc.move(to: .hidden, animated: false)
XCTAssertEqual(fpc.position, .hidden)
XCTAssertEqual(delegate.position, .hidden)
XCTAssertEqual(fpc.surfaceView.frame.minY, fpc.originYOfSurface(for: .hidden))
fpc.move(to: .full, animated: true)
XCTAssertEqual(fpc.position, .full)
XCTAssertEqual(delegate.position, .full)
XCTAssertEqual(fpc.surfaceView.frame.minY, fpc.originYOfSurface(for: .full))
fpc.move(to: .half, animated: true)
XCTAssertEqual(fpc.position, .half)
XCTAssertEqual(delegate.position, .half)
XCTAssertEqual(fpc.surfaceView.frame.minY, fpc.originYOfSurface(for: .half))
fpc.move(to: .tip, animated: true)
XCTAssertEqual(fpc.position, .tip)
XCTAssertEqual(delegate.position, .tip)
XCTAssertEqual(fpc.surfaceView.frame.minY, fpc.originYOfSurface(for: .tip))
fpc.move(to: .hidden, animated: true)
XCTAssertEqual(fpc.position, .hidden)
XCTAssertEqual(delegate.position, .hidden)
XCTAssertEqual(fpc.surfaceView.frame.minY, fpc.originYOfSurface(for: .hidden))
}
func test_originSurfaceY() {
let fpc = FloatingPanelController(delegate: nil)
fpc.loadViewIfNeeded()
fpc.view.frame = CGRect(x: 0, y: 0, width: 375, height: 667)
fpc.show(animated: false, completion: nil)
fpc.move(to: .full, animated: false)
XCTAssertEqual(fpc.surfaceView.frame.minY, fpc.originYOfSurface(for: .full))
fpc.move(to: .half, animated: false)
XCTAssertEqual(fpc.surfaceView.frame.minY, fpc.originYOfSurface(for: .half))
fpc.move(to: .tip, animated: false)
XCTAssertEqual(fpc.surfaceView.frame.minY, fpc.originYOfSurface(for: .tip))
fpc.move(to: .hidden, animated: false)
XCTAssertEqual(fpc.surfaceView.frame.minY, fpc.originYOfSurface(for: .hidden))
}
}
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
}
}
}
@@ -1,206 +0,0 @@
//
// Created by Shin Yamamoto on 2019/06/27.
// Copyright © 2019 scenee. All rights reserved.
//
import XCTest
@testable import FloatingPanel
class FloatingPanelLayoutTests: XCTestCase {
var fpc: FloatingPanelController!
override func setUp() {
fpc = FloatingPanelController(delegate: nil)
fpc.loadViewIfNeeded()
fpc.view.frame = CGRect(x: 0, y: 0, width: 375, height: 667)
}
override func tearDown() {}
func test_layoutAdapter_topAndBottomMostState() {
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, .hidden)
delegate.layout = FloatingPanelLayout2Positions()
fpc.delegate = delegate
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.topMostState, .half)
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.bottomMostState, .tip)
}
func test_layoutSegment_3position() {
class FloatingPanelLayout3Positions: FloatingPanelTestLayout {
let initialPosition: FloatingPanelPosition = .half
let supportedPositions: Set<FloatingPanelPosition> = [.tip, .half, .full]
}
let delegate = FloatingPanelTestDelegate()
delegate.layout = FloatingPanelLayout3Positions()
fpc.delegate = delegate
let fullPos = fpc.originYOfSurface(for: .full)
let halfPos = fpc.originYOfSurface(for: .half)
let tipPos = fpc.originYOfSurface(for: .tip)
let minPos = CGFloat.leastNormalMagnitude
let maxPos = CGFloat.greatestFiniteMagnitude
assertLayoutSegment(fpc.floatingPanel, with: [
(#line, pos: minPos, forwardY: true, lower: nil, upper: .full),
(#line, pos: minPos, forwardY: false, lower: nil, upper: .full),
(#line, pos: fullPos, forwardY: true, lower: .full, upper: .half),
(#line, pos: fullPos, forwardY: false, lower: nil, upper: .full),
(#line, pos: halfPos, forwardY: true, lower: .half, upper: .tip),
(#line, pos: halfPos, forwardY: false, lower: .full, upper: .half),
(#line, pos: tipPos, forwardY: true, lower: .tip, upper: nil),
(#line, pos: tipPos, forwardY: false, lower: .half, upper: .tip),
(#line, pos: maxPos, forwardY: true, lower: .tip, upper: nil),
(#line, pos: maxPos, forwardY: false, lower: .tip, upper: nil),
])
}
func test_layoutSegment_2positions() {
class FloatingPanelLayout2Positions: FloatingPanelTestLayout {
let initialPosition: FloatingPanelPosition = .half
let supportedPositions: Set<FloatingPanelPosition> = [.half, .full]
}
let delegate = FloatingPanelTestDelegate()
delegate.layout = FloatingPanelLayout2Positions()
fpc.delegate = delegate
let fullPos = fpc.originYOfSurface(for: .full)
let halfPos = fpc.originYOfSurface(for: .half)
let minPos = CGFloat.leastNormalMagnitude
let maxPos = CGFloat.greatestFiniteMagnitude
assertLayoutSegment(fpc.floatingPanel, with: [
(#line, pos: minPos, forwardY: true, lower: nil, upper: .full),
(#line, pos: minPos, forwardY: false, lower: nil, upper: .full),
(#line, pos: fullPos, forwardY: true, lower: .full, upper: .half),
(#line, pos: fullPos, forwardY: false, lower: nil, upper: .full),
(#line, pos: halfPos, forwardY: true, lower: .half, upper: nil),
(#line, pos: halfPos, forwardY: false, lower: .full, upper: .half),
(#line, pos: maxPos, forwardY: true, lower: .half, upper: nil),
(#line, pos: maxPos, forwardY: false, lower: .half, upper: nil),
])
}
func test_layoutSegment_1positions() {
class FloatingPanelLayout1Positions: FloatingPanelTestLayout {
let initialPosition: FloatingPanelPosition = .full
let supportedPositions: Set<FloatingPanelPosition> = [.full]
}
let delegate = FloatingPanelTestDelegate()
delegate.layout = FloatingPanelLayout1Positions()
fpc.delegate = delegate
let fullPos = fpc.originYOfSurface(for: .full)
let minPos = CGFloat.leastNormalMagnitude
let maxPos = CGFloat.greatestFiniteMagnitude
assertLayoutSegment(fpc.floatingPanel, with: [
(#line, pos: minPos, forwardY: true, lower: nil, upper: .full),
(#line, pos: minPos, forwardY: false, lower: nil, upper: .full),
(#line, pos: fullPos, forwardY: true, lower: .full, upper: nil),
(#line, pos: fullPos, forwardY: false, lower: nil, upper: .full),
(#line, pos: maxPos, forwardY: true, lower: .full, upper: nil),
(#line, pos: maxPos, forwardY: false, lower: .full, upper: nil),
])
}
func test_updateInteractiveTopConstraint() {
fpc.showForTest()
fpc.move(to: .full, animated: false)
fpc.floatingPanel.layoutAdapter.startInteraction(at: fpc.position)
fpc.floatingPanel.layoutAdapter.startInteraction(at: fpc.position) // Should be ignore
let fullPos = fpc.originYOfSurface(for: .full)
let tipPos = fpc.originYOfSurface(for: .tip)
var pre: CGFloat
var next: CGFloat
pre = fpc.surfaceView.frame.minY
fpc.floatingPanel.layoutAdapter.updateInteractiveTopConstraint(diff: -100.0, allowsTopBuffer: false, with: fpc.behavior)
next = fpc.surfaceView.frame.minY
XCTAssertEqual(next, pre)
fpc.floatingPanel.layoutAdapter.updateInteractiveTopConstraint(diff: -100.0, allowsTopBuffer: true, with: fpc.behavior)
next = fpc.surfaceView.frame.minY
XCTAssertEqual(next, fullPos - fpc.layout.topInteractionBuffer)
fpc.floatingPanel.layoutAdapter.updateInteractiveTopConstraint(diff: 100.0, allowsTopBuffer: true, with: fpc.behavior)
next = fpc.surfaceView.frame.minY
XCTAssertEqual(next, fullPos + 100.0)
fpc.floatingPanel.layoutAdapter.updateInteractiveTopConstraint(diff: tipPos - fullPos, allowsTopBuffer: true, with: fpc.behavior)
next = fpc.surfaceView.frame.minY
XCTAssertEqual(next, tipPos)
fpc.floatingPanel.layoutAdapter.updateInteractiveTopConstraint(diff: tipPos - fullPos + 100.0, allowsTopBuffer: true, with: fpc.behavior)
next = fpc.surfaceView.frame.minY
XCTAssertEqual(next, tipPos + fpc.layout.bottomInteractionBuffer)
fpc.floatingPanel.layoutAdapter.endInteraction(at: fpc.position)
}
func test_updateInteractiveTopConstraintWithHidden() {
class FloatingPanelLayout2Positions: FloatingPanelTestLayout {
let initialPosition: FloatingPanelPosition = .hidden
let supportedPositions: Set<FloatingPanelPosition> = [.hidden, .full]
}
let delegate = FloatingPanelTestDelegate()
delegate.layout = FloatingPanelLayout2Positions()
fpc.delegate = delegate
fpc.showForTest()
fpc.move(to: .full, animated: false)
fpc.floatingPanel.layoutAdapter.startInteraction(at: fpc.position)
let fullPos = fpc.originYOfSurface(for: .full)
let hiddenPos = fpc.originYOfSurface(for: .hidden)
var pre: CGFloat
var next: CGFloat
pre = fpc.surfaceView.frame.minY
fpc.floatingPanel.layoutAdapter.updateInteractiveTopConstraint(diff: -100.0, allowsTopBuffer: false, with: fpc.behavior)
next = fpc.surfaceView.frame.minY
XCTAssertEqual(next, pre)
fpc.floatingPanel.layoutAdapter.updateInteractiveTopConstraint(diff: -100.0, allowsTopBuffer: true, with: fpc.behavior)
next = fpc.surfaceView.frame.minY
XCTAssertEqual(next, fullPos - fpc.layout.topInteractionBuffer)
fpc.floatingPanel.layoutAdapter.updateInteractiveTopConstraint(diff: hiddenPos - fullPos + 100.0, allowsTopBuffer: true, with: fpc.behavior)
next = fpc.surfaceView.frame.minY
XCTAssertEqual(next, hiddenPos + fpc.layout.bottomInteractionBuffer)
fpc.floatingPanel.layoutAdapter.endInteraction(at: fpc.position)
}
}
private typealias LayoutSegmentTestParameter = (UInt, pos: CGFloat, forwardY: Bool, lower: FloatingPanelPosition?, upper: FloatingPanelPosition?)
private func assertLayoutSegment(_ floatingPanel: FloatingPanel, with params: [LayoutSegmentTestParameter]) {
params.forEach { (line, pos, forwardY, lowr, upper) in
let segument = floatingPanel.layoutAdapter.segument(at: pos, forward: forwardY)
XCTAssertEqual(segument.lower, lowr, line: line)
XCTAssertEqual(segument.upper, upper, line: line)
}
}
@@ -1,27 +0,0 @@
//
// Created by Shin Yamamoto on 2019/07/05.
// Copyright © 2019 scenee. All rights reserved.
//
import XCTest
@testable import FloatingPanel
class FloatingPanelPositionTests: XCTestCase {
override func setUp() { }
override func tearDown() { }
func test_nextAndPre() {
var positions: [FloatingPanelPosition]
positions = [.full, .half, .tip, .hidden]
XCTAssertEqual(FloatingPanelPosition.full.next(in: positions), .half)
XCTAssertEqual(FloatingPanelPosition.full.pre(in: positions), .full)
XCTAssertEqual(FloatingPanelPosition.hidden.next(in: positions), .hidden)
XCTAssertEqual(FloatingPanelPosition.hidden.pre(in: positions), .tip)
positions = [.full, .hidden]
XCTAssertEqual(FloatingPanelPosition.full.next(in: positions), .hidden)
XCTAssertEqual(FloatingPanelPosition.full.pre(in: positions), .full)
XCTAssertEqual(FloatingPanelPosition.hidden.next(in: positions), .hidden)
XCTAssertEqual(FloatingPanelPosition.hidden.pre(in: positions), .full)
}
}
@@ -1,82 +0,0 @@
//
// 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))
XCTAssertTrue(FloatingPanelSurfaceView.requiresConstraintBasedLayout)
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 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.grabberHandleWidth = 44.0
surface.grabberHandleHeight = 12.0
surface.setNeedsLayout()
surface.layoutIfNeeded()
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)")
}
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)
}
}
-46
View File
@@ -1,46 +0,0 @@
//
// 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))
}
extension FloatingPanelController {
func showForTest() {
loadViewIfNeeded()
view.frame = CGRect(x: 0, y: 0, width: 375, height: 667)
show(animated: false, completion: nil)
}
}
class FloatingPanelTestDelegate: FloatingPanelControllerDelegate {
var layout: FloatingPanelLayout?
var behavior: FloatingPanelBehavior?
var position: FloatingPanelPosition = .hidden
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? {
return layout
}
func floatingPanel(_ vc: FloatingPanelController, behaviorFor newCollection: UITraitCollection) -> FloatingPanelBehavior? {
return behavior
}
func floatingPanelDidChangePosition(_ vc: FloatingPanelController) {
position = vc.position
}
}
protocol FloatingPanelTestLayout: FloatingPanelFullScreenLayout {}
extension FloatingPanelTestLayout {
func insetFor(position: FloatingPanelPosition) -> CGFloat? {
switch position {
case .full: return 20.0
case .half: return 250.0
case .tip: return 60.0
default: return nil
}
}
}
+2 -2
View File
@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2018 Shin Yamamoto
Copyright (c) 2018-Present Shin Yamamoto
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
SOFTWARE.
+1 -1
View File
@@ -21,7 +21,7 @@ let package = Package(
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"),
.target(name: "FloatingPanel", path: "Sources"),
],
swiftLanguageVersions: [.version("5")]
)
+348 -139
View File
@@ -2,13 +2,9 @@
[![Version](https://img.shields.io/cocoapods/v/FloatingPanel.svg)](https://cocoapods.org/pods/FloatingPanel)
[![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage)
[![Platform](https://img.shields.io/cocoapods/p/FloatingPanel.svg)](https://cocoapods.org/pods/FloatingPanel)
[![Swift 4.1](https://img.shields.io/badge/Swift-4.1-orange.svg?style=flat)](https://swift.org/)
[![Swift 4.2](https://img.shields.io/badge/Swift-4.2-orange.svg?style=flat)](https://swift.org/)
[![Swift 5.0](https://img.shields.io/badge/Swift-5.0-orange.svg?style=flat)](https://swift.org/)
[![Swift 5.1](https://img.shields.io/badge/Swift-5.1-orange.svg?style=flat)](https://swift.org/)
# FloatingPanel
[![Swift 5](https://img.shields.io/badge/Swift-5-orange.svg?style=flat)](https://swift.org/)
# FloatingPanel
FloatingPanel is a simple and easy-to-use UI component for a new interface introduced in Apple Maps, Shortcuts and Stocks app.
The new interface displays the related contents and utilities in parallel as a user wants.
@@ -23,30 +19,45 @@ The new interface displays the related contents and utilities in parallel as a u
- [Features](#features)
- [Requirements](#requirements)
- [Installation](#installation)
- [CocoaPods](#cocoapods)
- [Carthage](#carthage)
- [CocoaPods](#cocoapods)
- [Carthage](#carthage)
- [Swift Package Manager](#swift-package-manager)
- [Getting Started](#getting-started)
- [Add a floating panel as a child view controller](#add-a-floating-panel-as-a-child-view-controller)
- [Present a floating panel as a modality](#present-a-floating-panel-as-a-modality)
- [Add a floating panel as a child view controller](#add-a-floating-panel-as-a-child-view-controller)
- [Present a floating panel as a modality](#present-a-floating-panel-as-a-modality)
- [View hierarchy](#view-hierarchy)
- [Usage](#usage)
- [Show/Hide a floating panel in a view with your view hierarchy](#showhide-a-floating-panel-in-a-view-with-your-view-hierarchy)
- [Customize the layout with `FloatingPanelLayout` protocol](#customize-the-layout-with-floatingpanellayout-protocol)
- [Change the initial position and height](#change-the-initial-position-and-height)
- [Support your landscape layout](#support-your-landscape-layout)
- [Use Intrinsic height layout](#use-intrinsic-height-layout)
- [Customize the behavior with `FloatingPanelBehavior` protocol](#customize-the-behavior-with-floatingpanelbehavior-protocol)
- [Modify your floating panel's interaction](#modify-your-floating-panels-interaction)
- [Use a custom grabber handle](#use-a-custom-grabber-handle)
- [Add tap gestures to the surface or backdrop views](#add-tap-gestures-to-the-surface-or-backdrop-views)
- [Create an additional floating panel for a detail](#create-an-additional-floating-panel-for-a-detail)
- [Move a position with an animation](#move-a-position-with-an-animation)
- [Work your contents together with a floating panel behavior](#work-your-contents-together-with-a-floating-panel-behavior)
- [Show/Hide a floating panel in a view with your view hierarchy](#showhide-a-floating-panel-in-a-view-with-your-view-hierarchy)
- [Scale the content view when the surface position changes](#scale-the-content-view-when-the-surface-position-changes)
- [Customize the layout with `FloatingPanelLayout` protocol](#customize-the-layout-with-floatingpanellayout-protocol)
- [Change the initial layout](#change-the-initial-layout)
- [Update your panel layout](#update-your-panel-layout)
- [Support your landscape layout](#support-your-landscape-layout)
- [Use the intrinsic size of a content in your panel layout](#use-the-intrinsic-size-of-a-content-in-your-panel-layout)
- [Specify an anchor for each state by an inset of the `FloatingPanelController.view` frame](#specify-an-anchor-for-each-state-by-an-inset-of-the-floatingpanelcontrollerview-frame)
- [Customize the behavior with `FloatingPanelBehavior` protocol](#customize-the-behavior-with-floatingpanelbehavior-protocol)
- [Modify your floating panel's interaction](#modify-your-floating-panels-interaction)
- [Activate the rubber-band effect on panel edges](#activate-the-rubber-band-effect-on-panel-edges)
- [Manage the projection of a pan gesture momentum](#manage-the-projection-of-a-pan-gesture-momentum)
- [Specify the panel move's boundary](#specify-the-panel-moves-boundary)
- [Customize the surface design](#customize-the-surface-design)
- [Modify your surface appearance](#modify-your-surface-appearance)
- [Use a custom grabber handle](#use-a-custom-grabber-handle)
- [Customize layout of the grabber handle](#customize-layout-of-the-grabber-handle)
- [Customize content padding from surface edges](#customize-content-padding-from-surface-edges)
- [Customize margins of the surface edges](#customize-margins-of-the-surface-edges)
- [Customize gestures](#customize-gestures)
- [Suppress the panel interaction](#suppress-the-panel-interaction)
- [Add tap gestures to the surface view](#add-tap-gestures-to-the-surface-view)
- [Interrupt the delegate methods of `FloatingPanelController.panGestureRecognizer`](#interrupt-the-delegate-methods-of-floatingpanelcontrollerpangesturerecognizer)
- [Create an additional floating panel for a detail](#create-an-additional-floating-panel-for-a-detail)
- [Move a position with an animation](#move-a-position-with-an-animation)
- [Work your contents together with a floating panel behavior](#work-your-contents-together-with-a-floating-panel-behavior)
- [Notes](#notes)
- ['Show' or 'Show Detail' Segues from `FloatingPanelController`'s content view controller](#show-or-show-detail-segues-from-floatingpanelcontrollers-content-view-controller)
- [UISearchController issue](#uisearchcontroller-issue)
- [FloatingPanelSurfaceView's issue on iOS 10](#floatingpanelsurfaceviews-issue-on-ios-10)
- [Author](#author)
- ['Show' or 'Show Detail' Segues from `FloatingPanelController`'s content view controller](#show-or-show-detail-segues-from-floatingpanelcontrollers-content-view-controller)
- [UISearchController issue](#uisearchcontroller-issue)
- [FloatingPanelSurfaceView's issue on iOS 10](#floatingpanelsurfaceviews-issue-on-ios-10)
- [Maintainer](#maintainer)
- [License](#license)
<!-- /TOC -->
@@ -54,25 +65,32 @@ The new interface displays the related contents and utilities in parallel as a u
## Features
- [x] Simple container view controller
- [x] Fluid animation and gesture handling
- [x] Fluid behavior using numeric springing
- [x] Scroll view tracking
- [x] Common UI elements: Grabber handle, Backdrop and Surface rounding corners
- [x] 1~3 anchor positions(full, half, tip)
- [x] Layout customization for all trait environments(i.e. Landscape orientation support)
- [x] Behavior customization
- [x] Free from common issues of Auto Layout and gesture handling
- [x] Removal interaction
- [x] Multi panel support
- [x] Modal presentation
- [x] 4 positioning support(top, left, bottom, right)
- [x] 1~3 magnetic anchors(full, half, tip)
- [x] Layout support for all trait environments(i.e. Landscape orientation)
- [x] Common UI elements: surface, backdrop and grabber handle
- [x] Free from common issues of Auto Layout and gesture handling
- [x] Compatible with Objective-C
Examples are here.
- [Examples/Maps](https://github.com/SCENEE/FloatingPanel/tree/master/Examples/Maps) like Apple Maps.app.
- [Examples/Stocks](https://github.com/SCENEE/FloatingPanel/tree/master/Examples/Stocks) like Apple Stocks.app.
- [Examples/Samples](https://github.com/SCENEE/FloatingPanel/tree/master/Examples/Samples)
- [Examples/SamplesObjC](https://github.com/SCENEE/FloatingPanel/tree/master/Examples/SamplesObjC)
## Requirements
FloatingPanel is written in Swift 4.0+. It can be built by Xcode 9.4.1 or later. Compatible with iOS 10.0+.
FloatingPanel is written in Swift 5.0+. Compatible with iOS 11.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.
The deployment is still iOS 10, but it is recommended to use this library on iOS 11+.
:pencil2: You would like to use Swift 4.0. Please use FloatingPanel v1.
## Installation
@@ -85,7 +103,7 @@ 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.
:pencil2: FloatingPanel v1.7.0 or later requires CocoaPods v1.7.0+ for `swift_versions` support.
### Carthage
@@ -95,7 +113,7 @@ For [Carthage](https://github.com/Carthage/Carthage), add the following to your
github "scenee/FloatingPanel"
```
### Swift Package Manager with Xcode 11
### Swift Package Manager
Follow [this doc](https://developer.apple.com/documentation/swift_packages/adding_package_dependencies_to_your_app).
@@ -128,13 +146,6 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate {
// Add and show the views managed by the `FloatingPanelController` object to self.view.
fpc.addPanel(toParent: self)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
// Remove the views managed by the `FloatingPanelController` object from self.view.
fpc.removePanelFromParent()
}
}
```
@@ -152,7 +163,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.
✏️ 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).
:pencil2: 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
@@ -164,74 +175,129 @@ FloatingPanelController.view (FloatingPanelPassThroughView)
└─ .surfaceView (FloatingPanelSurfaceView)
├─ .containerView (UIView)
│ └─ .contentView (FloatingPanelController.contentViewController.view)
└─ .grabberHandle (GrabberHandleView)
└─ .grabber (FloatingPanelGrabberView)
```
## Usage
### Show/Hide a floating panel in a view with your view hierarchy
If you need more control over showing and hiding the floating panel, you can forgo the `addPanel` and `removePanelFromParent` methods. These methods are a convenience wrapper for **FloatingPanel**'s `show` and `hide` methods along with some required setup.
There are two ways to work with the `FloatingPanelController`:
1. Add it to the hierarchy once and then call `show` and `hide` methods to make it appear/disappear.
2. Add it to the hierarchy when needed and remove afterwards.
The following example shows how to add the controller to your `UIViewController` and how to remove it. Make sure that you never add the same `FloatingPanelController` to the hierarchy before removing it.
**NOTE**: `self.` prefix is not required, nor recommended. It's used here to make it clearer where do the functions used come from. `self` is an instance of a custom UIViewController in your code.
```swift
// Add the controller and the managed views to a view controller.
// From the second time, just call `show(animated:completion)`.
view.addSubview(fpc.view)
// Add the floating panel view to the controller's view on top of other views.
self.view.addSubview(fpc.view)
// REQUIRED. It makes the floating panel view have the same size as the controller's view.
fpc.view.frame = self.view.bounds
fpc.view.frame = view.bounds // MUST
// In addition, Auto Layout constraints are highly recommended.
// Because it makes the layout more robust on trait collection change.
//
// fpc.view.translatesAutoresizingMaskIntoConstraints = false
// NSLayoutConstraint.activate([...])
//
// Constraint the fpc.view to all four edges of your controller's view.
// It makes the layout more robust on trait collection change.
fpc.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
fpc.view.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 0.0),
fpc.view.leftAnchor.constraint(equalTo: self.view.leftAnchor, constant: 0.0),
fpc.view.rightAnchor.constraint(equalTo: self.view.rightAnchor, constant: 0.0),
fpc.view.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: 0.0),
])
parent.addChild(fpc)
// Add the floating panel controller to the controller hierarchy.
self.addChild(fpc)
// Show a floating panel to the initial position defined in your `FloatingPanelLayout` object.
// Show the floating panel at the initial position defined in your `FloatingPanelLayout` object.
fpc.show(animated: true) {
// Only for the first time
self.didMove(toParent: self)
}
...
// Hide it
fpc.hide(animated: true) {
// Remove it if needed
self.willMove(toParent: nil)
self.view.removeFromSuperview()
self.removeFromParent()
// Inform the floating panel controller that the transition to the controller hierarchy has completed.
fpc.didMove(toParent: self)
}
```
NOTE: `FloatingPanelController` wraps `show`/`hide` with `addPanel`/`removePanelFromParent` for easy-to-use. But `show`/`hide` are more convenience for your app.
After you add the `FloatingPanelController` as seen above, you can call `fpc.show(animated: true) { }` to show the panel and `fpc.hide(animated: true) { }` to hide it.
To remove the `FloatingPanelController` from the hierarchy, follow the example below.
```swift
// Inform the panel controller that it will be removed from the hierarchy.
fpc.willMove(toParent: nil)
// Hide the floating panel.
fpc.hide(animated: true) {
// Remove the floating panel view from your controller's view.
fpc.view.removeFromSuperview()
// Remove the floating panel controller from the controller hierarchy.
fpc.removeFromParent()
}
```
### Scale the content view when the surface position changes
Specify the `contentMode` to `.fitToBounds` if the surface height fits the bounds of `FloatingPanelController.view` when the surface position changes
```swift
fpc.contentMode = .fitToBounds
```
Otherwise, `FloatingPanelController` fixes the content by the height of the top most position.
:pencil2: In `.fitToBounds` mode, the surface height changes as following a user interaction so that you have a responsibility to configure Auto Layout constrains not to break the layout of a content view by the elastic surface height.
### Customize the layout with `FloatingPanelLayout` protocol
#### Change the initial position and height
#### Change the initial layout
```swift
class ViewController: UIViewController, FloatingPanelControllerDelegate {
...
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? {
return MyFloatingPanelLayout()
... {
fpc = FloatingPanelController(delegate: self)
fpc.layout = MyFloatingPanelLayout()
}
}
class MyFloatingPanelLayout: FloatingPanelLayout {
public var initialPosition: FloatingPanelPosition {
return .tip
let position: FloatingPanelPosition = .bottom
let initialState: FloatingPanelState = .tip
var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] {
return [
.full: FloatingPanelLayoutAnchor(absoluteInset: 16.0, edge: .top, referenceGuide: .safeArea),
.half: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .safeArea),
.tip: FloatingPanelLayoutAnchor(absoluteInset: 44.0, edge: .bottom, referenceGuide: .safeArea),
]
}
}
```
### Update your panel layout
There are 2 ways to update the panel layout.
1. Manually set `FloatingPanelController.layout` to the new layout object directly.
```swift
fpc.layout = MyPanelLayout()
fpc.invalidateLayout() // If needed
```
2. Returns an appropriate layout object in one of 2 `floatingPanel(_:layoutFor:)` delegates.
```swift
class ViewController: UIViewController, FloatingPanelControllerDelegate {
...
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout {
return MyFloatingPanelLayout()
}
public func insetFor(position: FloatingPanelPosition) -> CGFloat? {
switch position {
case .full: return 16.0 // A top inset from safe area
case .half: return 216.0 // A bottom inset from the safe area
case .tip: return 44.0 // A bottom inset from the safe area
default: return nil // Or `case .hidden: return nil`
}
}
// OR
func floatingPanel(_ vc: FloatingPanelController, layoutFor size: CGSize) -> FloatingPanelLayout {
return MyFloatingPanelLayout()
}
}
```
@@ -240,28 +306,21 @@ class MyFloatingPanelLayout: FloatingPanelLayout {
```swift
class ViewController: UIViewController, FloatingPanelControllerDelegate {
...
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? {
return (newCollection.verticalSizeClass == .compact) ? FloatingPanelLandscapeLayout() : nil // Returning nil indicates to use the default layout
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout {
return (newCollection.verticalSizeClass == .compact) ? LandscapePanelLayout() : FloatingPanelBottomLayout()
}
}
class FloatingPanelLandscapeLayout: FloatingPanelLayout {
public var initialPosition: FloatingPanelPosition {
return .tip
class LandscapePanelLayout: FloatingPanelLayout {
let position: FloatingPanelPosition = .bottom
let initialState: FloatingPanelState = .tip
var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] {
return [
.full: FloatingPanelLayoutAnchor(absoluteInset: 16.0, edge: .top, referenceGuide: .safeArea),
.tip: FloatingPanelLayoutAnchor(absoluteInset: 69.0, edge: .bottom, referenceGuide: .safeArea),
]
}
public var supportedPositions: Set<FloatingPanelPosition> {
return [.full, .tip]
}
public func insetFor(position: FloatingPanelPosition) -> CGFloat? {
switch position {
case .full: return 16.0
case .tip: return 69.0
default: return nil
}
}
public func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint] {
func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint] {
return [
surfaceView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 8.0),
surfaceView.widthAnchor.constraint(equalToConstant: 291),
@@ -270,33 +329,47 @@ class FloatingPanelLandscapeLayout: FloatingPanelLayout {
}
```
#### Use Intrinsic height layout
#### Use the intrinsic size of a content in your panel layout
1. Lay out your content View with the intrinsic height size. For example, see "Detail View Controller scene"/"Intrinsic View Controller scene" of [Main.storyboard](https://github.com/SCENEE/FloatingPanel/blob/master/Examples/Samples/Sources/Base.lproj/Main.storyboard). The 'Stack View.bottom' constraint determines the intrinsic height.
2. Create a layout that adopts and conforms to `FloatingPanelIntrinsicLayout` and use it.
2. Specify layout anchors using `FloatingPanelIntrinsicLayoutAnchor`.
```swift
class ViewController: UIViewController, FloatingPanelControllerDelegate {
...
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? {
return RemovablePanelLayout()
}
}
class RemovablePanelLayout: FloatingPanelIntrinsicLayout {
var supportedPositions: Set<FloatingPanelPosition> {
return [.full, .half]
}
func insetFor(position: FloatingPanelPosition) -> CGFloat? {
switch position {
case .half: return 130.0
default: return nil // Must return nil for .full
}
class IntrinsicPanelLayout: FloatingPanelLayout {
let position: FloatingPanelPosition = .bottom
let initialState: FloatingPanelState = .full
var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] {
return [
.full: FloatingPanelIntrinsicLayoutAnchor(absoluteOffset: 0, referenceGuide: .safeArea),
.half: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 0.5, referenceGuide: .safeArea),
]
}
...
}
```
:pencil2: `FloatingPanelIntrinsicLayout` is deprecated on v1.
#### Specify an anchor for each state by an inset of the `FloatingPanelController.view` frame
Use `.superview` reference guide in your anchors.
```swift
class MyFullScreenLayout: FloatingPanelLayout {
...
var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] {
return [
.full: FloatingPanelLayoutAnchor(absoluteInset: 16.0, edge: .top, referenceGuide: .superview),
.half: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .superview),
.tip: FloatingPanelLayoutAnchor(absoluteInset: 44.0, edge: .bottom, referenceGuide: .superview),
]
}
}
```
:pencil2: `FloatingPanelFullScreenLayout` is deprecated on v1.
### Customize the behavior with `FloatingPanelBehavior` protocol
#### Modify your floating panel's interaction
@@ -304,22 +377,90 @@ class RemovablePanelLayout: FloatingPanelIntrinsicLayout {
```swift
class ViewController: UIViewController, FloatingPanelControllerDelegate {
...
func floatingPanel(_ vc: FloatingPanelController, behaviorFor newCollection: UITraitCollection) -> FloatingPanelBehavior? {
return FloatingPanelStocksBehavior()
func viewDidLoad() {
...
fpc.behavior = CustomPanelBehavior()
}
}
class FloatingPanelStocksBehavior: FloatingPanelBehavior {
...
func interactionAnimator(_ fpc: FloatingPanelController, to targetPosition: FloatingPanelPosition, with velocity: CGVector) -> UIViewPropertyAnimator {
let damping = self.damping(with: velocity)
let springTiming = UISpringTimingParameters(dampingRatio: damping, initialVelocity: velocity)
return UIViewPropertyAnimator(duration: 0.5, timingParameters: springTiming)
class CustomPanelBehavior: FloatingPanelBehavior {
let springDecelerationRate = UIScrollView.DecelerationRate.fast.rawValue + 0.02
let springResponseTime = 0.4
func shouldProjectMomentum(_ fpc: FloatingPanelController, to proposedTargetPosition: FloatingPanelState) -> Bool {
return true
}
}
```
### Use a custom grabber handle
:pencil2: `floatingPanel(_ vc:behaviorFor:)` is deprecated on v1.
#### Activate the rubber-band effect on panel edges
```swift
class MyPanelBehavior: FloatingPanelBehavior {
...
func allowsRubberBanding(for edge: UIRectEdge) -> Bool {
return true
}
}
```
#### Manage the projection of a pan gesture momentum
This allows full projectional panel behavior. For example, a user can swipe up a panel from tip to full nearby the tip position.
```swift
class MyPanelBehavior: FloatingPanelBehavior {
...
func shouldProjectMomentum(_ fpc: FloatingPanelController, to proposedTargetPosition: FloatingPanelPosition) -> Bool {
return true
}
}
```
### Specify the panel move's boundary
`FloatingPanelController.surfaceLocation` in `floatingPanelDidMove(_:)` delegate method behaves like `UIScrollView.contentOffset` in `scrollViewDidScroll(_:)`.
As a result, you can specify the boundary of a panel move as below.
```swift
func floatingPanelDidMove(_ vc: FloatingPanelController) {
if vc.isAttracting == false {
let loc = vc.surfaceLocation
let minY = vc.surfaceLocation(for: .full).y - 6.0
let maxY = vc.surfaceLocation(for: .tip).y + 6.0
vc.surfaceLocation = CGPoint(x: loc.x, y: min(max(loc.y, minY), maxY))
}
}
```
:pencil2: `{top,bottom}InteractionBuffer` property is removed from `FloatingPanelLayout` since v2.
### Customize the surface design
#### Modify your surface appearance
```swift
// Create a new appearance.
let appearance = SurfaceAppearance()
// Define shadows
let shadow = SurfaceAppearance.Shadow()
shadow.color = UIColor.black
shadow.offset = CGSize(width: 0, height: 16)
shadow.radius = 16
shadow.spread = 8
appearance.shadows = [shadow]
// Define corner radius and background color
appearance.cornerRadius = 8.0
appearance.backgroundColor = .clear
// Set the new appearance
fpc.surfaceView.appearance = appearance
````
#### Use a custom grabber handle
```swift
let myGrabberHandleView = MyGrabberHandleView()
@@ -327,17 +468,57 @@ fpc.surfaceView.grabberHandle.isHidden = true
fpc.surfaceView.addSubview(myGrabberHandleView)
```
### Add tap gestures to the surface or backdrop views
#### Customize layout of the grabber handle
```swift
fpc.surfaceView.grabberHandlePadding = 10.0
fpc.surfaceView.grabberHandleSize = .init(width: 44.0, height: 12.0)
```
:pencil2: Note that `grabberHandleSize` width and height are reversed in the left/right position.
#### Customize content padding from surface edges
```swift
fpc.surfaceView.contentPadding = .init(top: 20, left: 20, bottom: 20, right: 20)
```
#### Customize margins of the surface edges
```swift
fpc.surfaceView.containerMargins = .init(top: 20.0, left: 16.0, bottom: 16.0, right: 16.0)
```
The feature can be used for these 2 kind panels
* Facebook/Slack-like panel whose surface top edge is separated from the grabber handle.
* iOS native panel to display AirPods information, for example.
### Customize gestures
#### Suppress the panel interaction
You can disable the pan gesture recognizer directly
```swift
fpc.panGestureRecognizer.isEnabled = false
```
Or use this `FloatingPanelControllerDelegate` method.
```swift
func floatingPanelShouldBeginDragging(_ vc: FloatingPanelController) -> Bool {
return aCondition ? false : true
}
```
#### Add tap gestures to the surface view
```swift
override func viewDidLoad() {
...
surfaceTapGesture = UITapGestureRecognizer(target: self, action: #selector(handleSurface(tapGesture:)))
let surfaceTapGesture = UITapGestureRecognizer(target: self, action: #selector(handleSurface(tapGesture:)))
fpc.surfaceView.addGestureRecognizer(surfaceTapGesture)
backdropTapGesture = UITapGestureRecognizer(target: self, action: #selector(handleBackdrop(tapGesture:)))
fpc.backdropView.addGestureRecognizer(backdropTapGesture)
surfaceTapGesture.isEnabled = (fpc.position == .tip)
}
@@ -347,6 +528,26 @@ func floatingPanelDidChangePosition(_ vc: FloatingPanelController) {
}
```
#### Interrupt the delegate methods of `FloatingPanelController.panGestureRecognizer`
If you are set `FloatingPanelController.panGestureRecognizer.delegateProxy` to an object adopting `UIGestureRecognizerDelegate`, it overrides delegate methods of the pan gesture recognizer.
```swift
class MyGestureRecognizerDelegate: UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return false
}
}
class ViewController: UIViewController {
let myGestureDelegate = MyGestureRecognizerDelegate()
func setUpFpc() {
....
fpc.panGestureRecognizer.delegateProxy = myGestureDelegate
}
```
### Create an additional floating panel for a detail
```swift
@@ -387,6 +588,14 @@ func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
}
```
You can also use a view animation to move a panel.
```swift
UIView.animate(withDuration: 0.25) {
self.fpc.move(to: .half, animated: false)
}
```
### Work your contents together with a floating panel behavior
```swift
@@ -399,8 +608,8 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate {
}
}
func floatingPanelDidEndDragging(_ vc: FloatingPanelController, withVelocity velocity: CGPoint, targetPosition: FloatingPanelPosition) {
if targetPosition != .full {
func floatingPanelWillEndDragging(_ vc: FloatingPanelController, withVelocity velocity: CGPoint, targetState: UnsafeMutablePointer<FloatingPanelState>) {
if targetState.pointee != .full {
searchVC.hideHeader()
}
}
@@ -462,7 +671,7 @@ override func viewDidLayoutSubviews() {
```
* If you sets clear color to `FloatingPanelSurfaceView.backgroundColor`, please note the bottom overflow of your content on bouncing at full position. To prevent it, you need to expand your content. For example, See Example/Maps App's Auto Layout settings of `UIVisualEffectView` in Main.storyboard.
## Author
## Maintainer
Shin Yamamoto <shin@scenee.com> | [@scenee](https://twitter.com/scenee)
+11
View File
@@ -0,0 +1,11 @@
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
import UIKit
/// A view that presents a backdrop interface behind a panel.
@objc(FloatingPanelBackdropView)
public class BackdropView: UIView {
/// The gesture recognizer for tap gestures to dismiss a panel.
public var dismissalTapGestureRecognizer: UITapGestureRecognizer!
}
+125
View File
@@ -0,0 +1,125 @@
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
import UIKit
/// An interface for generating behavior information to fine-tune the behavior of a panel.
@objc
public protocol FloatingPanelBehavior {
/// A floating-point value that determines the rate of oscillation magnitude reduction after the user lifts their finger.
///
/// The oscillation magnitude to attract a panel to an anchor can be adjusted this value between 0.979 and 1.0
/// in increments of 0.001. When this value is around 0.979, the attraction uses a critically damped spring system.
/// When this value is between 0.978 and 1.0, it uses a underdamped spring system with a damping ratio computed by
/// this value. You shouldn't return less than 0.979 because the system is overdamped. If the pan gesture's velocity
/// is less than 300, this value is ignored and a panel applies a critically damped system.
@objc optional
var springDecelerationRate: CGFloat { get }
/// A floating-point value that determines the approximate time until a panel stops to an anchor after the user lifts their finger.
@objc optional
var springResponseTime: CGFloat { get }
/// Returns a deceleration rate to calculate a target position projected a dragging momentum.
///
/// The default implementation of this method returns the normal deceleration rate of UIScrollView.
@objc optional
var momentumProjectionRate: CGFloat { get }
/// Asks the behavior if a panel should project a momentum of a user interaction to move the proposed position.
///
/// The default implementation of this method returns true. This method is called for a layout to support all positions(tip, half and full).
/// Therefore, `proposedTargetPosition` can only be `FloatingPanelState.tip` or `FloatingPanelState.full`.
@objc optional
func shouldProjectMomentum(_ fpc: FloatingPanelController, to proposedTargetPosition: FloatingPanelState) -> Bool
/// 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 a 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.
@objc optional
func redirectionalProgress(_ fpc: FloatingPanelController, from: FloatingPanelState, to: FloatingPanelState) -> CGFloat
/// Asks the behavior whether the rubber band effect is enabled in moving over a given edge of the surface view.
///
/// This method allows a panel to activate the rubber band effect to a given edge of the surface view. By default, the effect is disabled.
@objc optional
func allowsRubberBanding(for edge: UIRectEdge) -> Bool
}
/// The default behavior object for a panel
///
/// This behavior object is fine-tuned to behave as a search panel(card) in Apple Maps on iPhone portrait orientation.
public class FloatingPanelDefaultBehavior: FloatingPanelBehavior {
public var springDecelerationRate: CGFloat {
return UIScrollView.DecelerationRate.fast.rawValue + 0.001
}
public var springResponseTime: CGFloat {
return 0.4
}
public var momentumProjectionRate: CGFloat {
return UIScrollView.DecelerationRate.normal.rawValue
}
public func redirectionalProgress(_ fpc: FloatingPanelController, from: FloatingPanelState, to: FloatingPanelState) -> CGFloat {
return 0.5
}
func addPanelAnimator(_ fpc: FloatingPanelController, to: FloatingPanelState) -> UIViewPropertyAnimator {
return UIViewPropertyAnimator(duration: 0.0,
timingParameters: UISpringTimingParameters(decelerationRate: UIScrollView.DecelerationRate.fast.rawValue,
frequencyResponse: 0.25))
}
func removePanelAnimator(_ fpc: FloatingPanelController, from: FloatingPanelState, with velocity: CGVector) -> UIViewPropertyAnimator {
return UIViewPropertyAnimator(duration: 0.0,
timingParameters: UISpringTimingParameters(decelerationRate: UIScrollView.DecelerationRate.fast.rawValue,
frequencyResponse: 0.25,
initialVelocity: velocity))
}
public func allowsRubberBanding(for edge: UIRectEdge) -> Bool {
return false
}
}
class BehaviorAdapter {
unowned let vc: FloatingPanelController
fileprivate var behavior: FloatingPanelBehavior
init(vc: FloatingPanelController, behavior: FloatingPanelBehavior) {
self.vc = vc
self.behavior = behavior
}
var springDecelerationRate: CGFloat {
behavior.springDecelerationRate ?? FloatingPanelDefaultBehavior().springDecelerationRate
}
var springResponseTime: CGFloat {
behavior.springResponseTime ?? FloatingPanelDefaultBehavior().springResponseTime
}
var momentumProjectionRate: CGFloat {
behavior.momentumProjectionRate ?? FloatingPanelDefaultBehavior().momentumProjectionRate
}
func redirectionalProgress(from: FloatingPanelState, to: FloatingPanelState) -> CGFloat {
behavior.redirectionalProgress?(vc, from: from, to: to) ?? FloatingPanelDefaultBehavior().redirectionalProgress(vc,from: from, to: to)
}
func shouldProjectMomentum(to: FloatingPanelState) -> Bool {
behavior.shouldProjectMomentum?(vc, to: to) ?? false
}
func allowsRubberBanding(for edge: UIRectEdge) -> Bool {
behavior.allowsRubberBanding?(for: edge) ?? false
}
}
extension FloatingPanelController {
var _behavior: FloatingPanelBehavior {
get { floatingPanel.behaviorAdapter.behavior }
set { floatingPanel.behaviorAdapter.behavior = newValue}
}
}
+691
View File
@@ -0,0 +1,691 @@
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
import UIKit
/// A set of methods implemented by the delegate of a panel controller allows the adopting delegate to respond to
/// messages from the FloatingPanelController class and thus respond to, and in some affect, operations such as
/// dragging, attracting a panel, layout of a panel and the content, and transition animations.
@objc public protocol FloatingPanelControllerDelegate: class {
/// Returns a FloatingPanelLayout object. If you use the default one, you can use a `FloatingPanelBottomLayout` object.
@objc(floatingPanel:layoutForTraitCollection:) optional
func floatingPanel(_ fpc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout
/// Returns a FloatingPanelLayout object. If you use the default one, you can use a `FloatingPanelBottomLayout` object.
@objc(floatingPanel:layoutForSize:) optional
func floatingPanel(_ fpc: FloatingPanelController, layoutFor size: CGSize) -> FloatingPanelLayout
/// Returns a UIViewPropertyAnimator object to add/present the panel to a position.
///
/// Default is the spring animation with 0.25 secs.
@objc(floatingPanel:animatorForPresentingToState:) optional
func floatingPanel(_ fpc: FloatingPanelController, animatorForPresentingTo state: FloatingPanelState) -> UIViewPropertyAnimator
/// Returns a UIViewPropertyAnimator object to remove/dismiss a panel from a position.
///
/// Default is the spring animator with 0.25 secs.
@objc(floatingPanel:animatorForDismissingWithVelocity:) optional
func floatingPanel(_ fpc: FloatingPanelController, animatorForDismissingWith velocity: CGVector) -> UIViewPropertyAnimator
/// Called when a panel has changed to a new position. Can be called inside an animation block, so any
/// view properties set inside this function will be automatically animated alongside a panel.
@objc optional
func floatingPanelDidChangePosition(_ fpc: FloatingPanelController)
/// Asks the delegate if dragging should begin by the pan gesture recognizer.
@objc optional
func floatingPanelShouldBeginDragging(_ fpc: FloatingPanelController) -> Bool
/// Called when the user drags the surface or the surface is attracted to a state anchor.
@objc optional
func floatingPanelDidMove(_ fpc: FloatingPanelController) // any surface frame changes in dragging
/// Called on start of dragging (may require some time and or distance to move)
@objc optional
func floatingPanelWillBeginDragging(_ fpc: FloatingPanelController)
/// Called on finger up if the user dragged. velocity is in points/second.
@objc optional
func floatingPanelWillEndDragging(_ fpc: FloatingPanelController, withVelocity velocity: CGPoint, targetState: UnsafeMutablePointer<FloatingPanelState>)
/// Called on finger up if the user dragged.
///
/// If `attract` is true, it will continue moving afterwards to a nearby state anchor.
@objc optional
func floatingPanelDidEndDragging(_ fpc: FloatingPanelController, willAttract attract: Bool)
/// Called when it is about to be attracted to a state anchor.
@objc optional
func floatingPanelWillBeginAttracting(_ fpc: FloatingPanelController, to state: FloatingPanelState) // called on finger up as a panel are moving
/// Called when attracting it is completed.
@objc optional
func floatingPanelDidEndAttracting(_ fpc: FloatingPanelController) // called when a panel stops
/// Asks the delegate whether a panel should be removed when dragging ended at the specified location
///
/// This delegate method is called only where `FloatingPanelController.isRemovalInteractionEnabled` is `true`.
/// The velocity vector is calculated from the distance to a point of the hidden state and the pan gesture's velocity.
@objc(floatingPanel:shouldRemoveAtLocation:withVelocity:)
optional
func floatingPanel(_ fpc: FloatingPanelController, shouldRemoveAt location: CGPoint, with velocity: CGVector) -> Bool
/// Called on start to remove its view controller from the parent view controller.
@objc(floatingPanelWillRemove:)
optional
func floatingPanelWillRemove(_ fpc: FloatingPanelController)
/// Called when a panel is removed from the parent view controller.
@objc optional
func floatingPanelDidRemove(_ fpc: FloatingPanelController)
/// Asks the delegate for a content offset of the tracking scroll view to be pinned when a panel moves
///
/// If you do not implement this method, the controller uses a value of the content offset plus the content insets
/// of the tracked scroll view. Your implementation of this method can return a value for a navigation bar with a large
/// title, for example.
///
/// This method will not be called if the controller doesn't track any scroll view.
@objc(floatingPanel:contentOffsetForPinningScrollView:)
optional
func floatingPanel(_ fpc: FloatingPanelController, contentOffsetForPinning trackingScrollView: UIScrollView) -> CGPoint
}
///
/// A container view controller to display a panel to present contents in parallel as a user wants.
///
@objc
open class FloatingPanelController: UIViewController {
/// Constants indicating how safe area insets are added to the adjusted content inset.
@objc
public enum ContentInsetAdjustmentBehavior: Int {
case always
case never
}
/// A flag used to determine how the controller object lays out the content view when the surface position changes.
@objc
public enum ContentMode: Int {
/// The option to fix the content to keep the height of the top most position.
case `static`
/// The option to scale the content to fit the bounds of the root view by changing the surface position.
case fitToBounds
}
/// The delegate of a panel controller object.
@objc
public weak var delegate: FloatingPanelControllerDelegate?{
didSet{
didUpdateDelegate()
}
}
/// Returns the surface view managed by the controller object. It's the same as `self.view`.
@objc
public var surfaceView: SurfaceView! {
return floatingPanel.surfaceView
}
/// Returns the backdrop view managed by the controller object.
@objc
public var backdropView: BackdropView! {
return floatingPanel.backdropView
}
/// Returns the scroll view that the controller tracks.
@objc
public weak var trackingScrollView: UIScrollView? {
return floatingPanel.scrollView
}
// The underlying gesture recognizer for pan gestures
@objc
public var panGestureRecognizer: FloatingPanelPanGestureRecognizer {
return floatingPanel.panGestureRecognizer
}
/// The current position of a panel controller's contents.
@objc
public var state: FloatingPanelState {
return floatingPanel.state
}
/// A Boolean value indicating whether a panel controller is attracting the surface to a state anchor.
@objc
public var isAttracting: Bool {
return floatingPanel.isAttracting
}
/// The layout object managed by the controller
@objc
public var layout: FloatingPanelLayout {
get { _layout }
set {
_layout = newValue
if let parent = parent, let layout = newValue as? UIViewController, layout == parent {
log.warning("A memory leak will occur by a retain cycle because \(self) owns the parent view controller(\(parent)) as the layout object. Don't let the parent adopt FloatingPanelLayout.")
}
}
}
/// The behavior object managed by the controller
@objc
public var behavior: FloatingPanelBehavior {
get { _behavior }
set {
_behavior = newValue
if let parent = parent, let behavior = newValue as? UIViewController, behavior == parent {
log.warning("A memory leak will occur by a retain cycle because \(self) owns the parent view controller(\(parent)) as the behavior object. Don't let the parent adopt FloatingPanelBehavior.")
}
}
}
/// The content insets of the tracking scroll view derived from this safe area
@objc
public var adjustedContentInsets: UIEdgeInsets {
return floatingPanel.layoutAdapter.adjustedContentInsets
}
/// The behavior for determining the adjusted content offsets.
///
/// This property specifies how the content area of the tracking scroll view is modified using `adjustedContentInsets`. The default value of this property is FloatingPanelController.ContentInsetAdjustmentBehavior.always.
@objc
public var contentInsetAdjustmentBehavior: ContentInsetAdjustmentBehavior = .always
/// A Boolean value that determines whether the removal interaction is enabled.
@objc
public var isRemovalInteractionEnabled: Bool {
@objc(setRemovalInteractionEnabled:) set { floatingPanel.isRemovalInteractionEnabled = newValue }
@objc(isRemovalInteractionEnabled) get { return floatingPanel.isRemovalInteractionEnabled }
}
/// The view controller responsible for the content portion of a panel.
@objc
public var contentViewController: UIViewController? {
set { set(contentViewController: newValue) }
get { return _contentViewController }
}
/// The NearbyState determines that finger's nearby state.
public var nearbyState: FloatingPanelState {
let currentY = surfaceLocation.y
return floatingPanel.targetPosition(from: currentY, with: .zero)
}
/// Constants that define how a panel content fills in the surface.
@objc
public var contentMode: ContentMode = .static {
didSet {
guard state != .hidden else { return }
activateLayout(forceLayout: false)
}
}
private var _contentViewController: UIViewController?
private(set) var floatingPanel: Core!
private var preSafeAreaInsets: UIEdgeInsets = .zero // Capture the latest one
private var safeAreaInsetsObservation: NSKeyValueObservation?
private let modalTransition = ModalTransition()
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setUp()
}
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
super.init(nibName: nil, bundle: nil)
setUp()
}
/// Initialize a newly created panel controller.
@objc
public init(delegate: FloatingPanelControllerDelegate? = nil) {
super.init(nibName: nil, bundle: nil)
self.delegate = delegate
setUp()
}
private func setUp() {
_ = FloatingPanelController.dismissSwizzling
modalPresentationStyle = .custom
transitioningDelegate = modalTransition
let initialLayout: FloatingPanelLayout
if let layout = delegate?.floatingPanel?(self, layoutFor: traitCollection) {
initialLayout = layout
} else {
initialLayout = FloatingPanelBottomLayout()
}
let initialBehavior = FloatingPanelDefaultBehavior()
floatingPanel = Core(self, layout: initialLayout, behavior: initialBehavior)
}
private func didUpdateDelegate(){
if let layout = delegate?.floatingPanel?(self, layoutFor: traitCollection) {
_layout = layout
}
}
// MARK:- Overrides
/// Creates the view that the controller manages.
open override func loadView() {
assert(self.storyboard == nil, "Storyboard isn't supported")
let view = PassThroughView()
view.backgroundColor = .clear
backdropView.frame = view.bounds
view.addSubview(backdropView)
surfaceView.frame = view.bounds
view.addSubview(surfaceView)
self.view = view as UIView
}
open override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if #available(iOS 11.0, *) {
// Ensure to update the static constraint of a panel after rotating a device in static mode
if contentMode == .static {
floatingPanel.layoutAdapter.updateStaticConstraint()
}
} else {
// Because {top,bottom}LayoutGuide is managed as a view
if floatingPanel.isAttracting == false {
self.update(safeAreaInsets: fp_safeAreaInsets)
}
}
}
open override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
if self.view.bounds.size == size {
return
}
// Change a layout for the new view size
if let newLayout = self.delegate?.floatingPanel?(self, layoutFor: size) {
layout = newLayout
activateLayout(forceLayout: false)
}
if view.translatesAutoresizingMaskIntoConstraints {
view.frame.size = size
view.layoutIfNeeded()
}
}
open override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) {
super.willTransition(to: newCollection, with: coordinator)
if shouldUpdateLayout(from: traitCollection, to: newCollection) == false {
return
}
// Change a layout for the new trait collection
if let newLayout = self.delegate?.floatingPanel?(self, layoutFor: newCollection) {
self.layout = newLayout
activateLayout(forceLayout: false)
}
}
open override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
safeAreaInsetsObservation = nil
}
// MARK:- Child view controller to consult
open override var childForStatusBarStyle: UIViewController? {
return contentViewController
}
open override var childForStatusBarHidden: UIViewController? {
return contentViewController
}
open override var childForScreenEdgesDeferringSystemGestures: UIViewController? {
return contentViewController
}
open override var childForHomeIndicatorAutoHidden: UIViewController? {
return contentViewController
}
// MARK:- Privates
private func shouldUpdateLayout(from previous: UITraitCollection, to new: UITraitCollection) -> Bool {
return previous.horizontalSizeClass != new.horizontalSizeClass
|| previous.verticalSizeClass != new.verticalSizeClass
|| previous.preferredContentSizeCategory != new.preferredContentSizeCategory
|| previous.layoutDirection != new.layoutDirection
}
private func update(safeAreaInsets: UIEdgeInsets) {
guard
preSafeAreaInsets != safeAreaInsets
else { return }
log.debug("Update safeAreaInsets", safeAreaInsets)
// Prevent an infinite loop on iOS 10: setUpLayout() -> viewDidLayoutSubviews() -> setUpLayout()
preSafeAreaInsets = safeAreaInsets
// preserve the current content offset if contentInsetAdjustmentBehavior is `.always`
var contentOffset: CGPoint?
if contentInsetAdjustmentBehavior == .always {
contentOffset = trackingScrollView?.contentOffset
}
floatingPanel.layoutAdapter.updateStaticConstraint()
if let contentOffset = contentOffset {
trackingScrollView?.contentOffset = contentOffset
}
switch contentInsetAdjustmentBehavior {
case .always:
trackingScrollView?.contentInset = adjustedContentInsets
default:
break
}
}
private func activateLayout(forceLayout: Bool = false) {
floatingPanel.layoutAdapter.prepareLayout()
// preserve the current content offset if contentInsetAdjustmentBehavior is `.always`
var contentOffset: CGPoint?
if contentInsetAdjustmentBehavior == .always {
contentOffset = trackingScrollView?.contentOffset
}
floatingPanel.layoutAdapter.updateStaticConstraint()
floatingPanel.layoutAdapter.activateLayout(for: floatingPanel.state, forceLayout: forceLayout)
if let contentOffset = contentOffset {
trackingScrollView?.contentOffset = contentOffset
}
}
func remove() {
if presentingViewController != nil, parent == nil {
delegate?.floatingPanelWillRemove?(self)
dismiss(animated: true) { [weak self] in
guard let self = self else { return }
self.delegate?.floatingPanelDidRemove?(self)
}
} else {
removePanelFromParent(animated: true)
}
}
// MARK: - Container view controller interface
/// Shows the surface view at the initial position defined by the current layout
@objc(show:completion:)
public func show(animated: Bool = false, completion: (() -> Void)? = nil) {
// Must apply the current layout here
activateLayout(forceLayout: true)
if #available(iOS 11.0, *) {
// Must track the safeAreaInsets of `self.view` to update the layout.
// There are 2 reasons.
// 1. This or the parent VC doesn't call viewSafeAreaInsetsDidChange() on the bottom
// inset's update expectedly.
// 2. The safe area top inset can be variable on the large title navigation bar(iOS11+).
// That's why it needs the observation to keep `adjustedContentInsets` correct.
safeAreaInsetsObservation = self.view.observe(\.safeAreaInsets, options: [.initial, .new, .old]) { [weak self] (_, change) in
// Use `self.view.safeAreaInsets` because `change.newValue` can be nil in particular case when
// is reported in https://github.com/SCENEE/FloatingPanel/issues/330
guard let self = self, change.oldValue != self.view.safeAreaInsets else { return }
self.update(safeAreaInsets: self.view.safeAreaInsets)
}
} else {
// KVOs for topLayoutGuide & bottomLayoutGuide are not effective.
// Instead, update(safeAreaInsets:) is called at `viewDidLayoutSubviews()`
}
move(to: floatingPanel.layoutAdapter.initialState,
animated: animated,
completion: completion)
}
/// Hides the surface view to the hidden position
@objc(hide:completion:)
public func hide(animated: Bool = false, completion: (() -> Void)? = nil) {
move(to: .hidden,
animated: animated,
completion: completion)
}
/// Adds the view managed by the controller as a child of the specified view controller.
/// - Parameters:
/// - parent: A parent view controller object that displays FloatingPanelController's view. A container view controller object isn't applicable.
/// - viewIndex: Insert the surface view managed by the controller below the specified view index. By default, the surface view will be added to the end of the parent list of subviews.
/// - animated: Pass true to animate the presentation; otherwise, pass false.
@objc(addPanelToParent:at:animated:)
public func addPanel(toParent parent: UIViewController, at viewIndex: Int = -1, animated: Bool = false) {
guard self.parent == nil else {
log.warning("Already added to a parent(\(parent))")
return
}
assert((parent is UINavigationController) == false, "UINavigationController displays only one child view controller at a time.")
assert((parent is UITabBarController) == false, "UITabBarController displays child view controllers with a radio-style selection interface")
assert((parent is UISplitViewController) == false, "UISplitViewController manages two child view controllers in a master-detail interface")
assert((parent is UITableViewController) == false, "UITableViewController should not be the parent because the view is a table view so that a panel doesn't work well")
assert((parent is UICollectionViewController) == false, "UICollectionViewController should not be the parent because the view is a collection view so that a panel doesn't work well")
if viewIndex < 0 {
parent.view.addSubview(self.view)
} else {
parent.view.insertSubview(self.view, at: viewIndex)
}
parent.addChild(self)
view.frame = parent.view.bounds // Needed for a correct safe area configuration
view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
self.view.topAnchor.constraint(equalTo: parent.view.topAnchor, constant: 0.0),
self.view.leftAnchor.constraint(equalTo: parent.view.leftAnchor, constant: 0.0),
self.view.rightAnchor.constraint(equalTo: parent.view.rightAnchor, constant: 0.0),
self.view.bottomAnchor.constraint(equalTo: parent.view.bottomAnchor, constant: 0.0),
])
show(animated: animated) { [weak self] in
guard let `self` = self else { return }
self.didMove(toParent: parent)
}
}
/// Removes the controller and the managed view from its parent view controller
/// - Parameters:
/// - animated: Pass true to animate the presentation; otherwise, pass false.
/// - completion: The block to execute after the view controller is dismissed. This block has no return value and takes no parameters. You may specify nil for this parameter.
@objc(removePanelFromParent:completion:)
public func removePanelFromParent(animated: Bool, completion: (() -> Void)? = nil) {
guard self.parent != nil else {
completion?()
return
}
delegate?.floatingPanelWillRemove?(self)
hide(animated: animated) { [weak self] in
guard let `self` = self else { return }
self.willMove(toParent: nil)
self.view.removeFromSuperview()
self.removeFromParent()
self.delegate?.floatingPanelDidRemove?(self)
completion?()
}
}
/// Moves the position to the specified position.
/// - Parameters:
/// - to: Pass a FloatingPanelPosition value to move the surface view to the position.
/// - animated: Pass true to animate the presentation; otherwise, pass false.
/// - completion: The block to execute after the view controller has finished moving. This block has no return value and takes no parameters. You may specify nil for this parameter.
@objc(moveToState:animated:completion:)
public func move(to: FloatingPanelState, animated: Bool, completion: (() -> Void)? = nil) {
assert(floatingPanel.layoutAdapter.vc != nil, "Use show(animated:completion)")
floatingPanel.move(to: to, animated: animated, completion: completion)
}
/// Sets the view controller responsible for the content portion of a panel.
public func set(contentViewController: UIViewController?) {
if let vc = _contentViewController {
vc.willMove(toParent: nil)
vc.view.removeFromSuperview()
vc.removeFromParent()
}
if let vc = contentViewController {
addChild(vc)
let surfaceView = floatingPanel.surfaceView
surfaceView.set(contentView: vc.view)
vc.didMove(toParent: self)
}
_contentViewController = contentViewController
}
// MARK: - Scroll view tracking
/// Tracks the specified scroll view to correspond with the scroll.
///
/// - Parameters:
/// - scrollView: Specify a scroll view to continuously and seamlessly work in concert with interactions of the surface view
@objc(trackScrollView:)
public func track(scrollView: UIScrollView) {
if let scrollView = floatingPanel.scrollView {
untrack(scrollView: scrollView)
}
floatingPanel.scrollView = scrollView
switch contentInsetAdjustmentBehavior {
case .always:
if #available(iOS 11.0, *) {
scrollView.contentInsetAdjustmentBehavior = .never
} else {
children.forEach { (vc) in
vc.automaticallyAdjustsScrollViewInsets = false
}
}
default:
break
}
}
/// Cancel tracking the specify scroll view.
///
@objc(untrackScrollView:)
public func untrack(scrollView: UIScrollView) {
if floatingPanel.scrollView == scrollView {
floatingPanel.scrollView = nil
}
}
// MARK: - Utilities
/// Updates the layout object from the delegate and lays out the views managed
/// by the controller immediately.
///
/// This method updates the `FloatingPanelLayout` object from the delegate and
/// then it calls `layoutIfNeeded()` of the root view to force the view
/// to update the layout immediately. It can be called in an
/// animation block.
@objc
public func invalidateLayout() {
activateLayout(forceLayout: true)
}
/// Returns the surface's position in a panel controller's view for the specified state.
///
/// If a panel is top positioned, this returns a point of the bottom-left corner of the surface. If it is left positioned
/// this returns a point of top-right corner of the surface. If it is bottom or right positioned, this returns a point
/// of the top-left corner of the surface.
@objc
public func surfaceLocation(for state: FloatingPanelState) -> CGPoint {
return floatingPanel.layoutAdapter.surfaceLocation(for: state)
}
/// The surface's position in a panel controller's view.
///
/// If a panel is top positioned, this returns a point of the bottom-left corner of the surface. If it is left positioned
/// this returns a point of top-right corner of the surface. If it is bottom or right positioned, this returns a point
/// of the top-left corner of the surface.
@objc
public var surfaceLocation: CGPoint {
get { floatingPanel.layoutAdapter.surfaceLocation }
set { floatingPanel.layoutAdapter.surfaceLocation = newValue }
}
}
extension FloatingPanelController {
func notifyDidMove() {
#if !TEST
guard self.view.window != nil else { return }
#endif
delegate?.floatingPanelDidMove?(self)
}
}
extension FloatingPanelController {
private static let dismissSwizzling: Any? = {
let aClass: AnyClass! = UIViewController.self //object_getClass(vc)
if let imp = class_getMethodImplementation(aClass, #selector(dismiss(animated:completion:))),
let originalAltMethod = class_getInstanceMethod(aClass, #selector(fp_original_dismiss(animated:completion:))) {
method_setImplementation(originalAltMethod, imp)
}
let originalMethod = class_getInstanceMethod(aClass, #selector(dismiss(animated:completion:)))
let swizzledMethod = class_getInstanceMethod(aClass, #selector(fp_dismiss(animated:completion:)))
if let originalMethod = originalMethod, let swizzledMethod = swizzledMethod {
// switch implementation..
method_exchangeImplementations(originalMethod, swizzledMethod)
}
return nil
}()
}
public extension UIViewController {
@objc func fp_original_dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
// Implementation will be replaced by IMP of self.dismiss(animated:completion:)
}
@objc func fp_dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
// Call dismiss(animated:completion:) to a content view controller
if let fpc = parent as? FloatingPanelController {
if fpc.presentingViewController != nil {
self.fp_original_dismiss(animated: flag, completion: completion)
} else {
fpc.removePanelFromParent(animated: flag, completion: completion)
}
return
}
// Call dismiss(animated:completion:) to FloatingPanelController directly
if let fpc = self as? FloatingPanelController {
// When a panel is presented modally and it's not a child view controller of the presented view controller.
if fpc.presentingViewController != nil, fpc.parent == nil {
self.fp_original_dismiss(animated: flag, completion: completion)
} else {
fpc.removePanelFromParent(animated: flag, completion: completion)
}
return
}
// For other view controllers
self.fp_original_dismiss(animated: flag, completion: completion)
}
}
+1184
View File
File diff suppressed because it is too large Load Diff
+11
View File
@@ -0,0 +1,11 @@
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
#ifndef FloatingPanel_h
#define FloatingPanel_h
#import <UIKit/UIKit.h>
FOUNDATION_EXPORT double FloatingPanelVersionNumber;
FOUNDATION_EXPORT const unsigned char FloatingPanelVersionString[];
#endif /* FloatingPanel_h */
@@ -1,11 +1,10 @@
//
// Created by Shin Yamamoto on 2018/09/19.
// Copyright © 2018 Shin Yamamoto. All rights reserved.
//
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
import UIKit
public class GrabberHandleView: UIView {
/// A view that presents a grabber handle in the surface of a panel.
@objc(FloatingPanelGrabberView)
public class GrabberView: UIView {
public var barColor = UIColor(displayP3Red: 0.76, green: 0.77, blue: 0.76, alpha: 1.0) { didSet { backgroundColor = barColor } }
@@ -30,6 +29,5 @@ public class GrabberHandleView: UIView {
private func render() {
self.layer.masksToBounds = true
self.layer.cornerRadius = frame.size.height * 0.5
}
}
@@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.6.6</string>
<string>2.0.0</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
</dict>
+829
View File
@@ -0,0 +1,829 @@
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
import UIKit
/// An interface for generating layout information for a panel.
@objc public protocol FloatingPanelLayout {
/// Returns the position of a panel in a `FloatingPanelController` view .
@objc var position: FloatingPanelPosition { get }
/// Returns the initial state when a panel is presented.
@objc var initialState: FloatingPanelState { get }
/// Returns the layout anchors to specify the snapping locations for each state.
@objc var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] { get }
/// Returns layout constraints to determine the cross dimension of a panel.
@objc optional func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint]
/// Returns the alpha value of the backdrop of a panel for each state.
@objc optional func backdropAlpha(for state: FloatingPanelState) -> CGFloat
}
/// A layout object that lays out a panel in bottom sheet style.
@objcMembers
open class FloatingPanelBottomLayout: NSObject, FloatingPanelLayout {
public override init() {
super.init()
}
open var initialState: FloatingPanelState {
return .half
}
open var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] {
return [
.full: FloatingPanelLayoutAnchor(absoluteInset: 18.0, edge: .top, referenceGuide: .safeArea),
.half: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .safeArea),
.tip: FloatingPanelLayoutAnchor(absoluteInset: 69.0, edge: .bottom, referenceGuide: .safeArea),
]
}
open var position: FloatingPanelPosition {
return .bottom
}
open func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint] {
return [
surfaceView.leftAnchor.constraint(equalTo: view.fp_safeAreaLayoutGuide.leftAnchor, constant: 0.0),
surfaceView.rightAnchor.constraint(equalTo: view.fp_safeAreaLayoutGuide.rightAnchor, constant: 0.0),
]
}
open func backdropAlpha(for state: FloatingPanelState) -> CGFloat {
return state == .full ? 0.3 : 0.0
}
}
struct LayoutSegment {
let lower: FloatingPanelState?
let upper: FloatingPanelState?
}
class LayoutAdapter {
weak var vc: FloatingPanelController!
private weak var surfaceView: SurfaceView!
private weak var backdropView: BackdropView!
private let defaultLayout = FloatingPanelBottomLayout()
fileprivate var layout: FloatingPanelLayout {
didSet {
surfaceView.position = position
}
}
private var safeAreaInsets: UIEdgeInsets {
return vc?.fp_safeAreaInsets ?? .zero
}
private var initialConst: CGFloat = 0.0
private var fixedConstraints: [NSLayoutConstraint] = []
private var fullConstraints: [NSLayoutConstraint] = []
private var halfConstraints: [NSLayoutConstraint] = []
private var tipConstraints: [NSLayoutConstraint] = []
private var offConstraints: [NSLayoutConstraint] = []
private var fitToBoundsConstraint: NSLayoutConstraint?
private(set) var interactionConstraint: NSLayoutConstraint?
private(set) var attractionConstraint: NSLayoutConstraint?
private var staticConstraint: NSLayoutConstraint?
private var activeStates: Set<FloatingPanelState> {
return Set(layout.anchors.keys)
}
var initialState: FloatingPanelState {
layout.initialState
}
var position: FloatingPanelPosition {
layout.position
}
var orderedStates: [FloatingPanelState] {
return activeStates.sorted(by: {
return $0.order < $1.order
})
}
var validStates: Set<FloatingPanelState> {
return activeStates.union([.hidden])
}
var sortedDirectionalStates: [FloatingPanelState] {
return activeStates.sorted(by: {
switch position {
case .top, .left:
return $0.order < $1.order
case .bottom, .right:
return $0.order > $1.order
}
})
}
private var directionalLeastState: FloatingPanelState {
return sortedDirectionalStates.first ?? .hidden
}
private var directionalMostState: FloatingPanelState {
return sortedDirectionalStates.last ?? .hidden
}
var edgeLeastState: FloatingPanelState {
if orderedStates.count == 1 {
return .hidden
}
return orderedStates.first ?? .hidden
}
var edgeMostState: FloatingPanelState {
if orderedStates.count == 1 {
return orderedStates[0]
}
return orderedStates.last ?? .hidden
}
var edgeMostY: CGFloat {
return position(for: edgeMostState)
}
var adjustedContentInsets: UIEdgeInsets {
switch position {
case .top:
return UIEdgeInsets(top: safeAreaInsets.top,
left: 0.0,
bottom: 0.0,
right: 0.0)
case .left:
return UIEdgeInsets(top: 0.0,
left: safeAreaInsets.left,
bottom: 0.0,
right: 0.0)
case .bottom:
return UIEdgeInsets(top: 0.0,
left: 0.0,
bottom: safeAreaInsets.bottom,
right: 0.0)
case .right:
return UIEdgeInsets(top: 0.0,
left: 0.0,
bottom: 0.0,
right: safeAreaInsets.right)
}
}
/*
Returns a constraint based value in the interaction and animation.
So that it doesn't need to call `surfaceView.layoutIfNeeded()`
after every interaction and animation update. It has an effect on
the smooth interaction because the content view doesn't need to update
its layout frequently.
*/
var surfaceLocation: CGPoint {
get {
var pos: CGFloat
if let constraint = interactionConstraint {
pos = constraint.constant
} else if let animationConstraint = attractionConstraint, let anchor = layout.anchors[vc.state] {
switch position {
case .top, .bottom:
switch referenceEdge(of: anchor) {
case .top:
pos = animationConstraint.constant
if anchor.referenceGuide == .safeArea {
pos += safeAreaInsets.top
}
case .bottom:
pos = vc.view.bounds.height + animationConstraint.constant
if anchor.referenceGuide == .safeArea {
pos -= safeAreaInsets.bottom
}
default:
fatalError("Unsupported reference edges")
}
case .left, .right:
switch referenceEdge(of: anchor) {
case .left:
pos = animationConstraint.constant
if anchor.referenceGuide == .safeArea {
pos += safeAreaInsets.left
}
case .right:
pos = vc.view.bounds.width + animationConstraint.constant
if anchor.referenceGuide == .safeArea {
pos -= safeAreaInsets.right
}
default:
fatalError("Unsupported reference edges")
}
}
} else {
pos = displayTrunc(edgePosition(surfaceView.frame), by: surfaceView.fp_displayScale)
}
switch position {
case .top, .bottom:
return CGPoint(x: 0.0, y: pos)
case .left, .right:
return CGPoint(x: pos, y: 0.0)
}
}
set {
let pos = position.mainLocation(newValue)
if let constraint = interactionConstraint {
constraint.constant = pos
} else if let animationConstraint = attractionConstraint, let anchor = layout.anchors[vc.state] {
let refEdge = referenceEdge(of: anchor)
switch refEdge {
case .top, .left:
animationConstraint.constant = pos
if anchor.referenceGuide == .safeArea {
animationConstraint.constant -= refEdge.inset(of: safeAreaInsets)
}
case .bottom, .right:
animationConstraint.constant = pos - position.mainDimension(vc.view.bounds.size)
if anchor.referenceGuide == .safeArea {
animationConstraint.constant += refEdge.inset(of: safeAreaInsets)
}
}
} else {
switch position {
case .top:
return surfaceView.frame.origin.y = pos - surfaceView.bounds.height
case .left:
return surfaceView.frame.origin.x = pos - surfaceView.bounds.width
case .bottom:
return surfaceView.frame.origin.y = pos
case .right:
return surfaceView.frame.origin.x = pos
}
}
}
}
var offsetFromEdgeMost: CGFloat {
switch position {
case .top, .left:
return edgePosition(surfaceView.presentationFrame) - position(for: directionalMostState)
case .bottom, .right:
return position(for: directionalLeastState) - edgePosition(surfaceView.presentationFrame)
}
}
private var hiddenAnchor: FloatingPanelLayoutAnchoring {
switch position {
case .top:
return FloatingPanelLayoutAnchor(absoluteInset: -100, edge: .top, referenceGuide: .superview)
case .left:
return FloatingPanelLayoutAnchor(absoluteInset: -100, edge: .left, referenceGuide: .superview)
case .bottom:
return FloatingPanelLayoutAnchor(absoluteInset: -100, edge: .bottom, referenceGuide: .superview)
case .right:
return FloatingPanelLayoutAnchor(absoluteInset: -100, edge: .right, referenceGuide: .superview)
}
}
init(vc: FloatingPanelController,
surfaceView: SurfaceView,
backdropView: BackdropView,
layout: FloatingPanelLayout) {
self.vc = vc
self.layout = layout
self.surfaceView = surfaceView
self.backdropView = backdropView
}
func surfaceLocation(for state: FloatingPanelState) -> CGPoint {
let pos = displayTrunc(position(for: state), by: surfaceView.fp_displayScale)
switch layout.position {
case .top, .bottom:
return CGPoint(x: 0.0, y: pos)
case .left, .right:
return CGPoint(x: pos, y: 0.0)
}
}
func position(for state: FloatingPanelState) -> CGFloat {
let bounds = vc.view.bounds
let anchor = layout.anchors[state] ?? self.hiddenAnchor
switch anchor {
case let anchor as FloatingPanelIntrinsicLayoutAnchor:
let intrinsicLength = position.mainDimension(surfaceView.intrinsicContentSize)
let diff = anchor.isAbsolute ? anchor.offset : intrinsicLength * anchor.offset
switch position {
case .top, .left:
var base: CGFloat = 0.0
if anchor.referenceGuide == .safeArea {
base += position.inset(safeAreaInsets)
}
return base + intrinsicLength - diff
case .bottom, .right:
var base = position.mainDimension(bounds.size)
if anchor.referenceGuide == .safeArea {
base -= position.inset(safeAreaInsets)
}
return base - intrinsicLength + diff
}
case let anchor as FloatingPanelLayoutAnchor:
let referenceBounds = anchor.referenceGuide == .safeArea ? bounds.inset(by: safeAreaInsets) : bounds
let diff = anchor.isAbsolute ? anchor.inset : position.mainDimension(referenceBounds.size) * anchor.inset
switch anchor.referenceEdge {
case .top:
return referenceBounds.minY + diff
case .left:
return referenceBounds.minX + diff
case .bottom:
return referenceBounds.maxY - diff
case .right:
return referenceBounds.maxX - diff
}
default:
fatalError("Unsupported a FloatingPanelLayoutAnchoring object")
}
}
func isIntrinsicAnchor(state: FloatingPanelState) -> Bool {
return layout.anchors[state] is FloatingPanelIntrinsicLayoutAnchor
}
private func edgePosition(_ frame: CGRect) -> CGFloat {
switch position {
case .top:
return frame.maxY
case .left:
return frame.maxX
case .bottom:
return frame.minY
case .right:
return frame.minX
}
}
private func referenceEdge(of anchor: FloatingPanelLayoutAnchoring) -> FloatingPanelReferenceEdge {
switch anchor {
case is FloatingPanelIntrinsicLayoutAnchor:
switch position {
case .top: return .top
case .left: return .left
case .bottom: return .bottom
case .right: return .right
}
case let anchor as FloatingPanelLayoutAnchor:
return anchor.referenceEdge
default:
fatalError("Unsupported a FloatingPanelLayoutAnchoring object")
}
}
func prepareLayout() {
NSLayoutConstraint.deactivate(fixedConstraints)
surfaceView.translatesAutoresizingMaskIntoConstraints = false
backdropView.translatesAutoresizingMaskIntoConstraints = false
// Fixed constraints of surface and backdrop views
let surfaceConstraints: [NSLayoutConstraint]
if let constraints = layout.prepareLayout?(surfaceView: surfaceView, in: vc.view) {
surfaceConstraints = constraints
} else {
switch position {
case .top, .bottom:
surfaceConstraints = [
surfaceView.leftAnchor.constraint(equalTo: vc.fp_safeAreaLayoutGuide.leftAnchor, constant: 0.0),
surfaceView.rightAnchor.constraint(equalTo: vc.fp_safeAreaLayoutGuide.rightAnchor, constant: 0.0),
]
case .left, .right:
surfaceConstraints = [
surfaceView.topAnchor.constraint(equalTo: vc.fp_safeAreaLayoutGuide.topAnchor, constant: 0.0),
surfaceView.bottomAnchor.constraint(equalTo: vc.fp_safeAreaLayoutGuide.bottomAnchor, constant: 0.0),
]
}
}
let backdropConstraints = [
backdropView.topAnchor.constraint(equalTo: vc.view.topAnchor, constant: 0.0),
backdropView.leftAnchor.constraint(equalTo: vc.view.leftAnchor,constant: 0.0),
backdropView.rightAnchor.constraint(equalTo: vc.view.rightAnchor, constant: 0.0),
backdropView.bottomAnchor.constraint(equalTo: vc.view.bottomAnchor, constant: 0.0),
]
fixedConstraints = surfaceConstraints + backdropConstraints
NSLayoutConstraint.deactivate(constraint: self.fitToBoundsConstraint)
self.fitToBoundsConstraint = nil
if vc.contentMode == .fitToBounds {
switch position {
case .top:
fitToBoundsConstraint = surfaceView.topAnchor.constraint(equalTo: vc.view.topAnchor, constant: 0.0)
fitToBoundsConstraint?.identifier = "FloatingPanel-fit-to-top"
case .left:
fitToBoundsConstraint = surfaceView.leftAnchor.constraint(equalTo: vc.view.leftAnchor, constant: 0.0)
fitToBoundsConstraint?.identifier = "FloatingPanel-fit-to-left"
case .bottom:
fitToBoundsConstraint = surfaceView.bottomAnchor.constraint(equalTo: vc.view.bottomAnchor, constant: 0.0)
fitToBoundsConstraint?.identifier = "FloatingPanel-fit-to-bottom"
case .right:
fitToBoundsConstraint = surfaceView.rightAnchor.constraint(equalTo: vc.view.rightAnchor, constant: 0.0)
fitToBoundsConstraint?.identifier = "FloatingPanel-fit-to-right"
}
fitToBoundsConstraint?.priority = .defaultHigh
}
updateStateConstraints()
}
private func updateStateConstraints() {
NSLayoutConstraint.deactivate(fullConstraints + halfConstraints + tipConstraints + offConstraints)
if let fullAnchor = layout.anchors[.full] {
fullConstraints = fullAnchor.layoutConstraints(vc, for: position)
fullConstraints.forEach {
$0.identifier = "FloatingPanel-full-constraint"
}
}
if let halfAnchor = layout.anchors[.half] {
halfConstraints = halfAnchor.layoutConstraints(vc, for: position)
halfConstraints.forEach {
$0.identifier = "FloatingPanel-half-constraint"
}
}
if let tipAnchors = layout.anchors[.tip] {
tipConstraints = tipAnchors.layoutConstraints(vc, for: position)
tipConstraints.forEach {
$0.identifier = "FloatingPanel-tip-constraint"
}
}
let hiddenAnchor = layout.anchors[.hidden] ?? self.hiddenAnchor
offConstraints = hiddenAnchor.layoutConstraints(vc, for: position)
offConstraints.forEach {
$0.identifier = "FloatingPanel-hidden-constraint"
}
}
func startInteraction(at state: FloatingPanelState, offset: CGPoint = .zero) {
if let constraint = interactionConstraint {
initialConst = constraint.constant
return
}
tearDownAttraction()
NSLayoutConstraint.deactivate(fullConstraints + halfConstraints + tipConstraints + offConstraints)
initialConst = edgePosition(surfaceView.frame) + offset.y
let constraint: NSLayoutConstraint
switch position {
case .top:
constraint = surfaceView.bottomAnchor.constraint(equalTo: vc.view.topAnchor, constant: initialConst)
case .left:
constraint = surfaceView.rightAnchor.constraint(equalTo: vc.view.leftAnchor, constant: initialConst)
case .bottom:
constraint = surfaceView.topAnchor.constraint(equalTo: vc.view.topAnchor, constant: initialConst)
case .right:
constraint = surfaceView.leftAnchor.constraint(equalTo: vc.view.leftAnchor, constant: initialConst)
}
constraint.priority = .defaultHigh
constraint.identifier = "FloatingPanel-interaction"
NSLayoutConstraint.activate([constraint])
self.interactionConstraint = constraint
}
func endInteraction(at state: FloatingPanelState) {
// Don't deactivate `interactiveTopConstraint` here because it leads to
// unsatisfiable constraints
if self.interactionConstraint == nil {
// Activate `interactiveTopConstraint` for `fitToBounds` mode.
// It goes through this path when the pan gesture state jumps
// from .begin to .end.
startInteraction(at: state)
}
}
func setUpAttraction(to state: FloatingPanelState) -> (NSLayoutConstraint, CGFloat) {
NSLayoutConstraint.deactivate(constraint: attractionConstraint)
let anchor = layout.anchors[state] ?? self.hiddenAnchor
NSLayoutConstraint.deactivate(fullConstraints + halfConstraints + tipConstraints + offConstraints)
NSLayoutConstraint.deactivate(constraint: interactionConstraint)
interactionConstraint = nil
let layoutGuideProvider: LayoutGuideProvider
switch anchor.referenceGuide {
case .safeArea:
layoutGuideProvider = vc.fp_safeAreaLayoutGuide
case .superview:
layoutGuideProvider = vc.view
}
let currentY = position.mainLocation(surfaceLocation)
let baseHeight = position.mainDimension(vc.view.bounds.size)
let animationConstraint: NSLayoutConstraint
var targetY = position(for: state)
switch position {
case .top:
switch referenceEdge(of: anchor) {
case .top:
animationConstraint = surfaceView.bottomAnchor.constraint(equalTo: layoutGuideProvider.topAnchor,
constant: currentY)
if anchor.referenceGuide == .safeArea {
animationConstraint.constant -= safeAreaInsets.top
targetY -= safeAreaInsets.top
}
case .bottom:
let baseHeight = vc.view.bounds.height
targetY = -(baseHeight - targetY)
animationConstraint = surfaceView.bottomAnchor.constraint(equalTo: layoutGuideProvider.bottomAnchor,
constant: -(baseHeight - currentY))
if anchor.referenceGuide == .safeArea {
animationConstraint.constant += safeAreaInsets.bottom
targetY += safeAreaInsets.bottom
}
default:
fatalError("Unsupported reference edges")
}
case .left:
switch referenceEdge(of: anchor) {
case .left:
animationConstraint = surfaceView.rightAnchor.constraint(equalTo: layoutGuideProvider.leftAnchor,
constant: currentY)
if anchor.referenceGuide == .safeArea {
animationConstraint.constant -= safeAreaInsets.right
targetY -= safeAreaInsets.right
}
case .right:
targetY = -(baseHeight - targetY)
animationConstraint = surfaceView.rightAnchor.constraint(equalTo: layoutGuideProvider.rightAnchor,
constant: -(baseHeight - currentY))
if anchor.referenceGuide == .safeArea {
animationConstraint.constant += safeAreaInsets.left
targetY += safeAreaInsets.left
}
default:
fatalError("Unsupported reference edges")
}
case .bottom:
switch referenceEdge(of: anchor) {
case .top:
animationConstraint = surfaceView.topAnchor.constraint(equalTo: layoutGuideProvider.topAnchor,
constant: currentY)
if anchor.referenceGuide == .safeArea {
animationConstraint.constant -= safeAreaInsets.top
targetY -= safeAreaInsets.top
}
case .bottom:
targetY = -(baseHeight - targetY)
animationConstraint = surfaceView.topAnchor.constraint(equalTo: layoutGuideProvider.bottomAnchor,
constant: -(baseHeight - currentY))
if anchor.referenceGuide == .safeArea {
animationConstraint.constant += safeAreaInsets.bottom
targetY += safeAreaInsets.bottom
}
default:
fatalError("Unsupported reference edges")
}
case .right:
switch referenceEdge(of: anchor) {
case .left:
animationConstraint = surfaceView.leftAnchor.constraint(equalTo: layoutGuideProvider.leftAnchor,
constant: currentY)
if anchor.referenceGuide == .safeArea {
animationConstraint.constant -= safeAreaInsets.left
targetY -= safeAreaInsets.left
}
case .right:
targetY = -(baseHeight - targetY)
animationConstraint = surfaceView.leftAnchor.constraint(equalTo: layoutGuideProvider.rightAnchor,
constant: -(baseHeight - currentY))
if anchor.referenceGuide == .safeArea {
animationConstraint.constant += safeAreaInsets.right
targetY += safeAreaInsets.right
}
default:
fatalError("Unsupported reference edges")
}
}
animationConstraint.priority = .defaultHigh
animationConstraint.identifier = "FloatingPanel-attraction"
NSLayoutConstraint.activate([animationConstraint])
self.attractionConstraint = animationConstraint
return (animationConstraint, targetY)
}
private func tearDownAttraction() {
NSLayoutConstraint.deactivate(constraint: attractionConstraint)
attractionConstraint = nil
}
// The method is separated from prepareLayout(to:) for the rotation support
// It must be called in FloatingPanelController.traitCollectionDidChange(_:)
func updateStaticConstraint() {
guard let vc = vc else { return }
NSLayoutConstraint.deactivate(constraint: staticConstraint)
staticConstraint = nil
if vc.contentMode == .fitToBounds {
surfaceView.containerOverflow = 0
return
}
let anchor = layout.anchors[self.edgeMostState]!
if anchor is FloatingPanelIntrinsicLayoutAnchor {
var constant = layout.position.mainDimension(surfaceView.intrinsicContentSize)
if anchor.referenceGuide == .safeArea {
constant += position.inset(safeAreaInsets)
}
staticConstraint = position.mainDimensionAnchor(surfaceView).constraint(equalToConstant: constant)
} else {
switch position {
case .top, .left:
staticConstraint = position.mainDimensionAnchor(surfaceView).constraint(equalToConstant: position(for: self.directionalMostState))
case .bottom, .right:
staticConstraint = position.mainDimensionAnchor(vc.view).constraint(equalTo: position.mainDimensionAnchor(surfaceView),
constant: position(for: self.directionalLeastState))
}
}
switch position {
case .top, .bottom:
staticConstraint?.identifier = "FloatingPanel-static-height"
case .left, .right:
staticConstraint?.identifier = "FloatingPanel-static-width"
}
NSLayoutConstraint.activate(constraint: staticConstraint)
surfaceView.containerOverflow = position.mainDimension(vc.view.bounds.size)
}
func updateInteractiveEdgeConstraint(diff: CGFloat, overflow: Bool, allowsRubberBanding: (UIRectEdge) -> Bool) {
defer {
log.debug("update surface location = \(surfaceLocation)")
}
let minConst: CGFloat = position(for: directionalLeastState)
let maxConst: CGFloat = position(for: directionalMostState)
var const = initialConst + diff
let base = position.mainDimension(vc.view.bounds.size)
// Rubber-banding top buffer
if allowsRubberBanding(.top), const < minConst {
let buffer = minConst - const
const = minConst - rubberBandEffect(for: buffer, base: base)
}
// Rubber-banding bottom buffer
if allowsRubberBanding(.bottom), const > maxConst {
let buffer = const - maxConst
const = maxConst + rubberBandEffect(for: buffer, base: base)
}
if overflow == false {
const = min(max(const, minConst), maxConst)
}
interactionConstraint?.constant = 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(for state: FloatingPanelState, forceLayout: Bool = false) {
defer {
if forceLayout {
layoutSurfaceIfNeeded()
log.debug("activateLayout for \(state) -- surface.presentation = \(self.surfaceView.presentationFrame) surface.frame = \(self.surfaceView.frame)")
} else {
log.debug("activateLayout for \(state)")
}
}
// Must deactivate `interactiveTopConstraint` here
NSLayoutConstraint.deactivate(constraint: self.interactionConstraint)
self.interactionConstraint = nil
tearDownAttraction()
NSLayoutConstraint.activate(fixedConstraints)
if vc.contentMode == .fitToBounds {
NSLayoutConstraint.activate(constraint: self.fitToBoundsConstraint)
}
var state = state
setBackdropAlpha(of: state)
if validStates.contains(state) == false {
state = layout.initialState
}
// Recalculate the intrinsic size of a content view. This is because
// UIView.systemLayoutSizeFitting() returns a different size between an
// on-screen and off-screen view which includes
// UIStackView(i.e. Settings view in Samples.app)
updateStateConstraints()
switch state {
case .full:
NSLayoutConstraint.activate(fullConstraints)
case .half:
NSLayoutConstraint.activate(halfConstraints)
case .tip:
NSLayoutConstraint.activate(tipConstraints)
case .hidden:
NSLayoutConstraint.activate(offConstraints)
default:
break
}
}
private func layoutSurfaceIfNeeded() {
#if !TEST
guard surfaceView.window != nil else { return }
#endif
surfaceView.superview?.layoutIfNeeded()
}
private func setBackdropAlpha(of target: FloatingPanelState) {
if target == .hidden {
self.backdropView.alpha = 0.0
} else {
self.backdropView.alpha = backdropAlpha(for: target)
}
}
func backdropAlpha(for state: FloatingPanelState) -> CGFloat {
return layout.backdropAlpha?(for: state) ?? defaultLayout.backdropAlpha(for: state)
}
fileprivate func checkLayout() {
// Verify layout configurations
assert(activeStates.count > 0)
assert(validStates.contains(layout.initialState),
"Does not include an initial state (\(layout.initialState)) in (\(validStates))")
/* This assertion does not work in a device rotating
let statePosOrder = activeStates.sorted(by: { position(for: $0) < position(for: $1) })
assert(sortedDirectionalStates == statePosOrder,
"Check your layout anchors because the state order(\(statePosOrder)) must be (\(sortedDirectionalStates))).")
*/
}
}
extension LayoutAdapter {
func segment(at pos: CGFloat, forward: Bool) -> LayoutSegment {
/// ----------------------->Y
/// --> forward <-- backward
/// |-------|===o===|-------| |-------|-------|===o===|
/// |-------|-------x=======| |-------|=======x-------|
/// |-------|-------|===o===| |-------|===o===|-------|
/// pos: o/x, segment: =
let sortedStates = sortedDirectionalStates
let upperIndex: Int?
if forward {
upperIndex = sortedStates.firstIndex(where: { pos < position(for: $0) })
} else {
upperIndex = sortedStates.firstIndex(where: { pos <= position(for: $0) })
}
switch upperIndex {
case 0:
return LayoutSegment(lower: nil, upper: sortedStates.first)
case let upperIndex?:
return LayoutSegment(lower: sortedStates[upperIndex - 1], upper: sortedStates[upperIndex])
default:
return LayoutSegment(lower: sortedStates[sortedStates.endIndex - 1], upper: nil)
}
}
}
extension FloatingPanelController {
var _layout: FloatingPanelLayout {
get {
floatingPanel.layoutAdapter.layout
}
set {
floatingPanel.layoutAdapter.layout = newValue
floatingPanel.layoutAdapter.checkLayout()
}
}
}
+138
View File
@@ -0,0 +1,138 @@
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
import UIKit
/// An interface for implementing custom layout anchor objects.
@objc public protocol FloatingPanelLayoutAnchoring {
var referenceGuide: FloatingPanelLayoutReferenceGuide { get }
func layoutConstraints(_ fpc: FloatingPanelController, for position: FloatingPanelPosition) -> [NSLayoutConstraint]
}
/// A layout anchor object that anchors a panel in a state.
@objc final public class FloatingPanelLayoutAnchor: NSObject, FloatingPanelLayoutAnchoring /*, NSCopying */ {
/// Initializes and returns a layout anchor object to specify an absolute inset value for the position of a panel.
///
/// The inset is a distance from the edge of the specified layout guide.
@objc public init(absoluteInset: CGFloat, edge: FloatingPanelReferenceEdge, referenceGuide: FloatingPanelLayoutReferenceGuide) {
self.inset = absoluteInset
self.referenceGuide = referenceGuide
self.referenceEdge = edge
self.isAbsolute = true
}
/// Initializes and returns a layout anchor object to specify a fractional inset value for the position of a panel.
///
/// The inset is a distance from the edge of the specified layout guide. The value is a floating-point number
/// in the range 0.0 to 1.0, where 0.0 represents zero distance from the edge and 1.0 represents a distance
/// to the opposite edge.
@objc public init(fractionalInset: CGFloat, edge: FloatingPanelReferenceEdge, referenceGuide: FloatingPanelLayoutReferenceGuide) {
self.inset = fractionalInset
self.referenceGuide = referenceGuide
self.referenceEdge = edge
self.isAbsolute = false
}
let inset: CGFloat
let isAbsolute: Bool
/// The reference rectangle area for the inset.
@objc public let referenceGuide: FloatingPanelLayoutReferenceGuide
@objc let referenceEdge: FloatingPanelReferenceEdge
}
public extension FloatingPanelLayoutAnchor {
func layoutConstraints(_ vc: FloatingPanelController, for position: FloatingPanelPosition) -> [NSLayoutConstraint] {
let layoutGuide = referenceGuide.layoutGuide(vc: vc)
switch position {
case .top:
return layoutConstraints(layoutGuide, for: vc.surfaceView.bottomAnchor)
case .left:
return layoutConstraints(layoutGuide, for: vc.surfaceView.rightAnchor)
case .bottom:
return layoutConstraints(layoutGuide, for: vc.surfaceView.topAnchor)
case .right:
return layoutConstraints(layoutGuide, for: vc.surfaceView.leftAnchor)
}
}
private func layoutConstraints(_ layoutGuide: LayoutGuideProvider, for edgeAnchor: NSLayoutYAxisAnchor) -> [NSLayoutConstraint] {
switch referenceEdge {
case .top:
if isAbsolute {
return [edgeAnchor.constraint(equalTo: layoutGuide.topAnchor, constant: inset)]
}
let offsetAnchor = layoutGuide.topAnchor.anchorWithOffset(to: edgeAnchor)
return [offsetAnchor.constraint(equalTo:layoutGuide.heightAnchor, multiplier: inset)]
case .bottom:
if isAbsolute {
return [layoutGuide.bottomAnchor.constraint(equalTo: edgeAnchor, constant: inset)]
}
let offsetAnchor = edgeAnchor.anchorWithOffset(to: layoutGuide.bottomAnchor)
return [offsetAnchor.constraint(equalTo: layoutGuide.heightAnchor, multiplier: inset)]
default:
fatalError("Unsupported reference edges")
}
}
private func layoutConstraints(_ layoutGuide: LayoutGuideProvider, for edgeAnchor: NSLayoutXAxisAnchor) -> [NSLayoutConstraint] {
switch referenceEdge {
case .left:
if isAbsolute {
return [edgeAnchor.constraint(equalTo: layoutGuide.leftAnchor, constant: inset)]
}
let offsetAnchor = layoutGuide.leftAnchor.anchorWithOffset(to: edgeAnchor)
return [offsetAnchor.constraint(equalTo: layoutGuide.widthAnchor, multiplier: inset)]
case .right:
if isAbsolute {
return [layoutGuide.rightAnchor.constraint(equalTo: edgeAnchor, constant: inset)]
}
let offsetAnchor = edgeAnchor.anchorWithOffset(to: layoutGuide.rightAnchor)
return [offsetAnchor.constraint(equalTo: layoutGuide.widthAnchor, multiplier: inset)]
default:
fatalError("Unsupported reference edges")
}
}
}
/// A layout anchor object that anchors a panel in a state using the intrinsic size for a content.
@objc final public class FloatingPanelIntrinsicLayoutAnchor: NSObject, FloatingPanelLayoutAnchoring /*, NSCopying */ {
/// Initializes and returns a layout anchor object to specify an absolute offset value for the position of a panel.
///
/// The offset is a distance from a position at which a panel displays the entire content.
@objc public init(absoluteOffset offset: CGFloat, referenceGuide: FloatingPanelLayoutReferenceGuide = .safeArea) {
self.offset = offset
self.referenceGuide = referenceGuide
self.isAbsolute = true
}
/// Initializes and returns a layout anchor object to specify a fractional offset value for the position of a panel.
///
/// The offset value is a floating-point number in the range 0.0 to 1.0, where 0.0 represents the full content
/// is displayed and 0.5 represents the half of content is displayed.
@objc public init(fractionalOffset offset: CGFloat, referenceGuide: FloatingPanelLayoutReferenceGuide = .safeArea) {
self.offset = offset
self.referenceGuide = referenceGuide
self.isAbsolute = false
}
let offset: CGFloat
let isAbsolute: Bool
/// The reference rectangle area for the offset
@objc public let referenceGuide: FloatingPanelLayoutReferenceGuide
}
public extension FloatingPanelIntrinsicLayoutAnchor {
func layoutConstraints(_ vc: FloatingPanelController, for position: FloatingPanelPosition) -> [NSLayoutConstraint] {
let surfaceIntrinsicLength = position.mainDimension(vc.surfaceView.intrinsicContentSize)
let constant = isAbsolute ? surfaceIntrinsicLength - offset : surfaceIntrinsicLength * (1 - offset)
let layoutGuide = referenceGuide.layoutGuide(vc: vc)
switch position {
case .top:
return [vc.surfaceView.bottomAnchor.constraint(equalTo: layoutGuide.topAnchor, constant: constant)]
case .left:
return [vc.surfaceView.rightAnchor.constraint(equalTo: layoutGuide.leftAnchor, constant: constant)]
case .bottom:
return [vc.surfaceView.topAnchor.constraint(equalTo: layoutGuide.bottomAnchor, constant: -constant)]
case .right:
return [vc.surfaceView.leftAnchor.constraint(equalTo: layoutGuide.rightAnchor, constant: -constant)]
}
}
}
+45
View File
@@ -0,0 +1,45 @@
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
import UIKit
/// Constants that specify the edge of the container of a panel.
@objc public enum FloatingPanelReferenceEdge: Int {
case top
case left
case bottom
case right
}
extension FloatingPanelReferenceEdge {
func inset(of insets: UIEdgeInsets) -> CGFloat {
switch self {
case .top: return insets.top
case .left: return insets.left
case .bottom: return insets.bottom
case .right: return insets.right
}
}
func mainDimension(_ size: CGSize) -> CGFloat {
switch self {
case .top, .bottom: return size.height
case .left, .right: return size.width
}
}
}
/// Constants that specify a layout guide to lay out a panel.
@objc public enum FloatingPanelLayoutReferenceGuide: Int {
case superview = 0
case safeArea = 1
}
extension FloatingPanelLayoutReferenceGuide {
func layoutGuide(vc: UIViewController) -> LayoutGuideProvider {
switch self {
case .safeArea:
return vc.fp_safeAreaLayoutGuide
case .superview:
return vc.view
}
}
}
@@ -1,7 +1,4 @@
//
// Created by Shin Yamamoto on 2018/10/09.
// Copyright © 2018 Shin Yamamoto. All rights reserved.
//
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
import Foundation
import os.log
@@ -23,9 +20,9 @@ struct Logger {
var displayName: String {
switch self {
case .debug:
return "D/"
return "Debug:"
case .info:
return "I/"
return "Info:"
case .warning:
return "Warning:"
case .error:
@@ -54,17 +51,18 @@ struct Logger {
osLog = OSLog(subsystem: "com.scenee.FloatingPanel", category: "FloatingPanel")
}
private func log(_ level: Level, _ message: Any, _ arguments: [Any], function: String, line: UInt) {
private func log(_ level: Level, _ message: Any, _ arguments: [Any], tag: String, function: String, line: UInt) {
_ = s.wait(timeout: .now() + 0.033)
defer { s.signal() }
let extraMessage: String = arguments.map({ String(describing: $0) }).joined(separator: " ")
let _tag = tag.isEmpty ? "" : "\(tag):"
let log: String = {
switch level {
case .debug:
return "\(level.displayName) \(message) \(extraMessage) (\(function):\(line))"
return "\(level.displayName)\(_tag) \(message) \(extraMessage) (\(function):\(line))"
default:
return "\(level.displayName) \(message) \(extraMessage)"
return "\(level.displayName)\(_tag) \(message) \(extraMessage)"
}
}()
@@ -81,21 +79,21 @@ struct Logger {
}
}
func debug(_ log: Any, _ arguments: Any..., function: String = #function, file: String = #file, line: UInt = #line) {
func debug(_ log: Any, _ arguments: Any..., tag: String = "", function: String = #function, file: String = #file, line: UInt = #line) {
#if __FP_LOG
self.log(.debug, log, arguments, function: getPrettyFunction(function, file), line: line)
self.log(.debug, log, arguments, tag: tag, function: getPrettyFunction(function, file), line: line)
#endif
}
func info(_ log: Any, _ arguments: Any..., function: String = #function, file: String = #file, line: UInt = #line) {
self.log(.info, log, arguments, function: getPrettyFunction(function, file), line: line)
func info(_ log: Any, _ arguments: Any..., tag: String = "", function: String = #function, file: String = #file, line: UInt = #line) {
self.log(.info, log, arguments, tag: tag, function: getPrettyFunction(function, file), line: line)
}
func warning(_ log: Any, _ arguments: Any..., function: String = #function, file: String = #file, line: UInt = #line) {
self.log(.warning, log, arguments, function: getPrettyFunction(function, file), line: line)
self.log(.warning, log, arguments, tag: "", function: getPrettyFunction(function, file), line: line)
}
func error(_ log: Any, _ arguments: Any..., function: String = #function, file: String = #file, line: UInt = #line) {
self.log(.error, log, arguments, function: getPrettyFunction(function, file), line: line)
self.log(.error, log, arguments, tag: "", function: getPrettyFunction(function, file), line: line)
}
}
@@ -1,11 +1,9 @@
//
// Created by Shin Yamamoto on 2018/11/21.
// Copyright © 2018 Shin Yamamoto. All rights reserved.
//
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
import UIKit
class FloatingPanelPassThroughView: UIView {
@objc(FloatingPanelPassThroughView)
class PassThroughView: UIView {
public weak var eventForwardingView: UIView?
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let hitView = super.hitTest(point, with: event)
+50
View File
@@ -0,0 +1,50 @@
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
import UIKit
/// Constants describing the position of a panel in a screen
@objc public enum FloatingPanelPosition: Int {
case top
case left
case bottom
case right
}
extension FloatingPanelPosition {
func mainLocation(_ point: CGPoint) -> CGFloat {
switch self {
case .top, .bottom: return point.y
case .left, .right: return point.x
}
}
func mainDimension(_ size: CGSize) -> CGFloat {
switch self {
case .top, .bottom: return size.height
case .left, .right: return size.width
}
}
func mainDimensionAnchor(_ layoutGuide: LayoutGuideProvider) -> NSLayoutDimension {
switch self {
case .top, .bottom: return layoutGuide.heightAnchor
case .left, .right: return layoutGuide.widthAnchor
}
}
func crossDimension(_ size: CGSize) -> CGFloat {
switch self {
case .top, .bottom: return size.width
case .left, .right: return size.height
}
}
func inset(_ insets: UIEdgeInsets) -> CGFloat {
switch self {
case .top: return insets.top
case .left: return insets.left
case .bottom: return insets.bottom
case .right: return insets.right
}
}
}
+63
View File
@@ -0,0 +1,63 @@
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
import Foundation
/// An object that represents the display state of a panel in a screen.
@objc
public class FloatingPanelState: NSObject, NSCopying, RawRepresentable {
public typealias RawValue = String
required public init?(rawValue: RawValue) {
self.order = 0
self.rawValue = rawValue
super.init()
}
public init(rawValue: RawValue, order: Int) {
self.rawValue = rawValue
self.order = order
super.init()
}
/// The corresponding value of the raw type.
public let rawValue: RawValue
/// The sorting order for states
public let order: Int
public func copy(with zone: NSZone? = nil) -> Any {
return self
}
public override var description: String {
return rawValue
}
public override var debugDescription: String {
return description
}
/// A panel state indicates the entire panel is shown.
@objc(Full) public static let full: FloatingPanelState = FloatingPanelState(rawValue: "full", order: 1000)
/// A panel state indicates the half of a panel is shown.
@objc(Half) public static let half: FloatingPanelState = FloatingPanelState(rawValue: "half", order: 500)
/// A panel state indicates the tip of a panel is shown.
@objc(Tip) public static let tip: FloatingPanelState = FloatingPanelState(rawValue: "tip", order: 100)
/// A panel state indicates it is hidden.
@objc(Hidden) public static let hidden: FloatingPanelState = FloatingPanelState(rawValue: "hidden", order: 0)
}
extension FloatingPanelState {
func next(in states: [FloatingPanelState]) -> FloatingPanelState {
if let index = states.firstIndex(of: self), states.indices.contains(index + 1) {
return states[index + 1]
}
return self
}
func pre(in states: [FloatingPanelState]) -> FloatingPanelState {
if let index = states.firstIndex(of: self), states.indices.contains(index - 1) {
return states[index - 1]
}
return self
}
}
+435
View File
@@ -0,0 +1,435 @@
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
import UIKit
/// An object for customizing the appearance of a surface view
@objc(FloatingPanelSurfaceAppearance)
@objcMembers
public class SurfaceAppearance: NSObject {
/// An object that represents information to render a shadow
@objc(FloatingPanelSurfaceAppearanceShadow)
public class Shadow: NSObject {
/// A Boolean indicating whether a shadow is displayed.
@objc
public var hidden: Bool = false
/// The color of a shadow.
@objc
public var color: UIColor = .black
/// The offset (in points) of a shadow.
@objc
public var offset: CGSize = CGSize(width: 0.0, height: 1.0)
/// The opacity of a shadow.
@objc
public var opacity: Float = 0.2
/// The blur radius (in points) used to render a shadow.
@objc
public var radius: CGFloat = 3
/// The inflated amount of a shadow prior to applying the blur.
@objc
public var spread: CGFloat = 0
}
/// The background color of a surface view
public var backgroundColor: UIColor? = {
if #available(iOS 13, *) {
return UIColor.systemBackground
} else {
return UIColor.white
}
}()
/// The radius to use when drawing the top rounded corners.
///
/// `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
/// An array of shadows used to create drop shadows underneath a surface view.
public var shadows: [Shadow] = [Shadow()]
/// The border width of a surface view.
public var borderColor: UIColor?
/// The border color of a surface view.
public var borderWidth: CGFloat = 0.0
}
/// A view that presents a surface interface in a panel.
@objc(FloatingPanelSurfaceView)
@objcMembers
public class SurfaceView: UIView {
/// A `FloatingPanelGrabberView` object displayed at the top of the surface view.
///
/// To use a custom grabber, hide this and then add it to the surface view at appropriate point.
public let grabberHandle = GrabberView()
/// Offset of the grabber handle from the interactive edge.
public var grabberHandlePadding: CGFloat = 6.0 { didSet {
setNeedsUpdateConstraints()
} }
/// The offset from the move edge to prevent the content scroll
public var grabberAreaOffset: CGFloat = 36.0
/// The grabber handle size
///
/// On left/right positioned panel the width dimension is used as the height of `grabberHandle`, and vice versa.
public var grabberHandleSize: CGSize = CGSize(width: 36.0, height: 5.0) { didSet {
setNeedsUpdateConstraints()
} }
/// The content view to be assigned a view of the content view controller of `FloatingPanelController`
public weak var contentView: UIView?
/// The content insets specifying the insets around the content view.
public var contentPadding: UIEdgeInsets = .zero {
didSet {
// Needs update constraints
self.setNeedsUpdateConstraints()
}
}
public override var backgroundColor: UIColor? {
get { return appearance.backgroundColor }
set { appearance.backgroundColor = newValue; setNeedsLayout() }
}
/// The appearance settings for a surface view.
public var appearance = SurfaceAppearance() { didSet {
shadowLayers = appearance.shadows.map { _ in CAShapeLayer() }
setNeedsLayout()
}}
/// The margins to use when laying out the container view wrapping content.
public var containerMargins: UIEdgeInsets = .zero { didSet {
setNeedsUpdateConstraints()
} }
/// The view that displays 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()
var containerOverflow: CGFloat = 0.0 { // Must not call setNeedsLayout()
didSet {
// Calling setNeedsUpdateConstraints() is necessary to fix a layout break
// when the contentMode is changed after laying out a panel, for instance,
// after calling viewDidAppear(_:) of the parent view controller.
setNeedsUpdateConstraints()
}
}
var position: FloatingPanelPosition = .bottom {
didSet {
guard position != oldValue else { return }
NSLayoutConstraint.deactivate([grabberHandleEdgePaddingConstraint,
grabberHandleCenterConstraint,
grabberHandleWidthConstraint,
grabberHandleHeightConstraint])
switch position {
case .top:
grabberHandleEdgePaddingConstraint = grabberHandle.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -grabberHandlePadding)
grabberHandleCenterConstraint = grabberHandle.centerXAnchor.constraint(equalTo: centerXAnchor)
grabberHandleWidthConstraint = grabberHandle.widthAnchor.constraint(equalToConstant: grabberHandleSize.width)
grabberHandleHeightConstraint = grabberHandle.heightAnchor.constraint(equalToConstant: grabberHandleSize.height)
case .left:
grabberHandleEdgePaddingConstraint = grabberHandle.rightAnchor.constraint(equalTo: rightAnchor, constant: -grabberHandlePadding)
grabberHandleCenterConstraint = grabberHandle.centerYAnchor.constraint(equalTo: centerYAnchor)
grabberHandleWidthConstraint = grabberHandle.widthAnchor.constraint(equalToConstant: grabberHandleSize.height)
grabberHandleHeightConstraint = grabberHandle.heightAnchor.constraint(equalToConstant: grabberHandleSize.width)
case .bottom:
grabberHandleEdgePaddingConstraint = grabberHandle.topAnchor.constraint(equalTo: topAnchor, constant: grabberHandlePadding)
grabberHandleCenterConstraint = grabberHandle.centerXAnchor.constraint(equalTo: centerXAnchor)
grabberHandleWidthConstraint = grabberHandle.widthAnchor.constraint(equalToConstant: grabberHandleSize.width)
grabberHandleHeightConstraint = grabberHandle.heightAnchor.constraint(equalToConstant: grabberHandleSize.height)
case .right:
grabberHandleEdgePaddingConstraint = grabberHandle.leftAnchor.constraint(equalTo: leftAnchor, constant: grabberHandlePadding)
grabberHandleCenterConstraint = grabberHandle.centerYAnchor.constraint(equalTo: centerYAnchor)
grabberHandleWidthConstraint = grabberHandle.widthAnchor.constraint(equalToConstant: grabberHandleSize.height)
grabberHandleHeightConstraint = grabberHandle.heightAnchor.constraint(equalToConstant: grabberHandleSize.width)
}
NSLayoutConstraint.activate([grabberHandleEdgePaddingConstraint,
grabberHandleCenterConstraint,
grabberHandleWidthConstraint,
grabberHandleHeightConstraint])
setNeedsUpdateConstraints()
}
}
var grabberAreaFrame: CGRect {
switch position {
case .top:
return CGRect(origin: .init(x: bounds.minX, y: bounds.maxY - grabberAreaOffset),
size: .init(width: bounds.width, height: grabberAreaOffset))
case .left:
return CGRect(origin: .init(x: bounds.maxX - grabberAreaOffset, y: bounds.minY),
size: .init(width: grabberAreaOffset, height: bounds.height))
case .bottom:
return CGRect(origin: CGPoint(x: bounds.minX, y: bounds.minY),
size: CGSize(width: bounds.width, height: grabberAreaOffset))
case .right:
return CGRect(origin: .init(x: bounds.minX, y: bounds.minY),
size: .init(width: grabberAreaOffset, height: bounds.height))
}
}
private lazy var containerViewTopConstraint = containerView.topAnchor.constraint(equalTo: topAnchor, constant: 0.0)
private lazy var containerViewLeftConstraint = containerView.leftAnchor.constraint(equalTo: leftAnchor, constant: 0.0)
private lazy var containerViewBottomConstraint = containerView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 0.0)
private lazy var containerViewRightConstraint = containerView.rightAnchor.constraint(equalTo: rightAnchor, constant: 0.0)
/// The content view's top constraint
private var contentViewTopConstraint: NSLayoutConstraint?
/// The content view's left constraint
private var contentViewLeftConstraint: NSLayoutConstraint?
/// The content view's right constraint
private var contentViewRightConstraint: NSLayoutConstraint?
/// The content view's bottom constraint
private var contentViewBottomConstraint: NSLayoutConstraint?
private lazy var grabberHandleWidthConstraint = grabberHandle.widthAnchor.constraint(equalToConstant: grabberHandleSize.width)
private lazy var grabberHandleHeightConstraint = grabberHandle.heightAnchor.constraint(equalToConstant: grabberHandleSize.height)
private lazy var grabberHandleCenterConstraint = grabberHandle.centerXAnchor.constraint(equalTo: centerXAnchor)
private lazy var grabberHandleEdgePaddingConstraint = grabberHandle.topAnchor.constraint(equalTo: topAnchor, constant: grabberHandlePadding)
private var shadowLayers: [CALayer] = [] {
willSet {
for shadowLayer in shadowLayers {
shadowLayer.removeFromSuperlayer()
}
}
didSet {
for shadowLayer in shadowLayers {
layer.insertSublayer(shadowLayer, at: 0)
}
}
}
public override class var requiresConstraintBasedLayout: Bool { return true }
override init(frame: CGRect) {
super.init(frame: frame)
addSubViews()
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
addSubViews()
}
private func addSubViews() {
super.backgroundColor = .clear
self.clipsToBounds = false
addSubview(containerView)
containerView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
containerViewTopConstraint,
containerViewLeftConstraint,
containerViewBottomConstraint,
containerViewRightConstraint,
].map {
$0.identifier = "FloatingPanel-surface-container"
return $0;
})
addSubview(grabberHandle)
grabberHandle.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
grabberHandleEdgePaddingConstraint,
grabberHandleCenterConstraint,
grabberHandleWidthConstraint,
grabberHandleHeightConstraint,
].map {
$0.identifier = "FloatingPanel-surface-grabber"
return $0;
})
shadowLayers = appearance.shadows.map { _ in CALayer() }
}
public override func updateConstraints() {
switch position {
case .top:
containerViewTopConstraint.constant = (containerMargins.top == 0) ? -containerOverflow : containerMargins.top
containerViewLeftConstraint.constant = containerMargins.left
containerViewRightConstraint.constant = -containerMargins.right
containerViewBottomConstraint.constant = -containerMargins.bottom
case .left:
containerViewTopConstraint.constant = containerMargins.top
containerViewLeftConstraint.constant = (containerMargins.left == 0) ? -containerOverflow : containerMargins.left
containerViewRightConstraint.constant = -containerMargins.right
containerViewBottomConstraint.constant = -containerMargins.bottom
case .bottom:
containerViewTopConstraint.constant = containerMargins.top
containerViewLeftConstraint.constant = containerMargins.left
containerViewRightConstraint.constant = -containerMargins.right
containerViewBottomConstraint.constant = (containerMargins.bottom == 0) ? containerOverflow : -containerMargins.bottom
case .right:
containerViewTopConstraint.constant = containerMargins.top
containerViewLeftConstraint.constant = containerMargins.left
containerViewRightConstraint.constant = (containerMargins.right == 0) ? containerOverflow : -containerMargins.right
containerViewBottomConstraint.constant = -containerMargins.bottom
}
contentViewTopConstraint?.constant = containerMargins.top + contentPadding.top
contentViewLeftConstraint?.constant = containerMargins.left + contentPadding.left
contentViewRightConstraint?.constant = containerMargins.right + contentPadding.right
contentViewBottomConstraint?.constant = containerMargins.bottom + contentPadding.bottom
switch position {
case .top, .left:
grabberHandleEdgePaddingConstraint.constant = -grabberHandlePadding
case .bottom, .right:
grabberHandleEdgePaddingConstraint.constant = grabberHandlePadding
}
switch position {
case .top, .bottom:
grabberHandleWidthConstraint.constant = grabberHandleSize.width
grabberHandleHeightConstraint.constant = grabberHandleSize.height
case .left, .right:
grabberHandleWidthConstraint.constant = grabberHandleSize.height
grabberHandleHeightConstraint.constant = grabberHandleSize.width
}
super.updateConstraints()
}
public override func layoutSubviews() {
super.layoutSubviews()
log.debug("surface view frame = \(frame)")
containerView.backgroundColor = appearance.backgroundColor
updateShadow()
updateCornerRadius()
updateBorder()
grabberHandle.layer.cornerRadius = grabberHandleSize.height / 2
}
public override var intrinsicContentSize: CGSize {
let fittingSize = UIView.layoutFittingCompressedSize
let contentSize = contentView?.systemLayoutSizeFitting(fittingSize) ?? .zero
return CGSize(width: containerMargins.horizontalInset + contentPadding.horizontalInset + contentSize.width,
height: containerMargins.verticalInset + contentPadding.verticalInset + contentSize.height)
}
private func updateShadow() {
// Disable shadow animation when the surface's frame jumps to a new value.
CATransaction.begin()
CATransaction.setDisableActions(true)
for (i, shadow) in appearance.shadows.enumerated() {
let shadowLayer = shadowLayers[i]
shadowLayer.backgroundColor = UIColor.clear.cgColor
shadowLayer.frame = layer.bounds
let spread = shadow.spread
let shadowPath = UIBezierPath(roundedRect: containerView.frame.insetBy(dx: -spread,
dy: -spread),
byRoundingCorners: [.allCorners],
cornerRadii: CGSize(width: appearance.cornerRadius, height: 0))
shadowLayer.shadowPath = shadowPath.cgPath
shadowLayer.shadowColor = shadow.color.cgColor
shadowLayer.shadowOffset = shadow.offset
// A shadow.radius value isn't manipulated by a scale(i.e. the display scale). It should be applied to the value by itself.
shadowLayer.shadowRadius = shadow.radius
shadowLayer.shadowOpacity = shadow.opacity
let mask = CAShapeLayer()
let path = UIBezierPath(roundedRect: containerView.frame,
byRoundingCorners: [.allCorners],
cornerRadii: CGSize(width: appearance.cornerRadius, height: 0))
let size = window?.bounds.size ?? CGSize(width: 1000.0, height: 1000.0)
path.append(UIBezierPath(rect: layer.bounds.insetBy(dx: -size.width,
dy: -size.height)))
mask.fillRule = .evenOdd
mask.path = path.cgPath
if #available(iOS 13.0, *) {
mask.cornerCurve = containerView.layer.cornerCurve
}
shadowLayer.mask = mask
}
CATransaction.commit()
}
private func updateCornerRadius() {
containerView.layer.cornerRadius = appearance.cornerRadius
guard containerView.layer.cornerRadius != 0.0 else {
containerView.layer.masksToBounds = false
return
}
containerView.layer.masksToBounds = true
if position.inset(containerMargins) != 0 {
if #available(iOS 11, *) {
containerView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner,
.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
}
return
}
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.
switch position {
case .top:
containerView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
case .left:
containerView.layer.maskedCorners = [.layerMaxXMinYCorner, .layerMaxXMaxYCorner]
case .bottom:
containerView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
case .right:
containerView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMinXMaxYCorner]
}
} else {
// 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 = appearance.borderColor?.cgColor
containerView.layer.borderWidth = appearance.borderWidth
}
func set(contentView: UIView) {
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: containerMargins.top + contentPadding.top)
let leftConstraint = contentView.leftAnchor.constraint(equalTo: leftAnchor, constant: containerMargins.left + contentPadding.left)
let rightConstraint = rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: containerMargins.right + contentPadding.right)
let bottomConstraint = bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: containerMargins.bottom + contentPadding.bottom)
NSLayoutConstraint.activate([
topConstraint,
leftConstraint,
rightConstraint,
bottomConstraint,
].map {
$0.priority = .required - 1;
$0.identifier = "FloatingPanel-surface-content"
return $0;
})
self.contentViewTopConstraint = topConstraint
self.contentViewLeftConstraint = leftConstraint
self.contentViewRightConstraint = rightConstraint
self.contentViewBottomConstraint = bottomConstraint
}
func hasStackView() -> Bool {
return contentView?.subviews.reduce(false) { $0 || ($1 is UIStackView) } ?? false
}
}
@@ -1,36 +1,33 @@
//
// Created by Shin Yamamoto on 2018/11/21.
// Copyright © 2018 Shin Yamamoto. All rights reserved.
//
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
import UIKit
class FloatingPanelModalTransition: NSObject, UIViewControllerTransitioningDelegate {
class ModalTransition: NSObject, UIViewControllerTransitioningDelegate {
func animationController(forPresented presented: UIViewController,
presenting: UIViewController,
source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return FloatingPanelModalPresentTransition()
return ModalPresentTransition()
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return FloatingPanelModalDismissTransition()
return ModalDismissTransition()
}
func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
return FloatingPanelPresentationController(presentedViewController: presented, presenting: presenting)
return PresentationController(presentedViewController: presented, presenting: presenting)
}
}
class FloatingPanelPresentationController: UIPresentationController {
class PresentationController: UIPresentationController {
override func presentationTransitionWillBegin() {
// Must call here even if duplicating on in containerViewWillLayoutSubviews()
// Because it let the floating panel present correctly with the presentation animation
// Because it let the panel present correctly with the presentation animation
addFloatingPanel()
}
override func presentationTransitionDidEnd(_ completed: Bool) {
// For non-animated presentation
if let fpc = presentedViewController as? FloatingPanelController, fpc.position == .hidden {
if let fpc = presentedViewController as? FloatingPanelController, fpc.state == .hidden {
fpc.show(animated: false, completion: nil)
}
}
@@ -38,7 +35,7 @@ class FloatingPanelPresentationController: UIPresentationController {
override func dismissalTransitionDidEnd(_ completed: Bool) {
if let fpc = presentedViewController as? FloatingPanelController {
// For non-animated dismissal
if fpc.position != .hidden {
if fpc.state != .hidden {
fpc.hide(animated: false, completion: nil)
}
fpc.view.removeFromSuperview()
@@ -47,8 +44,15 @@ class FloatingPanelPresentationController: UIPresentationController {
override func containerViewWillLayoutSubviews() {
guard
let fpc = presentedViewController as? FloatingPanelController
else { fatalError() }
let fpc = presentedViewController as? FloatingPanelController,
/**
This condition fixes https://github.com/SCENEE/FloatingPanel/issues/369.
The issue is that this method is called in presenting a
UIImagePickerViewController and then a FloatingPanelController
view is added unnecessarily.
*/
fpc.presentedViewController == nil
else { return }
/*
* Layout the views managed by `FloatingPanelController` here for the
@@ -57,11 +61,7 @@ class FloatingPanelPresentationController: UIPresentationController {
addFloatingPanel()
// Forward touch events to the presenting view controller
(fpc.view as? FloatingPanelPassThroughView)?.eventForwardingView = presentingViewController.view
// Set tap-to-dismiss in the backdrop view
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleBackdrop(tapGesture:)))
fpc.backdropView.addGestureRecognizer(tapGesture)
(fpc.view as? PassThroughView)?.eventForwardingView = presentingViewController.view
}
@objc func handleBackdrop(tapGesture: UITapGestureRecognizer) {
@@ -80,13 +80,14 @@ class FloatingPanelPresentationController: UIPresentationController {
}
}
class FloatingPanelModalPresentTransition: NSObject, UIViewControllerAnimatedTransitioning {
class ModalPresentTransition: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
guard
let fpc = transitionContext?.viewController(forKey: .to) as? FloatingPanelController
else { fatalError()}
let animator = fpc.behavior.addAnimator(fpc, to: fpc.layout.initialPosition)
let animator = fpc.delegate?.floatingPanel?(fpc, animatorForPresentingTo: fpc.layout.initialState)
?? FloatingPanelDefaultBehavior().addPanelAnimator(fpc, to: fpc.layout.initialState)
return TimeInterval(animator.duration)
}
@@ -101,13 +102,14 @@ class FloatingPanelModalPresentTransition: NSObject, UIViewControllerAnimatedTra
}
}
class FloatingPanelModalDismissTransition: NSObject, UIViewControllerAnimatedTransitioning {
class ModalDismissTransition: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
guard
let fpc = transitionContext?.viewController(forKey: .from) as? FloatingPanelController
else { fatalError()}
let animator = fpc.behavior.removeAnimator(fpc, from: fpc.position)
let animator = fpc.delegate?.floatingPanel?(fpc, animatorForDismissingWith: .zero)
?? FloatingPanelDefaultBehavior().removePanelAnimator(fpc, from: fpc.state, with: .zero)
return TimeInterval(animator.duration)
}
+203
View File
@@ -0,0 +1,203 @@
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
import UIKit
internal func displayTrunc(_ v: CGFloat, by s: CGFloat) -> CGFloat {
let base = (1 / s)
let t = v.rounded(.down)
return t + ((v - t) / base).rounded(.toNearestOrAwayFromZero) * base
}
internal func displayEqual(_ lhs: CGFloat, _ rhs: CGFloat, by displayScale: CGFloat) -> Bool {
return displayTrunc(lhs, by: displayScale) == displayTrunc(rhs, by: displayScale)
}
protocol LayoutGuideProvider {
var topAnchor: NSLayoutYAxisAnchor { get }
var leftAnchor: NSLayoutXAxisAnchor { get }
var bottomAnchor: NSLayoutYAxisAnchor { get }
var rightAnchor: NSLayoutXAxisAnchor { get }
var widthAnchor: NSLayoutDimension { get }
var heightAnchor: NSLayoutDimension { get }
}
extension UILayoutGuide: LayoutGuideProvider {}
extension UIView: LayoutGuideProvider {}
private class CustomLayoutGuide: LayoutGuideProvider {
let topAnchor: NSLayoutYAxisAnchor
let leftAnchor: NSLayoutXAxisAnchor
let bottomAnchor: NSLayoutYAxisAnchor
let rightAnchor: NSLayoutXAxisAnchor
let widthAnchor: NSLayoutDimension
let heightAnchor: NSLayoutDimension
init(topAnchor: NSLayoutYAxisAnchor,
leftAnchor: NSLayoutXAxisAnchor,
bottomAnchor: NSLayoutYAxisAnchor,
rightAnchor: NSLayoutXAxisAnchor,
widthAnchor: NSLayoutDimension,
heightAnchor: NSLayoutDimension) {
self.topAnchor = topAnchor
self.leftAnchor = leftAnchor
self.bottomAnchor = bottomAnchor
self.rightAnchor = rightAnchor
self.widthAnchor = widthAnchor
self.heightAnchor = heightAnchor
}
}
extension UIViewController {
@objc var fp_safeAreaInsets: UIEdgeInsets {
if #available(iOS 11.0, *) {
return view.safeAreaInsets
} else {
return UIEdgeInsets(top: topLayoutGuide.length,
left: 0.0,
bottom: bottomLayoutGuide.length,
right: 0.0)
}
}
var fp_safeAreaLayoutGuide: LayoutGuideProvider {
if #available(iOS 11.0, *) {
return view!.safeAreaLayoutGuide
} else {
return CustomLayoutGuide(topAnchor: topLayoutGuide.bottomAnchor,
leftAnchor: view.leftAnchor,
bottomAnchor: bottomLayoutGuide.topAnchor,
rightAnchor: view.rightAnchor,
widthAnchor: view.widthAnchor,
heightAnchor: topLayoutGuide.bottomAnchor.anchorWithOffset(to: bottomLayoutGuide.topAnchor))
}
}
}
// The reason why UIView has no extensions of safe area insets and top/bottom guides
// is for iOS10 compatibility.
extension UIView {
var fp_safeAreaLayoutGuide: LayoutGuideProvider {
if #available(iOS 11.0, *) {
return safeAreaLayoutGuide
} else {
return self
}
}
var presentationFrame: CGRect {
return layer.presentation()?.frame ?? frame
}
/// Returns non-zero displayScale
///
/// On iOS 11 or earlier the `traitCollection.displayScale` of a view can be
/// 0.0(indicating unspecified) when its view hasn't been added yet into a view tree in a window.
/// So this method returns `UIScreen.main` scale if the scale value is zero, for testing mainly.
var fp_displayScale: CGFloat {
let ret = traitCollection.displayScale
if ret == 0.0 {
return UIScreen.main.scale
}
return ret
}
}
extension UIView {
func disableAutoLayout() {
let frame = self.frame
translatesAutoresizingMaskIntoConstraints = true
self.frame = frame
}
func enableAutoLayout() {
translatesAutoresizingMaskIntoConstraints = false
}
static func performWithLinear(startTime: Double = 0.0, relativeDuration: Double = 1.0, _ animations: @escaping (() -> Void)) {
UIView.animateKeyframes(withDuration: 0.0, delay: 0.0, options: [.calculationModeCubic], animations: {
UIView.addKeyframe(withRelativeStartTime: startTime, relativeDuration: relativeDuration, animations: animations)
}, completion: nil)
}
}
#if __FP_LOG
extension UIGestureRecognizer.State: CustomDebugStringConvertible {
public var debugDescription: String {
switch self {
case .began: return "began"
case .changed: return "changed"
case .failed: return "failed"
case .cancelled: return "cancelled"
case .ended: return "ended"
case .possible: return "possible"
@unknown default: return ""
}
}
}
#endif
extension UIScrollView {
var isLocked: Bool {
return !showsVerticalScrollIndicator && !bounces && isDirectionalLockEnabled
}
var fp_contentInset: UIEdgeInsets {
if #available(iOS 11.0, *) {
return adjustedContentInset
} else {
return contentInset
}
}
var fp_contentOffsetMax: CGPoint {
return CGPoint(x: max((contentSize.width + fp_contentInset.right) - bounds.width, 0.0),
y: max((contentSize.height + fp_contentInset.bottom) - bounds.height, 0.0))
}
}
extension UISpringTimingParameters {
public convenience init(decelerationRate: CGFloat, frequencyResponse: CGFloat, initialVelocity: CGVector = .zero) {
let dampingRatio = CoreGraphics.log(decelerationRate) / (-4 * .pi * 0.001)
self.init(dampingRatio: dampingRatio, frequencyResponse: frequencyResponse, initialVelocity: initialVelocity)
}
public convenience init(dampingRatio: CGFloat, frequencyResponse: CGFloat, initialVelocity: CGVector = .zero) {
let mass = 1 as CGFloat
let stiffness = pow(2 * .pi / frequencyResponse, 2) * mass
let damp = 4 * .pi * dampingRatio * mass / frequencyResponse
self.init(mass: mass, stiffness: stiffness, damping: damp, initialVelocity: initialVelocity)
}
}
extension CGPoint {
static var leastNonzeroMagnitude: CGPoint {
return CGPoint(x: CGFloat.leastNonzeroMagnitude, y: CGFloat.leastNonzeroMagnitude)
}
static func + (lhs: CGPoint, rhs: CGPoint) -> CGPoint {
return CGPoint(x: lhs.x + rhs.x, y: lhs.y + rhs.y)
}
static func - (lhs: CGPoint, rhs: CGPoint) -> CGPoint {
return CGPoint(x: lhs.x - rhs.x, y: lhs.y - rhs.y)
}
static prefix func - (point: CGPoint) -> CGPoint {
return CGPoint(x: -point.x, y: -point.y)
}
}
extension NSLayoutConstraint {
static func activate(constraint: NSLayoutConstraint?) {
guard let constraint = constraint else { return }
self.activate([constraint])
}
static func deactivate(constraint: NSLayoutConstraint?) {
guard let constraint = constraint else { return }
self.deactivate([constraint])
}
}
extension UIEdgeInsets {
var horizontalInset: CGFloat {
return self.left + self.right
}
var verticalInset: CGFloat {
return self.top + self.bottom
}
}
+352
View File
@@ -0,0 +1,352 @@
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
import XCTest
@testable import FloatingPanel
class ControllerTests: XCTestCase {
override func setUp() {}
override func tearDown() {}
func test_warningRetainCycle() {
let exp = expectation(description: "Warning retain cycle")
exp.expectedFulfillmentCount = 2 // For layout & behavior logs
log.hook = {(log, level) in
if log.contains("A memory leak will occur by a retain cycle because") {
XCTAssert(level == .warning)
exp.fulfill()
}
}
let myVC = MyZombieViewController(nibName: nil, bundle: nil)
myVC.loadViewIfNeeded()
wait(for: [exp], timeout: 10)
}
func test_addPanel() {
guard let rootVC = UIApplication.shared.keyWindow?.rootViewController else { fatalError() }
let fpc = FloatingPanelController()
fpc.addPanel(toParent: rootVC)
XCTAssertEqual(fpc.surfaceLocation.y, fpc.surfaceLocation(for: .half).y)
fpc.move(to: .tip, animated: false)
XCTAssertEqual(fpc.surfaceLocation.y, fpc.surfaceLocation(for: .tip).y)
}
@available(iOS 12.0, *)
func test_updateLayout_willTransition() {
class MyDelegate: FloatingPanelControllerDelegate {
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout {
if newCollection.userInterfaceStyle == .dark {
XCTFail()
}
return FloatingPanelBottomLayout()
}
}
let myDelegate = MyDelegate()
let fpc = FloatingPanelController(delegate: myDelegate)
let traitCollection = UITraitCollection(traitsFrom: [fpc.traitCollection,
UITraitCollection(userInterfaceStyle: .dark)])
XCTAssertEqual(traitCollection.userInterfaceStyle, .dark)
}
func test_moveTo() {
let delegate = FloatingPanelTestDelegate()
let fpc = FloatingPanelController(delegate: delegate)
XCTAssertEqual(delegate.position, .hidden)
fpc.showForTest()
XCTAssertEqual(delegate.position, .half)
fpc.hide()
XCTAssertEqual(delegate.position, .hidden)
fpc.move(to: .full, animated: false)
XCTAssertEqual(fpc.state, .full)
XCTAssertEqual(delegate.position, .full)
XCTAssertEqual(fpc.surfaceLocation.y, fpc.surfaceLocation(for: .full).y)
fpc.move(to: .half, animated: false)
XCTAssertEqual(fpc.state, .half)
XCTAssertEqual(delegate.position, .half)
XCTAssertEqual(fpc.surfaceLocation, fpc.surfaceLocation(for: .half))
fpc.move(to: .tip, animated: false)
XCTAssertEqual(fpc.state, .tip)
XCTAssertEqual(delegate.position, .tip)
XCTAssertEqual(fpc.surfaceLocation.y, fpc.surfaceLocation(for: .tip).y)
fpc.move(to: .hidden, animated: false)
XCTAssertEqual(fpc.state, .hidden)
XCTAssertEqual(delegate.position, .hidden)
XCTAssertEqual(fpc.surfaceLocation.y, fpc.surfaceLocation(for: .hidden).y)
XCTContext.runActivity(named: "move to full(animated)") { act in
let exp = expectation(description: act.name)
fpc.move(to: .full, animated: true) {
XCTAssertEqual(fpc.surfaceLocation.y, fpc.surfaceLocation(for: .full).y)
exp.fulfill()
}
XCTAssertEqual(fpc.state, .full)
XCTAssertEqual(delegate.position, .full)
wait(for: [exp], timeout: 1.0)
}
XCTContext.runActivity(named: "move to half(animated)") { act in
let exp = expectation(description: act.name)
fpc.move(to: .half, animated: true) {
XCTAssertEqual(fpc.surfaceLocation, fpc.surfaceLocation(for: .half))
exp.fulfill()
}
XCTAssertEqual(fpc.state, .half)
XCTAssertEqual(delegate.position, .half)
wait(for: [exp], timeout: 1.0)
}
XCTContext.runActivity(named: "move to tip(animated)") { act in
let exp = expectation(description: act.name)
fpc.move(to: .tip, animated: true) {
XCTAssertEqual(fpc.surfaceLocation.y, fpc.surfaceLocation(for: .tip).y)
exp.fulfill()
}
XCTAssertEqual(fpc.state, .tip)
XCTAssertEqual(delegate.position, .tip)
wait(for: [exp], timeout: 1.0)
}
fpc.move(to: .hidden, animated: true)
XCTAssertEqual(fpc.state, .hidden)
XCTAssertEqual(delegate.position, .hidden)
XCTAssertEqual(fpc.surfaceLocation.y, fpc.surfaceLocation(for: .hidden).y)
}
func test_moveTo_bottomEdge() {
class MyFloatingPanelTop2BottomLayout: FloatingPanelTop2BottomTestLayout {
override var initialState: FloatingPanelState { return .half }
}
let delegate = FloatingPanelTestDelegate()
let fpc = FloatingPanelController(delegate: delegate)
fpc.layout = MyFloatingPanelTop2BottomLayout()
XCTAssertEqual(delegate.position, .hidden)
fpc.showForTest()
XCTAssertEqual(delegate.position, .half)
fpc.hide()
XCTAssertEqual(delegate.position, .hidden)
fpc.move(to: .full, animated: false)
XCTAssertEqual(fpc.state, .full)
XCTAssertEqual(delegate.position, .full)
XCTAssertEqual(fpc.surfaceLocation.y, fpc.surfaceLocation(for: .full).y)
fpc.move(to: .half, animated: false)
XCTAssertEqual(fpc.state, .half)
XCTAssertEqual(delegate.position, .half)
XCTAssertEqual(fpc.surfaceLocation.y, fpc.surfaceLocation(for: .half).y)
fpc.move(to: .tip, animated: false)
XCTAssertEqual(fpc.state, .tip)
XCTAssertEqual(delegate.position, .tip)
XCTAssertEqual(fpc.surfaceLocation.y, fpc.surfaceLocation(for: .tip).y)
fpc.move(to: .hidden, animated: false)
XCTAssertEqual(fpc.state, .hidden)
XCTAssertEqual(delegate.position, .hidden)
XCTAssertEqual(fpc.surfaceLocation.y, fpc.surfaceLocation(for: .hidden).y)
XCTContext.runActivity(named: "move to full(animated)") { act in
let exp = expectation(description: act.name)
fpc.move(to: .full, animated: true) {
XCTAssertEqual(fpc.surfaceLocation.y, fpc.surfaceLocation(for: .full).y)
exp.fulfill()
}
XCTAssertEqual(fpc.state, .full)
XCTAssertEqual(delegate.position, .full)
wait(for: [exp], timeout: 1.0)
}
XCTContext.runActivity(named: "move to half(animated)") { act in
let exp = expectation(description: act.name)
fpc.move(to: .half, animated: true) {
XCTAssertEqual(fpc.surfaceLocation, fpc.surfaceLocation(for: .half))
exp.fulfill()
}
XCTAssertEqual(fpc.state, .half)
XCTAssertEqual(delegate.position, .half)
wait(for: [exp], timeout: 1.0)
}
XCTContext.runActivity(named: "move to tip(animated)") { act in
let exp = expectation(description: act.name)
fpc.move(to: .tip, animated: true) {
XCTAssertEqual(fpc.surfaceLocation.y, fpc.surfaceLocation(for: .tip).y)
exp.fulfill()
}
XCTAssertEqual(fpc.state, .tip)
XCTAssertEqual(delegate.position, .tip)
wait(for: [exp], timeout: 1.0)
}
fpc.move(to: .hidden, animated: true)
XCTAssertEqual(fpc.state, .hidden)
XCTAssertEqual(delegate.position, .hidden)
XCTAssertEqual(fpc.surfaceLocation.y, fpc.surfaceLocation(for: .hidden).y)
}
func test_moveWithNearbyPosition() {
let delegate = FloatingPanelTestDelegate()
let fpc = FloatingPanelController(delegate: delegate)
XCTAssertEqual(delegate.position, .hidden)
fpc.showForTest()
XCTAssertEqual(fpc.nearbyState, .half)
fpc.hide()
XCTAssertEqual(fpc.nearbyState, .tip)
fpc.move(to: .full, animated: false)
XCTAssertEqual(fpc.nearbyState, .full)
XCTAssertEqual(fpc.surfaceView.frame.minY, fpc.surfaceLocation(for: .full).y)
}
func test_moveTo_didMoveDelegate() {
let delegate = FloatingPanelTestDelegate()
let fpc = FloatingPanelController(delegate: delegate)
XCTAssertEqual(delegate.position, .hidden)
fpc.showForTest()
XCTContext.runActivity(named: "move(to:animated:false") { act in
let exp = expectation(description: act.name)
exp.expectedFulfillmentCount = 1
var count = 0
delegate.didMoveCallback = { _ in
count += 1
exp.fulfill()
}
fpc.move(to: .full, animated: false)
wait(for: [exp], timeout: 1.0)
XCTAssertEqual(count, 1)
}
XCTContext.runActivity(named: "move(to:animated:true)") { act in
let exp = expectation(description: act.name)
exp.assertForOverFulfill = false
exp.expectedFulfillmentCount = 1
var count = 0
delegate.didMoveCallback = { _ in
count += 1
}
fpc.move(to: .half, animated: true) {
exp.fulfill()
}
wait(for: [exp], timeout: 1.0)
XCTAssertGreaterThan(count, 1)
}
XCTContext.runActivity(named: "move(to:animated:false) with animation") { act in
let exp = expectation(description: act.name)
exp.expectedFulfillmentCount = 1
var count = 0
delegate.didMoveCallback = { _ in
count += 1
}
UIView.animate(withDuration: 0.3) {
fpc.move(to: .full, animated: false) {
exp.fulfill()
}
}
wait(for: [exp], timeout: 1.0)
XCTAssertEqual(count, 1)
}
XCTContext.runActivity(named: "move(to:animated:true) with animation") { act in
let exp = expectation(description: act.name)
exp.assertForOverFulfill = false
exp.expectedFulfillmentCount = 1
var count = 0
delegate.didMoveCallback = { _ in
count += 1
}
UIView.animate(withDuration: 0.3) {
fpc.move(to: .half, animated: true) {
exp.fulfill()
}
}
wait(for: [exp], timeout: 1.0)
XCTAssertGreaterThan(count, 1)
}
}
func test_originSurfaceY() {
let fpc = FloatingPanelController(delegate: nil)
fpc.loadViewIfNeeded()
fpc.view.frame = CGRect(x: 0, y: 0, width: 375, height: 667)
fpc.show(animated: false, completion: nil)
fpc.move(to: .full, animated: false)
XCTAssertEqual(fpc.surfaceLocation, fpc.surfaceLocation(for: .full))
fpc.move(to: .half, animated: false)
XCTAssertEqual(fpc.surfaceLocation, fpc.surfaceLocation(for: .half))
fpc.move(to: .tip, animated: false)
XCTAssertEqual(fpc.surfaceLocation, fpc.surfaceLocation(for: .tip))
fpc.move(to: .hidden, animated: false)
XCTAssertEqual(fpc.surfaceLocation, fpc.surfaceLocation(for: .hidden))
}
func test_contentMode() {
let fpc = FloatingPanelController(delegate: nil)
fpc.loadViewIfNeeded()
fpc.view.frame = CGRect(x: 0, y: 0, width: 375, height: 667)
fpc.show(animated: false, completion: nil)
fpc.contentMode = .static
fpc.move(to: .full, animated: false)
XCTAssertEqual(fpc.surfaceView.frame.height, fpc.view.bounds.height - fpc.surfaceLocation(for: .full).y)
fpc.move(to: .half, animated: false)
XCTAssertEqual(fpc.surfaceView.frame.height, fpc.view.bounds.height - fpc.surfaceLocation(for: .full).y)
fpc.move(to: .tip, animated: false)
XCTAssertEqual(fpc.surfaceView.frame.height, fpc.view.bounds.height - fpc.surfaceLocation(for: .full).y)
fpc.contentMode = .fitToBounds
fpc.move(to: .full, animated: false)
XCTAssertEqual(fpc.surfaceView.frame.height, fpc.view.bounds.height - fpc.surfaceLocation(for: .full).y)
fpc.move(to: .half, animated: false)
print(1 / fpc.surfaceView.fp_displayScale)
XCTAssertEqual(fpc.surfaceView.frame.height, fpc.view.bounds.height - fpc.surfaceLocation(for: .half).y)
fpc.move(to: .tip, animated: false)
XCTAssertEqual(fpc.surfaceView.frame.height, fpc.view.bounds.height - fpc.surfaceLocation(for: .tip).y)
}
}
private class MyZombieViewController: UIViewController, FloatingPanelLayout, FloatingPanelBehavior, FloatingPanelControllerDelegate {
var fpc: FloatingPanelController?
override func viewDidLoad() {
fpc = FloatingPanelController(delegate: self)
fpc?.addPanel(toParent: self)
fpc?.layout = self
fpc?.behavior = self
}
var position: FloatingPanelPosition {
return .bottom
}
var initialState: FloatingPanelState {
return .half
}
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
return [
.full: FloatingPanelLayoutAnchor(absoluteInset: UIScreen.main.bounds.height == 667.0 ? 18.0 : 16.0,
edge: .top,
referenceGuide: .superview),
.half: FloatingPanelLayoutAnchor(absoluteInset: 250.0,
edge: .bottom,
referenceGuide: .superview),
.tip: FloatingPanelLayoutAnchor(absoluteInset: 60.0,
edge: .bottom,
referenceGuide: .superview),
]
}
}
@@ -1,12 +1,9 @@
//
// Created by Shin Yamamoto on 2019/05/23.
// Copyright © 2019 Shin Yamamoto. All rights reserved.
//
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
import XCTest
@testable import FloatingPanel
class FloatingPanelTests: XCTestCase {
class CoreTests: XCTestCase {
override func setUp() {}
override func tearDown() {}
@@ -20,7 +17,7 @@ class FloatingPanelTests: XCTestCase {
fpc.track(scrollView: contentVC1.tableView)
fpc.showForTest()
XCTAssertEqual(fpc.position, .half)
XCTAssertEqual(fpc.state, .half)
XCTAssertEqual(contentVC1.tableView.showsVerticalScrollIndicator, false)
XCTAssertEqual(contentVC1.tableView.bounces, false)
@@ -55,112 +52,155 @@ class FloatingPanelTests: XCTestCase {
fpc.set(contentViewController: contentVC2)
fpc.track(scrollView: contentVC2.tableView)
fpc.show(animated: false, completion: nil)
XCTAssertEqual(fpc.position, .half)
XCTAssertEqual(fpc.state, .half)
XCTAssertEqual(contentVC2.tableView.showsVerticalScrollIndicator, false)
XCTAssertEqual(contentVC2.tableView.bounces, false)
}
func test_getBackdropAlpha_1positions() {
class FloatingPanelLayout1Positions: FloatingPanelTestLayout {
let initialPosition: FloatingPanelPosition = .full
let supportedPositions: Set<FloatingPanelPosition> = [.full]
class FloatingPanelLayout1Positions: FloatingPanelLayout {
let initialState: FloatingPanelState = .full
let position: FloatingPanelPosition = .bottom
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
return [.full: FloatingPanelLayoutAnchor(absoluteInset: 20.0, edge: .top, referenceGuide: .superview)]
}
}
let delegate = FloatingPanelTestDelegate()
delegate.layout = FloatingPanelLayout1Positions()
let delegate = FloatingPanelTestDelegate()
let fpc = FloatingPanelController(delegate: delegate)
fpc.layout = FloatingPanelLayout1Positions()
fpc.showForTest()
let fullPos = fpc.originYOfSurface(for: .full)
let fullPos = fpc.surfaceLocation(for: .full).y
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: fullPos - 100.0, with: CGPoint(x: 0.0, y: -100.0)), 0.3)
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: fullPos, with: CGPoint(x: 0.0, y: 0.0)), 0.3)
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: fullPos + 100.0, with: CGPoint(x: 0.0, y: 100.0)), 0.3) // ok??
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: fullPos - 100.0, with: -100.0), 0.3)
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: fullPos, with: 0), 0.3)
XCTAssertLessThan(fpc.floatingPanel.getBackdropAlpha(at: fullPos + 100.0, with: 100.0), 0.3)
}
func test_getBackdropAlpha_1positionsWithInitialHidden() {
class FloatingPanelLayout2Positions: FloatingPanelTestLayout {
override var initialState: FloatingPanelState { .hidden }
override var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
return [
.full: FloatingPanelLayoutAnchor(absoluteInset: fullInset, edge: .top, referenceGuide: referenceGuide),
]
}
}
let delegate = FloatingPanelTestDelegate()
let fpc = FloatingPanelController(delegate: delegate)
fpc.layout = FloatingPanelLayout2Positions()
fpc.showForTest()
let fullPos = fpc.surfaceLocation(for: .full).y
let hiddenPos = fpc.surfaceLocation(for: .hidden).y
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: fullPos - 100.0, with: -100.0), 0.3)
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: fullPos, with: 0.0), 0.3)
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: hiddenPos, with: 100.0), 0.0)
}
func test_getBackdropAlpha_2positions() {
class FloatingPanelLayout2Positions: FloatingPanelTestLayout {
let initialPosition: FloatingPanelPosition = .half
let supportedPositions: Set<FloatingPanelPosition> = [.half, .full]
class FloatingPanelLayout2Positions: FloatingPanelLayout {
let initialState: FloatingPanelState = .half
let position: FloatingPanelPosition = .bottom
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
return [
.full: FloatingPanelLayoutAnchor(absoluteInset: 20.0, edge: .top, referenceGuide: .superview),
.half: FloatingPanelLayoutAnchor(absoluteInset: 250.0, edge: .bottom, referenceGuide: .superview),
]
}
}
let delegate = FloatingPanelTestDelegate()
delegate.layout = FloatingPanelLayout2Positions()
let delegate = FloatingPanelTestDelegate()
let fpc = FloatingPanelController(delegate: delegate)
fpc.layout = FloatingPanelLayout2Positions()
fpc.showForTest()
let fullPos = fpc.originYOfSurface(for: .full)
let halfPos = fpc.originYOfSurface(for: .half)
let fullPos = fpc.surfaceLocation(for: .full).y
let halfPos = fpc.surfaceLocation(for: .half).y
let distance1 = abs(halfPos - fullPos)
fpc.move(to: .full, animated: false)
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: fullPos, with: CGPoint(x: 0.0, y: 0.0)), 0.3)
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: fullPos + distance1 * 0.5, with: CGPoint(x: 0.0, y: distance1 * 0.5)), 0.3 * 0.5)
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: halfPos, with: CGPoint(x: 0.0, y: distance1)), 0.0)
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: fullPos, with: 0.0), 0.3)
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: fullPos + distance1 * 0.5, with: distance1), 0.3 * 0.5)
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: halfPos, with: distance1), 0.0)
fpc.move(to: .half, animated: false)
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: halfPos, with: CGPoint(x: 0.0, y: 0.0)), 0.0)
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: fullPos + distance1 * 0.5, with: CGPoint(x: 0.0, y: -0.5 * distance1)), 0.3 * 0.5)
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: fullPos, with: CGPoint(x: 0.0, y: -1 * distance1)), 0.3)
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: halfPos, with: 0.0), 0.0)
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: fullPos + distance1 * 0.5, with: -0.5 * distance1), 0.3 * 0.5)
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: fullPos, with: -1 * distance1), 0.3)
}
func test_getBackdropAlpha_2positionsWithHidden() {
class FloatingPanelLayout2Positions: FloatingPanelTestLayout {
let initialPosition: FloatingPanelPosition = .hidden
let supportedPositions: Set<FloatingPanelPosition> = [.hidden, .full]
override var initialState: FloatingPanelState { .hidden }
override var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
return [
.full: FloatingPanelLayoutAnchor(absoluteInset: fullInset, edge: .top, referenceGuide: referenceGuide),
.hidden: FloatingPanelLayoutAnchor(absoluteInset: 0.0, edge: .bottom, referenceGuide: referenceGuide),
]
}
}
let delegate = FloatingPanelTestDelegate()
delegate.layout = FloatingPanelLayout2Positions()
let fpc = FloatingPanelController(delegate: delegate)
fpc.layout = FloatingPanelLayout2Positions()
fpc.showForTest()
let fullPos = fpc.originYOfSurface(for: .full)
let hiddenPos = fpc.originYOfSurface(for: .hidden)
let fullPos = fpc.surfaceLocation(for: .full).y
let hiddenPos = fpc.surfaceLocation(for: .hidden).y
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: fullPos - 100.0, with: CGPoint(x: 0.0, y: -100.0)), 0.3)
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: fullPos, with: CGPoint(x: 0.0, y: 0.0)), 0.3)
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: hiddenPos, with: CGPoint(x: 0.0, y: 100.0)), 0.0)
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: fullPos - 100.0, with: -100.0), 0.3)
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: fullPos, with: 0.0), 0.3)
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: hiddenPos, with: 100.0), 0.0)
}
func test_getBackdropAlpha_3positions() {
let fpc = FloatingPanelController()
fpc.showForTest()
let fullPos = fpc.originYOfSurface(for: .full)
let halfPos = fpc.originYOfSurface(for: .half)
let tipPos = fpc.originYOfSurface(for: .tip)
let fullPos = fpc.surfaceLocation(for: .full).y
let halfPos = fpc.surfaceLocation(for: .half).y
let tipPos = fpc.surfaceLocation(for: .tip).y
let distance1 = abs(halfPos - fullPos)
let distance2 = abs(tipPos - halfPos)
fpc.move(to: .full, animated: false)
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: fullPos, with: CGPoint(x: 0.0, y: 0.0)), 0.3)
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: fullPos + distance1 * 0.5, with: CGPoint(x: 0.0, y: distance1 * 0.5)), 0.3 * 0.5)
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: halfPos, with: CGPoint(x: 0.0, y: distance1)), 0.0)
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: fullPos, with: 0.0), 0.3)
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: fullPos + distance1 * 0.5, with: distance1 * 0.5), 0.3 * 0.5)
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: halfPos, with: distance1), 0.0)
fpc.move(to: .half, animated: false)
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: halfPos, with: CGPoint(x: 0.0, y: 0.0)), 0.0)
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: fullPos + distance1 * 0.5, with: CGPoint(x: 0.0, y: -0.5 * distance1)), 0.3 * 0.5)
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: fullPos, with: CGPoint(x: 0.0, y: -1 * distance1)), 0.3)
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: halfPos, with: 0.0), 0.0)
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: fullPos + distance1 * 0.5, with: -0.5 * distance1), 0.3 * 0.5)
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: fullPos, with: -1 * distance1), 0.3)
fpc.move(to: .tip, animated: false)
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: tipPos, with: CGPoint(x: 0.0, y: 0.0)), 0.0)
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: halfPos + distance2 * 0.5, with: CGPoint(x: 0.0, y: -0.5 * distance2)), 0.0)
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: halfPos, with: CGPoint(x: 0.0, y: -1 * distance2)), 0.0)
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: tipPos, with: 0.0), 0.0)
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: halfPos + distance2 * 0.5, with: -0.5 * distance2), 0.0)
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: halfPos, with: -1 * distance2), 0.0)
}
func test_targetPosition_1positions() {
class FloatingPanelLayout1Positions: FloatingPanelTestLayout {
let initialPosition: FloatingPanelPosition = .full
let supportedPositions: Set<FloatingPanelPosition> = [.full]
class FloatingPanelLayout1Positions: FloatingPanelLayout {
let initialState: FloatingPanelState = .full
let position: FloatingPanelPosition = .bottom
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
return [.full: FloatingPanelLayoutAnchor(absoluteInset: 20.0, edge: .top, referenceGuide: .superview)]
}
}
let delegate = FloatingPanelTestDelegate()
delegate.layout = FloatingPanelLayout1Positions()
let delegate = FloatingPanelTestDelegate()
let fpc = FloatingPanelController(delegate: delegate)
fpc.layout = FloatingPanelLayout1Positions()
fpc.showForTest()
let fullPos = fpc.originYOfSurface(for: .full)
let fullPos = fpc.surfaceLocation(for: .full).y
fpc.move(to: .full, animated: false)
assertTargetPosition(fpc.floatingPanel, with: [
@@ -175,18 +215,25 @@ class FloatingPanelTests: XCTestCase {
}
func test_targetPosition_2positions() {
class FloatingPanelLayout2Positions: FloatingPanelTestLayout {
let initialPosition: FloatingPanelPosition = .half
let supportedPositions: Set<FloatingPanelPosition> = [.half, .full]
class FloatingPanelLayout2Positions: FloatingPanelLayout {
let initialState: FloatingPanelState = .half
let position: FloatingPanelPosition = .bottom
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
return [
.full: FloatingPanelLayoutAnchor(absoluteInset: 20.0, edge: .top, referenceGuide: .superview),
.half: FloatingPanelLayoutAnchor(absoluteInset: 250.0, edge: .bottom, referenceGuide: .superview),
]
}
}
let delegate = FloatingPanelTestDelegate()
delegate.layout = FloatingPanelLayout2Positions()
let delegate = FloatingPanelTestDelegate()
let fpc = FloatingPanelController(delegate: delegate)
fpc.layout = FloatingPanelLayout2Positions()
fpc.showForTest()
let fullPos = fpc.originYOfSurface(for: .full)
let halfPos = fpc.originYOfSurface(for: .half)
let fullPos = fpc.surfaceLocation(for: .full).y
let halfPos = fpc.surfaceLocation(for: .half).y
fpc.move(to: .full, animated: false)
assertTargetPosition(fpc.floatingPanel, with: [
@@ -225,18 +272,25 @@ class FloatingPanelTests: XCTestCase {
}
func test_targetPosition_2positionsWithHidden() {
class FloatingPanelLayout2Positions: FloatingPanelTestLayout {
let initialPosition: FloatingPanelPosition = .hidden
let supportedPositions: Set<FloatingPanelPosition> = [.hidden, .full]
class FloatingPanelLayout2Positions: FloatingPanelLayout {
let initialState: FloatingPanelState = .hidden
let position: FloatingPanelPosition = .bottom
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
return [
.full: FloatingPanelLayoutAnchor(absoluteInset: 20.0, edge: .top, referenceGuide: .superview),
.hidden: FloatingPanelLayoutAnchor(absoluteInset: 0, edge: .bottom, referenceGuide: .superview),
]
}
}
let delegate = FloatingPanelTestDelegate()
delegate.layout = FloatingPanelLayout2Positions()
let delegate = FloatingPanelTestDelegate()
let fpc = FloatingPanelController(delegate: delegate)
fpc.layout = FloatingPanelLayout2Positions()
fpc.showForTest()
let fullPos = fpc.originYOfSurface(for: .full)
let hiddenPos = fpc.originYOfSurface(for: .hidden)
let fullPos = fpc.surfaceLocation(for: .full).y
let hiddenPos = fpc.surfaceLocation(for: .hidden).y
fpc.move(to: .full, animated: false)
assertTargetPosition(fpc.floatingPanel, with: [
@@ -274,15 +328,16 @@ class FloatingPanelTests: XCTestCase {
])
}
func test_targetPosition_2positionsFromFull() {
func test_targetPosition_3positionsFromFull() {
let delegate = FloatingPanelTestDelegate()
delegate.layout = FloatingPanelLayout3Positions()
let fpc = FloatingPanelController(delegate: delegate)
fpc.layout = FloatingPanelLayout3Positions()
fpc.showForTest()
let fullPos = fpc.originYOfSurface(for: .full)
let halfPos = fpc.originYOfSurface(for: .half)
let tipPos = fpc.originYOfSurface(for: .tip)
let fullPos = fpc.surfaceLocation(for: .full).y
let halfPos = fpc.surfaceLocation(for: .half).y
let tipPos = fpc.surfaceLocation(for: .tip).y
// From .full
fpc.move(to: .full, animated: false)
assertTargetPosition(fpc.floatingPanel, with: [
@@ -314,18 +369,63 @@ class FloatingPanelTests: XCTestCase {
(#line, tipPos + 500.0, CGPoint(x: 0.0, y: -100.0), .tip), // far from bottomMostState
(#line, tipPos + 500.0, CGPoint(x: 0.0, y: 0.0), .tip), // far from bottomMostState
(#line, tipPos + 500.0, CGPoint(x: 0.0, y: 100.0), .tip), // far from bottomMostState
])
])
}
func test_targetPosition_3positionsFromFull_bottomEdge() {
let delegate = FloatingPanelTestDelegate()
let fpc = FloatingPanelController(delegate: delegate)
fpc.layout = FloatingPanelLayout3PositionsBottomEdge()
fpc.showForTest()
let fullPos = fpc.surfaceLocation(for: .full).y
let halfPos = fpc.surfaceLocation(for: .half).y
let tipPos = fpc.surfaceLocation(for: .tip).y
// From .full
fpc.move(to: .full, animated: false)
assertTargetPosition(fpc.floatingPanel, with: [
(#line, tipPos - 500.0, CGPoint(x: 0.0, y: -100.0), .tip), // far from topMostState
(#line, tipPos - 500.0, CGPoint(x: 0.0, y: 0.0), .tip), // far from topMostState
(#line, tipPos - 500.0, CGPoint(x: 0.0, y: 100.0), .tip), // far from topMostState
(#line, tipPos - 10.0, CGPoint(x: 0.0, y: 3000.0), .half), // block projecting to full at half
(#line, tipPos, CGPoint(x: 0.0, y: -100.0), .tip),
(#line, tipPos, CGPoint(x: 0.0, y: 0.0), .tip),
(#line, tipPos, CGPoint(x: 0.0, y: 500.0), .half), // project to half
(#line, tipPos, CGPoint(x: 0.0, y: 1000.0), .half), // block projecting to full at half
(#line, tipPos, CGPoint(x: 0.0, y: 3000.0), .half), // block projecting to full at half
(#line, tipPos + 10.0, CGPoint(x: 0.0, y: 100.0), .tip), // redirect
(#line, halfPos - 10.0, CGPoint(x: 0.0, y: -100.0), .half), // redirect
(#line, halfPos, CGPoint(x: 0.0, y: -1000.0), .tip), //project to tip
(#line, halfPos, CGPoint(x: 0.0, y: -100.0), .half),
(#line, halfPos, CGPoint(x: 0.0, y: 0.0), .half),
(#line, halfPos, CGPoint(x: 0.0, y: 100.0), .half),
(#line, halfPos, CGPoint(x: 0.0, y: 1000.0), .full), // project to full
(#line, halfPos + 10.0, CGPoint(x: 0.0, y: 100.0), .half), // redirect
(#line, fullPos - 10.0, CGPoint(x: 0.0, y: -100.0), .full), // redirect
(#line, fullPos, CGPoint(x: 0.0, y: -3000.0), .half), // block projecting to tip at half
(#line, fullPos, CGPoint(x: 0.0, y: -1000.0), .half), // block projecting to tip at half
(#line, fullPos, CGPoint(x: 0.0, y: -500.0), .half), // project to half
(#line, fullPos, CGPoint(x: 0.0, y: -100.0), .full),
(#line, fullPos, CGPoint(x: 0.0, y: 0.0), .full),
(#line, fullPos, CGPoint(x: 0.0, y: 100.0), .full),
(#line, fullPos + 10.0, CGPoint(x: 0.0, y: -3000.0), .half), // block projecting to tip at half
(#line, fullPos + 500.0, CGPoint(x: 0.0, y: -100.0), .full), // far from bottomMostState
(#line, fullPos + 500.0, CGPoint(x: 0.0, y: 0.0), .full), // far from bottomMostState
(#line, fullPos + 500.0, CGPoint(x: 0.0, y: 100.0), .full), // far from bottomMostState
])
}
func test_targetPosition_3positionsFromHalf() {
let delegate = FloatingPanelTestDelegate()
delegate.layout = FloatingPanelLayout3Positions()
let fpc = FloatingPanelController(delegate: delegate)
fpc.layout = FloatingPanelLayout3Positions()
fpc.showForTest()
let fullPos = fpc.originYOfSurface(for: .full)
let halfPos = fpc.originYOfSurface(for: .half)
let tipPos = fpc.originYOfSurface(for: .tip)
let fullPos = fpc.surfaceLocation(for: .full).y
let halfPos = fpc.surfaceLocation(for: .half).y
let tipPos = fpc.surfaceLocation(for: .tip).y
// From .half
fpc.move(to: .half, animated: false)
assertTargetPosition(fpc.floatingPanel, with: [
@@ -355,18 +455,61 @@ class FloatingPanelTests: XCTestCase {
(#line, tipPos + 500.0, CGPoint(x: 0.0, y: -100.0), .tip), // far from bottomMostState
(#line, tipPos + 500.0, CGPoint(x: 0.0, y: 0.0), .tip), // far from bottomMostState
(#line, tipPos + 500.0, CGPoint(x: 0.0, y: 100.0), .tip), // far from bottomMostState
])
}
func test_targetPosition_3positionsFromHalf_bottomEdge() {
let delegate = FloatingPanelTestDelegate()
let fpc = FloatingPanelController(delegate: delegate)
fpc.layout = FloatingPanelLayout3PositionsBottomEdge()
fpc.showForTest()
let fullPos = fpc.surfaceLocation(for: .full).y
let halfPos = fpc.surfaceLocation(for: .half).y
let tipPos = fpc.surfaceLocation(for: .tip).y
// From .half
fpc.move(to: .half, animated: false)
assertTargetPosition(fpc.floatingPanel, with: [
(#line, tipPos - 500.0, CGPoint(x: 0.0, y: -100.0), .tip), // far from topMostState
(#line, tipPos - 500.0, CGPoint(x: 0.0, y: 0.0), .tip), // far from topMostState
(#line, tipPos - 500.0, CGPoint(x: 0.0, y: 100.0), .tip), // far from topMostState
(#line, tipPos, CGPoint(x: 0.0, y: -100.0), .tip),
(#line, tipPos, CGPoint(x: 0.0, y: 0.0), .tip),
(#line, tipPos, CGPoint(x: 0.0, y: 500.0), .half), // project to half
(#line, tipPos, CGPoint(x: 0.0, y: 1000.0), .half), // block projecting to full at half
(#line, tipPos, CGPoint(x: 0.0, y: 3000.0), .half), // block projecting to full at half
(#line, tipPos + 10.0, CGPoint(x: 0.0, y: 100.0), .tip), // redirect
(#line, halfPos - 10.0, CGPoint(x: 0.0, y: -100.0), .half), // redirect
(#line, halfPos, CGPoint(x: 0.0, y: -1000.0), .tip),// project to tip
(#line, halfPos, CGPoint(x: 0.0, y: -100.0), .half),
(#line, halfPos, CGPoint(x: 0.0, y: 0.0), .half),
(#line, halfPos, CGPoint(x: 0.0, y: 100.0), .half),
(#line, halfPos, CGPoint(x: 0.0, y: 1000.0), .full), // project to full
(#line, halfPos + 10.0, CGPoint(x: 0.0, y: 100.0), .half), // redirect
(#line, fullPos - 10.0, CGPoint(x: 0.0, y: -100.0), .full), // redirect
(#line, fullPos, CGPoint(x: 0.0, y: -3000.0), .half), // block projecting to tip at half
(#line, fullPos, CGPoint(x: 0.0, y: -1000.0), .half), // block projecting to tip at half
(#line, fullPos, CGPoint(x: 0.0, y: -500.0), .half), // project to half
(#line, fullPos, CGPoint(x: 0.0, y: -100.0), .full),
(#line, fullPos, CGPoint(x: 0.0, y: 0.0), .full),
(#line, fullPos, CGPoint(x: 0.0, y: 100.0), .full),
(#line, fullPos + 500.0, CGPoint(x: 0.0, y: -100.0), .full), // far from bottomMostState
(#line, fullPos + 500.0, CGPoint(x: 0.0, y: 0.0), .full), // far from bottomMostState
(#line, fullPos + 500.0, CGPoint(x: 0.0, y: 100.0), .full), // far from bottomMostState
])
}
func test_targetPosition_3positionsFromTip() {
let delegate = FloatingPanelTestDelegate()
delegate.layout = FloatingPanelLayout3Positions()
let fpc = FloatingPanelController(delegate: delegate)
fpc.layout = FloatingPanelLayout3Positions()
fpc.showForTest()
let fullPos = fpc.originYOfSurface(for: .full)
let halfPos = fpc.originYOfSurface(for: .half)
let tipPos = fpc.originYOfSurface(for: .tip)
let fullPos = fpc.surfaceLocation(for: .full).y
let halfPos = fpc.surfaceLocation(for: .half).y
let tipPos = fpc.surfaceLocation(for: .tip).y
// From .tip
fpc.move(to: .tip, animated: false)
@@ -397,20 +540,63 @@ class FloatingPanelTests: XCTestCase {
(#line, tipPos + 500.0, CGPoint(x: 0.0, y: -100.0), .tip), // far from bottomMostState
(#line, tipPos + 500.0, CGPoint(x: 0.0, y: 0.0), .tip), // far from bottomMostState
(#line, tipPos + 500.0, CGPoint(x: 0.0, y: 100.0), .tip), // far from bottomMostState
])
}
func test_targetPosition_3positionsFromTip_bottomEdge() {
let delegate = FloatingPanelTestDelegate()
let fpc = FloatingPanelController(delegate: delegate)
fpc.layout = FloatingPanelLayout3PositionsBottomEdge()
fpc.showForTest()
let fullPos = fpc.surfaceLocation(for: .full).y
let halfPos = fpc.surfaceLocation(for: .half).y
let tipPos = fpc.surfaceLocation(for: .tip).y
// From .tip
fpc.move(to: .tip, animated: false)
assertTargetPosition(fpc.floatingPanel, with: [
(#line, tipPos - 500.0, CGPoint(x: 0.0, y: -100.0), .tip), // far from topMostState
(#line, tipPos - 500.0, CGPoint(x: 0.0, y: 0.0), .tip), // far from topMostState
(#line, tipPos - 500.0, CGPoint(x: 0.0, y: 100.0), .tip), // far from topMostState
(#line, tipPos, CGPoint(x: 0.0, y: -100.0), .tip),
(#line, tipPos, CGPoint(x: 0.0, y: 0.0), .tip),
(#line, tipPos, CGPoint(x: 0.0, y: 500.0), .half), // project to half
(#line, tipPos, CGPoint(x: 0.0, y: 1000.0), .half), // block projecting to tip at half
(#line, tipPos, CGPoint(x: 0.0, y: 3000.0), .half), // block projecting to tip at half
(#line, tipPos + 10.0, CGPoint(x: 0.0, y: 100.0), .tip), // redirect
(#line, halfPos - 10.0, CGPoint(x: 0.0, y: -100.0), .half), // redirect
(#line, halfPos, CGPoint(x: 0.0, y: -3000.0), .tip), // project to full
(#line, halfPos, CGPoint(x: 0.0, y: -100.0), .half),
(#line, halfPos, CGPoint(x: 0.0, y: 0.0), .half),
(#line, halfPos, CGPoint(x: 0.0, y: 100.0), .half),
(#line, halfPos, CGPoint(x: 0.0, y: 1000.0), .full), // project to tip
(#line, halfPos + 10.0, CGPoint(x: 0.0, y: 100.0), .half), // redirect
(#line, fullPos - 10.0, CGPoint(x: 0.0, y: -100.0), .full), // redirect
(#line, fullPos, CGPoint(x: 0.0, y: -3000.0), .half), // block projecting to full at half
(#line, fullPos, CGPoint(x: 0.0, y: -1000.0), .half), // block projecting to full at half
(#line, fullPos, CGPoint(x: 0.0, y: -500.0), .half), // project to half
(#line, fullPos, CGPoint(x: 0.0, y: -100.0), .full),
(#line, fullPos, CGPoint(x: 0.0, y: 0.0), .full),
(#line, fullPos, CGPoint(x: 0.0, y: 100.0), .full),
(#line, fullPos + 500.0, CGPoint(x: 0.0, y: -100.0), .full), // far from bottomMostState
(#line, fullPos + 500.0, CGPoint(x: 0.0, y: 0.0), .full), // far from bottomMostState
(#line, fullPos + 500.0, CGPoint(x: 0.0, y: 100.0), .full), // far from bottomMostState
])
}
func test_targetPosition_3positionsAllProjection() {
let delegate = FloatingPanelTestDelegate()
delegate.layout = FloatingPanelLayout3Positions()
delegate.behavior = FloatingPanelProjectionalBehavior()
let fpc = FloatingPanelController(delegate: delegate)
fpc.layout = FloatingPanelLayout3Positions()
fpc.behavior = FloatingPanelProjectableBehavior()
fpc.showForTest()
let fullPos = fpc.originYOfSurface(for: .full)
let halfPos = fpc.originYOfSurface(for: .half)
let tipPos = fpc.originYOfSurface(for: .tip)
let fullPos = fpc.surfaceLocation(for: .full).y
let halfPos = fpc.surfaceLocation(for: .half).y
let tipPos = fpc.surfaceLocation(for: .tip).y
// From .full
fpc.move(to: .full, animated: false)
@@ -449,16 +635,23 @@ class FloatingPanelTests: XCTestCase {
}
func test_targetPosition_3positionsWithHidden() {
class FloatingPanelLayout3PositionsWithHidden: FloatingPanelTestLayout {
let initialPosition: FloatingPanelPosition = .hidden
let supportedPositions: Set<FloatingPanelPosition> = [.hidden, .half, .full]
class FloatingPanelLayout3PositionsWithHidden: FloatingPanelLayout {
let initialState: FloatingPanelState = .hidden
let position: FloatingPanelPosition = .bottom
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
return [
.full: FloatingPanelLayoutAnchor(absoluteInset: 20.0, edge: .top, referenceGuide: .superview),
.half: FloatingPanelLayoutAnchor(absoluteInset: 250.0, edge: .bottom, referenceGuide: .superview),
.hidden: FloatingPanelLayoutAnchor(absoluteInset: 0, edge: .bottom, referenceGuide: .superview),
]
}
}
let delegate = FloatingPanelTestDelegate()
delegate.layout = FloatingPanelLayout3PositionsWithHidden()
let fpc = FloatingPanelController(delegate: delegate)
fpc.layout = FloatingPanelLayout3PositionsWithHidden()
fpc.showForTest()
XCTAssertEqual(fpc.position, .hidden)
XCTAssertEqual(fpc.state, .hidden)
fpc.move(to: .full, animated: false)
assertTargetPosition(fpc.floatingPanel, with: [
@@ -474,22 +667,29 @@ class FloatingPanelTests: XCTestCase {
}
func test_targetPosition_3positionsWithHiddenWithoutFull() {
class FloatingPanelLayout3Positions: FloatingPanelTestLayout {
let initialPosition: FloatingPanelPosition = .hidden
let supportedPositions: Set<FloatingPanelPosition> = [.hidden, .tip, .half]
class FloatingPanelLayout3Positions: FloatingPanelLayout {
let initialState: FloatingPanelState = .hidden
let position: FloatingPanelPosition = .bottom
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
return [
.half: FloatingPanelLayoutAnchor(absoluteInset: 250.0, edge: .bottom, referenceGuide: .superview),
.tip: FloatingPanelLayoutAnchor(absoluteInset: 60.0, edge: .bottom, referenceGuide: .superview),
.hidden: FloatingPanelLayoutAnchor(absoluteInset: 0, edge: .bottom, referenceGuide: .superview),
]
}
}
let delegate = FloatingPanelTestDelegate()
delegate.layout = FloatingPanelLayout3Positions()
delegate.behavior = FloatingPanelProjectionalBehavior()
let fpc = FloatingPanelController(delegate: delegate)
fpc.showForTest()
XCTAssertEqual(fpc.position, .hidden)
fpc.layout = FloatingPanelLayout3Positions()
let halfPos = fpc.originYOfSurface(for: .half)
let tipPos = fpc.originYOfSurface(for: .tip)
//let hiddenPos = fpc.originYOfSurface(for: .hidden)
fpc.showForTest()
fpc.behavior = FloatingPanelProjectableBehavior()
XCTAssertEqual(fpc.state, .hidden)
let halfPos = fpc.surfaceLocation(for: .half).y
let tipPos = fpc.surfaceLocation(for: .tip).y
//let hiddenPos = fpc.surfaceLocation(for: .hidden)
fpc.move(to: .half, animated: false)
assertTargetPosition(fpc.floatingPanel, with: [
@@ -518,20 +718,21 @@ class FloatingPanelTests: XCTestCase {
}
private class FloatingPanelLayout3Positions: FloatingPanelTestLayout {
let initialPosition: FloatingPanelPosition = .tip
let supportedPositions: Set<FloatingPanelPosition> = [.tip, .half, .full]
override var initialState: FloatingPanelState {
return .tip
}
}
private typealias TestParameter = (UInt, CGFloat,CGPoint, FloatingPanelPosition)
private func assertTargetPosition(_ floatingPanel: FloatingPanel, with params: [TestParameter]) {
private class FloatingPanelLayout3PositionsBottomEdge: FloatingPanelTop2BottomTestLayout {
override var initialState: FloatingPanelState {
return .tip
}
}
private typealias TestParameter = (UInt, CGFloat, CGPoint, FloatingPanelState)
private func assertTargetPosition(_ floatingPanel: Core, with params: [TestParameter]) {
params.forEach { (line, pos, velocity, result) in
floatingPanel.surfaceView.frame.origin.y = pos
XCTAssertEqual(floatingPanel.targetPosition(from: pos, with: velocity), result, line: line)
}
}
private class FloatingPanelProjectionalBehavior: FloatingPanelBehavior {
func shouldProjectMomentum(_ fpc: FloatingPanelController, for proposedTargetPosition: FloatingPanelPosition) -> Bool {
return true
XCTAssertEqual(floatingPanel.targetPosition(from: pos, with: velocity.y), result, line: line)
}
}
+662
View File
@@ -0,0 +1,662 @@
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
import XCTest
@testable import FloatingPanel
class LayoutTests: XCTestCase {
var fpc: FloatingPanelController!
override func setUp() {
fpc = FloatingPanelController(delegate: nil)
fpc.loadViewIfNeeded()
fpc.view.frame = CGRect(x: 0, y: 0, width: 375, height: 667)
}
override func tearDown() {}
func test_layoutAdapter_topAndBottomMostState() {
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.edgeMostState, .full)
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.edgeLeastState, .tip)
class FloatingPanelLayoutWithHidden: FloatingPanelLayout {
var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] {
return [
.full: FloatingPanelLayoutAnchor(absoluteInset: 18.0, edge: .top, referenceGuide: .safeArea),
.half: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .safeArea),
.hidden: FloatingPanelLayoutAnchor(absoluteInset: 0, edge: .bottom, referenceGuide: .superview)
]
}
let initialState: FloatingPanelState = .hidden
let position: FloatingPanelPosition = .bottom
}
class FloatingPanelLayout2Positions: FloatingPanelLayout {
var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] {
return [
.half: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .safeArea),
.tip: FloatingPanelLayoutAnchor(absoluteInset: 69.0, edge: .bottom, referenceGuide: .safeArea),
]
}
let initialState: FloatingPanelState = .tip
let position: FloatingPanelPosition = .bottom
}
fpc.layout = FloatingPanelLayoutWithHidden()
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.edgeMostState, .full)
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.edgeLeastState, .hidden)
fpc.layout = FloatingPanelLayout2Positions()
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.edgeMostState, .half)
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.edgeLeastState, .tip)
}
func test_layoutSegment_3position() {
class FloatingPanelLayout3Positions: FloatingPanelTestLayout {
override var initialState: FloatingPanelState { .half }
}
fpc.layout = FloatingPanelLayout3Positions()
let fullPos = fpc.surfaceLocation(for: .full).y
let halfPos = fpc.surfaceLocation(for: .half).y
let tipPos = fpc.surfaceLocation(for: .tip).y
let minPos = CGFloat.leastNormalMagnitude
let maxPos = CGFloat.greatestFiniteMagnitude
assertLayoutSegment(fpc.floatingPanel, with: [
(#line, pos: minPos, forwardY: true, lower: nil, upper: .full),
(#line, pos: minPos, forwardY: false, lower: nil, upper: .full),
(#line, pos: fullPos, forwardY: true, lower: .full, upper: .half),
(#line, pos: fullPos, forwardY: false, lower: nil, upper: .full),
(#line, pos: halfPos, forwardY: true, lower: .half, upper: .tip),
(#line, pos: halfPos, forwardY: false, lower: .full, upper: .half),
(#line, pos: tipPos, forwardY: true, lower: .tip, upper: nil),
(#line, pos: tipPos, forwardY: false, lower: .half, upper: .tip),
(#line, pos: maxPos, forwardY: true, lower: .tip, upper: nil),
(#line, pos: maxPos, forwardY: false, lower: .tip, upper: nil),
])
}
func test_layoutSegment_2positions() {
class FloatingPanelLayout2Positions: FloatingPanelTestLayout {
override var initialState: FloatingPanelState { .half }
override var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring]
{ super.anchors.filter { (key, _) in key != .tip } }
}
fpc.layout = FloatingPanelLayout2Positions()
let fullPos = fpc.surfaceLocation(for: .full).y
let halfPos = fpc.surfaceLocation(for: .half).y
let minPos = CGFloat.leastNormalMagnitude
let maxPos = CGFloat.greatestFiniteMagnitude
assertLayoutSegment(fpc.floatingPanel, with: [
(#line, pos: minPos, forwardY: true, lower: nil, upper: .full),
(#line, pos: minPos, forwardY: false, lower: nil, upper: .full),
(#line, pos: fullPos, forwardY: true, lower: .full, upper: .half),
(#line, pos: fullPos, forwardY: false, lower: nil, upper: .full),
(#line, pos: halfPos, forwardY: true, lower: .half, upper: nil),
(#line, pos: halfPos, forwardY: false, lower: .full, upper: .half),
(#line, pos: maxPos, forwardY: true, lower: .half, upper: nil),
(#line, pos: maxPos, forwardY: false, lower: .half, upper: nil),
])
}
func test_layoutSegment_1positions() {
class FloatingPanelLayout1Positions: FloatingPanelTestLayout {
override var initialState: FloatingPanelState { .full }
override var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring]
{ super.anchors.filter { (key, _) in key == .full } }
}
fpc.layout = FloatingPanelLayout1Positions()
let fullPos = fpc.surfaceLocation(for: .full).y
let minPos = CGFloat.leastNormalMagnitude
let maxPos = CGFloat.greatestFiniteMagnitude
assertLayoutSegment(fpc.floatingPanel, with: [
(#line, pos: minPos, forwardY: true, lower: nil, upper: .full),
(#line, pos: minPos, forwardY: false, lower: nil, upper: .full),
(#line, pos: fullPos, forwardY: true, lower: .full, upper: nil),
(#line, pos: fullPos, forwardY: false, lower: nil, upper: .full),
(#line, pos: maxPos, forwardY: true, lower: .full, upper: nil),
(#line, pos: maxPos, forwardY: false, lower: .full, upper: nil),
])
}
func test_updateInteractiveEdgeConstraint() {
fpc.showForTest()
fpc.move(to: .full, animated: false)
fpc.floatingPanel.layoutAdapter.startInteraction(at: fpc.state)
fpc.floatingPanel.layoutAdapter.startInteraction(at: fpc.state) // Should be ignore
let fullPos = fpc.surfaceLocation(for: .full).y
let tipPos = fpc.surfaceLocation(for: .tip).y
var next: CGFloat
fpc.floatingPanel.layoutAdapter.updateInteractiveEdgeConstraint(diff: -100.0,
overflow: false,
allowsRubberBanding: fpc.floatingPanel.behaviorAdapter.allowsRubberBanding(for:))
next = fpc.surfaceLocation.y
XCTAssertEqual(next, fullPos)
fpc.floatingPanel.layoutAdapter.updateInteractiveEdgeConstraint(diff: 100.0,
overflow: true,
allowsRubberBanding: fpc.floatingPanel.behaviorAdapter.allowsRubberBanding(for:))
next = fpc.surfaceLocation.y
XCTAssertEqual(next, fullPos + 100.0)
fpc.floatingPanel.layoutAdapter.updateInteractiveEdgeConstraint(diff: tipPos - fullPos,
overflow: true,
allowsRubberBanding: fpc.floatingPanel.behaviorAdapter.allowsRubberBanding(for:))
next = fpc.surfaceLocation.y
XCTAssertEqual(next, tipPos)
fpc.floatingPanel.layoutAdapter.updateInteractiveEdgeConstraint(diff: tipPos - fullPos + 100.0,
overflow: false,
allowsRubberBanding: fpc.floatingPanel.behaviorAdapter.allowsRubberBanding(for:))
next = fpc.surfaceLocation.y
XCTAssertEqual(next, tipPos)
fpc.floatingPanel.layoutAdapter.updateInteractiveEdgeConstraint(diff: tipPos - fullPos + 100.0,
overflow: true,
allowsRubberBanding: fpc.floatingPanel.behaviorAdapter.allowsRubberBanding(for:))
next = fpc.surfaceLocation.y
XCTAssertEqual(next, tipPos + 100.0)
fpc.floatingPanel.layoutAdapter.endInteraction(at: fpc.state)
}
func test_updateInteractiveEdgeConstraint_bottomEdge() {
fpc.layout = FloatingPanelTop2BottomTestLayout()
fpc.showForTest()
fpc.move(to: .tip, animated: false)
XCTAssertEqual(fpc.surfaceView.frame, CGRect(x: 0.0, y: -667.0 + 60.0, width: 375.0, height: 667))
XCTAssertEqual(fpc.surfaceView.containerView.frame, CGRect(x: 0.0, y: -667.0,
width: 375.0, height: 667 * 2.0))
fpc.floatingPanel.layoutAdapter.startInteraction(at: fpc.state)
fpc.floatingPanel.layoutAdapter.startInteraction(at: fpc.state) // Should be ignore
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.interactionConstraint?.constant, 60.0)
let fullPos = fpc.surfaceLocation(for: .full).y
let tipPos = fpc.surfaceLocation(for: .tip).y
var pre: CGFloat
var next: CGFloat
pre = fpc.surfaceLocation.y
fpc.floatingPanel.layoutAdapter.updateInteractiveEdgeConstraint(diff: -100.0,
overflow: false,
allowsRubberBanding: fpc.floatingPanel.behaviorAdapter.allowsRubberBanding(for:))
next = fpc.surfaceLocation.y
XCTAssertEqual(next, pre)
fpc.floatingPanel.layoutAdapter.updateInteractiveEdgeConstraint(diff: 100.0,
overflow: true,
allowsRubberBanding: fpc.floatingPanel.behaviorAdapter.allowsRubberBanding(for:))
next = fpc.surfaceLocation.y
XCTAssertEqual(next, tipPos + 100.0)
fpc.floatingPanel.layoutAdapter.updateInteractiveEdgeConstraint(diff: fullPos - tipPos,
overflow: true,
allowsRubberBanding: fpc.floatingPanel.behaviorAdapter.allowsRubberBanding(for:))
next = fpc.surfaceLocation.y
XCTAssertEqual(next, fullPos)
fpc.floatingPanel.layoutAdapter.updateInteractiveEdgeConstraint(diff: fullPos - tipPos + 100,
overflow: false,
allowsRubberBanding: fpc.floatingPanel.behaviorAdapter.allowsRubberBanding(for:))
next = fpc.surfaceLocation.y
XCTAssertEqual(next, fullPos)
fpc.floatingPanel.layoutAdapter.updateInteractiveEdgeConstraint(diff: fullPos - tipPos + 100,
overflow: true,
allowsRubberBanding: fpc.floatingPanel.behaviorAdapter.allowsRubberBanding(for:))
next = fpc.surfaceLocation.y
XCTAssertEqual(next, fullPos + 100.0)
fpc.floatingPanel.layoutAdapter.endInteraction(at: fpc.state)
}
func test_updateInteractiveEdgeConstraintWithHidden() {
class FloatingPanelLayout2Positions: FloatingPanelLayout {
var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] {
return [
.full: FloatingPanelLayoutAnchor(absoluteInset: 18.0, edge: .bottom, referenceGuide: .safeArea),
.hidden: FloatingPanelLayoutAnchor(absoluteInset: 0, edge: .bottom, referenceGuide: .superview),
]
}
let initialState: FloatingPanelState = .hidden
let position: FloatingPanelPosition = .bottom
}
fpc.layout = FloatingPanelLayout2Positions()
fpc.showForTest()
fpc.move(to: .full, animated: false)
fpc.floatingPanel.layoutAdapter.startInteraction(at: fpc.state)
let fullPos = fpc.surfaceLocation(for: .full).y
let hiddenPos = fpc.surfaceLocation(for: .hidden).y
var next: CGFloat
fpc.floatingPanel.layoutAdapter.updateInteractiveEdgeConstraint(diff: -100.0,
overflow: false,
allowsRubberBanding: fpc.floatingPanel.behaviorAdapter.allowsRubberBanding(for:))
next = fpc.surfaceLocation.y
XCTAssertEqual(next, fullPos)
fpc.floatingPanel.layoutAdapter.updateInteractiveEdgeConstraint(diff: -100.0,
overflow: true,
allowsRubberBanding: fpc.floatingPanel.behaviorAdapter.allowsRubberBanding(for:))
next = fpc.surfaceLocation.y
XCTAssertEqual(next, fullPos - 100.0)
fpc.floatingPanel.layoutAdapter.updateInteractiveEdgeConstraint(diff: hiddenPos - fullPos + 100.0,
overflow: true,
allowsRubberBanding: fpc.floatingPanel.behaviorAdapter.allowsRubberBanding(for:))
next = fpc.surfaceLocation.y
XCTAssertEqual(next, hiddenPos + 100.0)
fpc.floatingPanel.layoutAdapter.endInteraction(at: fpc.state)
}
func test_updateInteractiveEdgeConstraintWithHidden_bottomEdge() {
class FloatingPanelLayout2Positions: FloatingPanelLayout {
var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] {
[
.full: FloatingPanelLayoutAnchor(absoluteInset: 18.0, edge: .bottom, referenceGuide: .safeArea),
.hidden: FloatingPanelLayoutAnchor(absoluteInset: 0, edge: .top, referenceGuide: .superview),
]
}
let initialState: FloatingPanelState = .hidden
let position: FloatingPanelPosition = .top
}
fpc.layout = FloatingPanelLayout2Positions()
fpc.showForTest()
fpc.move(to: .full, animated: false)
fpc.floatingPanel.layoutAdapter.startInteraction(at: fpc.state)
let fullPos = fpc.surfaceLocation(for: .full).y
let hiddenPos = fpc.surfaceLocation(for: .hidden).y
var next: CGFloat
fpc.floatingPanel.layoutAdapter.updateInteractiveEdgeConstraint(diff: 100.0,
overflow: false,
allowsRubberBanding: fpc.floatingPanel.behaviorAdapter.allowsRubberBanding(for:))
next = fpc.surfaceLocation.y
XCTAssertEqual(next, fullPos)
fpc.floatingPanel.layoutAdapter.updateInteractiveEdgeConstraint(diff: 100.0,
overflow: true,
allowsRubberBanding: fpc.floatingPanel.behaviorAdapter.allowsRubberBanding(for:))
next = fpc.surfaceLocation.y
XCTAssertEqual(next, fullPos + 100.0)
fpc.floatingPanel.layoutAdapter.updateInteractiveEdgeConstraint(diff: hiddenPos - fullPos + 100.0,
overflow: true,
allowsRubberBanding: fpc.floatingPanel.behaviorAdapter.allowsRubberBanding(for:))
next = fpc.surfaceLocation.y
XCTAssertEqual(next, hiddenPos + 100.0)
fpc.floatingPanel.layoutAdapter.endInteraction(at: fpc.state)
}
func test_updateInteractiveTopConstraintWithMinusInsets() {
class FloatingPanelLayoutMinusInsets: FloatingPanelLayout {
var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] {
[
.full: FloatingPanelLayoutAnchor(absoluteInset: -200, edge: .top, referenceGuide: .safeArea),
.tip: FloatingPanelLayoutAnchor(absoluteInset: -200, edge: .bottom, referenceGuide: .safeArea),
]
}
let initialState: FloatingPanelState = .full
let position: FloatingPanelPosition = .bottom
}
fpc.layout = FloatingPanelLayoutMinusInsets()
fpc.showForTest()
fpc.floatingPanel.layoutAdapter.startInteraction(at: fpc.state)
let fullPos = fpc.surfaceLocation(for: .full).y
let tipPos = fpc.surfaceLocation(for: .tip).y
var next: CGFloat
fpc.floatingPanel.layoutAdapter.updateInteractiveEdgeConstraint(diff: -100.0,
overflow: false,
allowsRubberBanding: fpc.floatingPanel.behaviorAdapter.allowsRubberBanding(for:))
next = fpc.surfaceLocation.y
XCTAssertEqual(next, fullPos)
fpc.floatingPanel.layoutAdapter.updateInteractiveEdgeConstraint(diff: -100.0,
overflow: true,
allowsRubberBanding: fpc.floatingPanel.behaviorAdapter.allowsRubberBanding(for:))
next = fpc.surfaceLocation.y
XCTAssertEqual(next, fullPos - 100)
fpc.floatingPanel.layoutAdapter.updateInteractiveEdgeConstraint(diff: tipPos - fullPos + 100.0,
overflow: true,
allowsRubberBanding: fpc.floatingPanel.behaviorAdapter.allowsRubberBanding(for:))
next = fpc.surfaceLocation.y
XCTAssertEqual(next, tipPos + 100)
fpc.floatingPanel.layoutAdapter.endInteraction(at: fpc.state)
}
func test_surfaceLocation() {
fpc = CustomSafeAreaFloatingPanelController()
fpc.loadViewIfNeeded()
fpc.view.frame = CGRect(x: 0, y: 0, width: 375, height: 667)
class MyFloatingPanelFullLayout: FloatingPanelTestLayout {}
class MyFloatingPanelSafeAreaLayout: FloatingPanelTestLayout {
override var referenceGuide: FloatingPanelLayoutReferenceGuide {
return .safeArea
}
}
let myLayout = MyFloatingPanelFullLayout()
fpc.layout = myLayout
fpc.showForTest()
let bounds = fpc.view!.bounds
XCTAssertEqual(fpc.layout.anchors.filter({ $0.value.referenceGuide != .superview }).count, 0)
XCTAssertEqual(fpc.surfaceLocation(for: .full).y, myLayout.fullInset)
XCTAssertEqual(fpc.surfaceLocation(for: .half).y, bounds.height - myLayout.halfInset)
XCTAssertEqual(fpc.surfaceLocation(for: .tip).y, bounds.height - myLayout.tipInset)
XCTAssertEqual(fpc.surfaceLocation(for: .hidden).y, bounds.height + 100.0)
fpc.layout = MyFloatingPanelSafeAreaLayout()
XCTAssertEqual(fpc.layout.anchors.filter({ $0.value.referenceGuide != .safeArea }).count, 0)
XCTAssertEqual(fpc.surfaceLocation(for: .full).y, myLayout.fullInset + fpc.fp_safeAreaInsets.top)
XCTAssertEqual(fpc.surfaceLocation(for: .half).y, bounds.height - myLayout.halfInset + fpc.fp_safeAreaInsets.bottom)
XCTAssertEqual(fpc.surfaceLocation(for: .tip).y, bounds.height - myLayout.tipInset + fpc.fp_safeAreaInsets.bottom)
XCTAssertEqual(fpc.surfaceLocation(for: .hidden).y, bounds.height + 100.0)
}
func test_surfaceLocation_bottomEdge() {
fpc = CustomSafeAreaFloatingPanelController()
fpc.loadViewIfNeeded()
fpc.view.frame = CGRect(x: 0, y: 0, width: 375, height: 667)
class MyFloatingPanelFullLayout: FloatingPanelTop2BottomTestLayout { }
class MyFloatingPanelSafeAreaLayout: FloatingPanelTop2BottomTestLayout {
override var referenceGuide: FloatingPanelLayoutReferenceGuide {
return .safeArea
}
}
let myLayout = MyFloatingPanelFullLayout()
fpc.layout = myLayout
fpc.showForTest()
let bounds = fpc.view!.bounds
XCTAssertEqual(fpc.layout.anchors.filter({ $0.value.referenceGuide != .superview }).count, 0)
XCTAssertEqual(fpc.surfaceLocation(for: .full).y, bounds.height - myLayout.fullInset)
XCTAssertEqual(fpc.surfaceLocation(for: .half).y, myLayout.halfInset)
XCTAssertEqual(fpc.surfaceLocation(for: .tip).y, myLayout.tipInset)
XCTAssertEqual(fpc.surfaceLocation(for: .hidden).y, -100.0)
fpc.layout = MyFloatingPanelSafeAreaLayout()
XCTAssertEqual(fpc.layout.anchors.filter({ $0.value.referenceGuide != .safeArea }).count, 0)
XCTAssertEqual(fpc.surfaceLocation(for: .full).y, bounds.height - myLayout.fullInset + fpc.fp_safeAreaInsets.bottom)
XCTAssertEqual(fpc.surfaceLocation(for: .half).y, myLayout.halfInset + fpc.fp_safeAreaInsets.top)
XCTAssertEqual(fpc.surfaceLocation(for: .tip).y, myLayout.tipInset + fpc.fp_safeAreaInsets.top)
XCTAssertEqual(fpc.surfaceLocation(for: .hidden).y, -100.0)
}
func test_layoutAnchor_topPosition() {
let position: FloatingPanelPosition = .top
fpc = CustomSafeAreaFloatingPanelController()
fpc.loadViewIfNeeded()
fpc.view.frame = CGRect(x: 0, y: 0, width: 375, height: 667)
for prop in [
// from top edge
(anchor: FloatingPanelLayoutAnchor(absoluteInset: 0.0, edge: .top, referenceGuide: .safeArea),
result: (#line, constant: 0.0, firstAnchor: fpc.surfaceView.bottomAnchor, secondAnchor: fpc.fp_safeAreaLayoutGuide.topAnchor)),
(anchor: FloatingPanelLayoutAnchor(absoluteInset: 100.0, edge: .top, referenceGuide: .safeArea),
result: (#line, constant: 100.0, firstAnchor: fpc.surfaceView.bottomAnchor, secondAnchor: fpc.fp_safeAreaLayoutGuide.topAnchor)),
(anchor: FloatingPanelLayoutAnchor(absoluteInset: 0.0, edge: .top, referenceGuide: .superview),
result: (#line, constant: 0.0, firstAnchor: fpc.surfaceView.bottomAnchor, secondAnchor: fpc.view.topAnchor)),
(anchor: FloatingPanelLayoutAnchor(absoluteInset: 100.0, edge: .top, referenceGuide: .superview),
result: (#line, constant: 100.0, firstAnchor: fpc.surfaceView.bottomAnchor, secondAnchor: fpc.view.topAnchor)),
// from bottom edge
(anchor: FloatingPanelLayoutAnchor(absoluteInset: 0.0, edge: .bottom, referenceGuide: .safeArea),
result: (#line, constant: 0.0, firstAnchor: fpc.fp_safeAreaLayoutGuide.bottomAnchor, secondAnchor: fpc.surfaceView.bottomAnchor)),
(anchor: FloatingPanelLayoutAnchor(absoluteInset: 100.0, edge: .bottom, referenceGuide: .safeArea),
result: (#line, constant: 100.0, firstAnchor: fpc.fp_safeAreaLayoutGuide.bottomAnchor, secondAnchor: fpc.surfaceView.bottomAnchor)),
(anchor: FloatingPanelLayoutAnchor(absoluteInset: 0.0, edge: .bottom, referenceGuide: .superview),
result: (#line, constant: 0.0, firstAnchor: fpc.view.bottomAnchor, secondAnchor: fpc.surfaceView.bottomAnchor)),
(anchor: FloatingPanelLayoutAnchor(absoluteInset: 100.0, edge: .bottom, referenceGuide: .superview),
result: (#line, constant: 100.0, firstAnchor: fpc.view.bottomAnchor, secondAnchor: fpc.surfaceView.bottomAnchor)),
] {
let c = prop.anchor.layoutConstraints(fpc, for: position)[0]
XCTAssertEqual(c.constant, CGFloat(prop.result.constant), line: UInt(prop.result.0))
XCTAssertEqual(c.firstAnchor, prop.result.firstAnchor, line: UInt(prop.result.0))
XCTAssertEqual(c.secondAnchor, prop.result.secondAnchor, line: UInt(prop.result.0))
}
// fractional
for prop in [
// from top edge
(anchor: FloatingPanelLayoutAnchor(fractionalInset: 0.0, edge: .top, referenceGuide: .safeArea),
result: (#line, multiplier: 1.0, secondAnchor: nil)),
(anchor: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .top, referenceGuide: .safeArea),
result: (#line, multiplier: 0.5, secondAnchor: fpc.fp_safeAreaLayoutGuide.heightAnchor)),
(anchor: FloatingPanelLayoutAnchor(fractionalInset: 0.0, edge: .top, referenceGuide: .superview),
result: (#line, multiplier: 1.0, secondAnchor: nil)),
(anchor: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .top, referenceGuide: .superview),
result: (#line, multiplier: 0.5, secondAnchor: fpc.view.heightAnchor)),
// from bottom edge
(anchor: FloatingPanelLayoutAnchor(fractionalInset: 0.0, edge: .bottom, referenceGuide: .safeArea),
result: (#line, multiplier: 1.0, secondAnchor: nil)),
(anchor: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .safeArea),
result: (#line, multiplier: 0.5, secondAnchor: fpc.fp_safeAreaLayoutGuide.heightAnchor)),
(anchor: FloatingPanelLayoutAnchor(fractionalInset: 0.0, edge: .bottom, referenceGuide: .superview),
result: (#line, multiplier: 1.0, secondAnchor: nil)),
(anchor: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .superview),
result: (#line, multiplier: 0.5, secondAnchor: fpc.view.heightAnchor)),
] {
let c = prop.anchor.layoutConstraints(fpc, for: position)[0]
XCTAssertEqual(c.multiplier, CGFloat(prop.result.multiplier), line: UInt(prop.result.0))
XCTAssertTrue(c.firstAnchor is NSLayoutAnchor<NSLayoutDimension>, line: UInt(prop.result.0))
// On iOS 10, `c.secondAnchor` can't be equal object to `prop.result.secondAnchor`
// because there is no safe area on iOS 10 and `fp_safeAreaLayoutGuide` emulates it.
if #available(iOS 11, *) {
XCTAssertEqual(c.secondAnchor, prop.result.secondAnchor, line: UInt(prop.result.0))
}
print(c)
}
}
func test_layoutAnchor_bottomPosition() {
let position: FloatingPanelPosition = .bottom
fpc = CustomSafeAreaFloatingPanelController()
fpc.loadViewIfNeeded()
fpc.view.frame = CGRect(x: 0, y: 0, width: 375, height: 667)
for prop in [
// from top edge
(anchor: FloatingPanelLayoutAnchor(absoluteInset: 0.0, edge: .top, referenceGuide: .safeArea),
result: (#line, constant: 0.0, firstAnchor: fpc.surfaceView.topAnchor, secondAnchor: fpc.fp_safeAreaLayoutGuide.topAnchor)),
(anchor: FloatingPanelLayoutAnchor(absoluteInset: 100.0, edge: .top, referenceGuide: .safeArea),
result: (#line, constant: 100.0, firstAnchor: fpc.surfaceView.topAnchor, secondAnchor: fpc.fp_safeAreaLayoutGuide.topAnchor)),
(anchor: FloatingPanelLayoutAnchor(absoluteInset: 0.0, edge: .top, referenceGuide: .superview),
result: (#line, constant: 0.0, firstAnchor: fpc.surfaceView.topAnchor, secondAnchor: fpc.view.topAnchor)),
(anchor: FloatingPanelLayoutAnchor(absoluteInset: 100.0, edge: .top, referenceGuide: .superview),
result: (#line, constant: 100.0, firstAnchor: fpc.surfaceView.topAnchor, secondAnchor: fpc.view.topAnchor)),
// from bottom edge
(anchor: FloatingPanelLayoutAnchor(absoluteInset: 0.0, edge: .bottom, referenceGuide: .safeArea),
result: (#line, constant: 0.0, firstAnchor: fpc.fp_safeAreaLayoutGuide.bottomAnchor, secondAnchor: fpc.surfaceView.topAnchor)),
(anchor: FloatingPanelLayoutAnchor(absoluteInset: 100.0, edge: .bottom, referenceGuide: .safeArea),
result: (#line, constant: 100.0, firstAnchor: fpc.fp_safeAreaLayoutGuide.bottomAnchor, secondAnchor: fpc.surfaceView.topAnchor)),
(anchor: FloatingPanelLayoutAnchor(absoluteInset: 0.0, edge: .bottom, referenceGuide: .superview),
result: (#line, constant: 0.0, firstAnchor: fpc.view.bottomAnchor, secondAnchor: fpc.surfaceView.topAnchor)),
(anchor: FloatingPanelLayoutAnchor(absoluteInset: 100.0, edge: .bottom, referenceGuide: .superview),
result: (#line, constant: 100.0, firstAnchor: fpc.view.bottomAnchor, secondAnchor: fpc.surfaceView.topAnchor)),
] {
let c = prop.anchor.layoutConstraints(fpc, for: position)[0]
XCTAssertEqual(c.constant, CGFloat(prop.result.constant), line: UInt(prop.result.0))
XCTAssertEqual(c.firstAnchor, prop.result.firstAnchor, line: UInt(prop.result.0))
XCTAssertEqual(c.secondAnchor, prop.result.secondAnchor, line: UInt(prop.result.0))
}
// fractional
for prop in [
// from top edge
(anchor: FloatingPanelLayoutAnchor(fractionalInset: 0.0, edge: .top, referenceGuide: .safeArea),
result: (#line, multiplier: 1.0, secondAnchor: nil)),
(anchor: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .top, referenceGuide: .safeArea),
result: (#line, multiplier: 0.5, secondAnchor: fpc.fp_safeAreaLayoutGuide.heightAnchor)),
(anchor: FloatingPanelLayoutAnchor(fractionalInset: 0.0, edge: .top, referenceGuide: .superview),
result: (#line, multiplier: 1.0, secondAnchor: nil)),
(anchor: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .top, referenceGuide: .superview),
result: (#line, multiplier: 0.5, secondAnchor: fpc.view.heightAnchor)),
// from bottom edge
(anchor: FloatingPanelLayoutAnchor(fractionalInset: 0.0, edge: .bottom, referenceGuide: .safeArea),
result: (#line, multiplier: 1.0, secondAnchor: nil)),
(anchor: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .safeArea),
result: (#line, multiplier: 0.5, secondAnchor: fpc.fp_safeAreaLayoutGuide.heightAnchor)),
(anchor: FloatingPanelLayoutAnchor(fractionalInset: 0.0, edge: .bottom, referenceGuide: .superview),
result: (#line, multiplier: 1.0, secondAnchor: nil)),
(anchor: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .superview),
result: (#line, multiplier: 0.5, secondAnchor: fpc.view.heightAnchor)),
] {
let c = prop.anchor.layoutConstraints(fpc, for: position)[0]
XCTAssertEqual(c.multiplier, CGFloat(prop.result.multiplier), line: UInt(prop.result.0))
XCTAssertTrue(c.firstAnchor is NSLayoutAnchor<NSLayoutDimension>, line: UInt(prop.result.0))
// On iOS 10, `c.secondAnchor` can't be equal object to `prop.result.secondAnchor`
// because there is no safe area on iOS 10 and `fp_safeAreaLayoutGuide` emulates it.
if #available(iOS 11, *) {
XCTAssertEqual(c.secondAnchor, prop.result.secondAnchor, line: UInt(prop.result.0))
}
print(c)
}
}
func test_intrinsicLayoutAnchor_topPosition() {
class ContentViewController: UIViewController {
class IntrinsicView: UIView {
override var intrinsicContentSize: CGSize {
return CGSize(width: UIView.noIntrinsicMetric, height: 420)
}
}
override func loadView() {
self.view = IntrinsicView()
}
}
let position: FloatingPanelPosition = .top
fpc = CustomSafeAreaFloatingPanelController()
fpc.loadViewIfNeeded()
fpc.view.frame = CGRect(x: 0, y: 0, width: 375, height: 667)
let contentVC = ContentViewController()
contentVC.loadViewIfNeeded()
fpc.set(contentViewController: contentVC)
for prop in [
(anchor: FloatingPanelIntrinsicLayoutAnchor(absoluteOffset: 0.0, referenceGuide: .safeArea),
result: (#line, constant: 420, firstAnchor: fpc.surfaceView.bottomAnchor, secondAnchor: fpc.fp_safeAreaLayoutGuide.topAnchor)),
(anchor: FloatingPanelIntrinsicLayoutAnchor(absoluteOffset: 42.0, referenceGuide: .safeArea),
result: (#line, constant: 420 - 42, firstAnchor: fpc.surfaceView.bottomAnchor, secondAnchor: fpc.fp_safeAreaLayoutGuide.topAnchor)),
(anchor: FloatingPanelIntrinsicLayoutAnchor(absoluteOffset: 0.0, referenceGuide: .superview),
result: (#line, constant: 420, firstAnchor: fpc.surfaceView.bottomAnchor, secondAnchor: fpc.view.topAnchor)),
(anchor: FloatingPanelIntrinsicLayoutAnchor(absoluteOffset: 42.0, referenceGuide: .superview),
result: (#line, constant: 420 - 42, firstAnchor: fpc.surfaceView.bottomAnchor, secondAnchor: fpc.view.topAnchor)),
(anchor: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 0.0, referenceGuide: .safeArea),
result: (#line, constant: 420, firstAnchor: fpc.surfaceView.bottomAnchor, secondAnchor: fpc.fp_safeAreaLayoutGuide.topAnchor)),
(anchor: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 0.5, referenceGuide: .safeArea),
result: (#line, constant: 210, firstAnchor: fpc.surfaceView.bottomAnchor, secondAnchor: fpc.fp_safeAreaLayoutGuide.topAnchor)),
(anchor: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 1.0, referenceGuide: .safeArea),
result: (#line, constant: 0, firstAnchor: fpc.surfaceView.bottomAnchor, secondAnchor: fpc.fp_safeAreaLayoutGuide.topAnchor)),
(anchor: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 0.0, referenceGuide: .superview),
result: (#line, constant: 420, firstAnchor: fpc.surfaceView.bottomAnchor, secondAnchor: fpc.view.topAnchor)),
(anchor: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 0.5, referenceGuide: .superview),
result: (#line, constant: 210, firstAnchor: fpc.surfaceView.bottomAnchor, secondAnchor: fpc.view.topAnchor)),
(anchor: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 1.0, referenceGuide: .superview),
result: (#line, constant: 0, firstAnchor: fpc.surfaceView.bottomAnchor, secondAnchor: fpc.view.topAnchor)),
] {
let c = prop.anchor.layoutConstraints(fpc, for: position)[0]
XCTAssertEqual(c.constant, CGFloat(prop.result.constant), line: UInt(prop.result.0))
XCTAssertEqual(c.firstAnchor, prop.result.firstAnchor, line: UInt(prop.result.0))
XCTAssertEqual(c.secondAnchor, prop.result.secondAnchor, line: UInt(prop.result.0))
}
}
func test_intrinsicLayoutAnchor_bottomPosition() {
class ContentViewController: UIViewController {
class IntrinsicView: UIView {
override var intrinsicContentSize: CGSize {
return CGSize(width: UIView.noIntrinsicMetric, height: 420)
}
}
override func loadView() {
self.view = IntrinsicView()
}
}
let position: FloatingPanelPosition = .bottom
fpc = CustomSafeAreaFloatingPanelController()
fpc.loadViewIfNeeded()
fpc.view.frame = CGRect(x: 0, y: 0, width: 375, height: 667)
let contentVC = ContentViewController()
contentVC.loadViewIfNeeded()
fpc.set(contentViewController: contentVC)
for prop in [
(anchor: FloatingPanelIntrinsicLayoutAnchor(absoluteOffset: 0.0, referenceGuide: .safeArea),
result: (#line, constant: -420, firstAnchor: fpc.surfaceView.topAnchor, secondAnchor: fpc.fp_safeAreaLayoutGuide.bottomAnchor)),
(anchor: FloatingPanelIntrinsicLayoutAnchor(absoluteOffset: 42.0, referenceGuide: .safeArea),
result: (#line, constant: -420 + 42, firstAnchor: fpc.surfaceView.topAnchor, secondAnchor: fpc.fp_safeAreaLayoutGuide.bottomAnchor)),
(anchor: FloatingPanelIntrinsicLayoutAnchor(absoluteOffset: 0.0, referenceGuide: .superview),
result: (#line, constant: -420, firstAnchor: fpc.surfaceView.topAnchor, secondAnchor: fpc.view.bottomAnchor)),
(anchor: FloatingPanelIntrinsicLayoutAnchor(absoluteOffset: 42.0, referenceGuide: .superview),
result: (#line, constant: -420 + 42, firstAnchor: fpc.surfaceView.topAnchor, secondAnchor: fpc.view.bottomAnchor)),
(anchor: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 0.0, referenceGuide: .safeArea),
result: (#line, constant: -420, firstAnchor: fpc.surfaceView.topAnchor, secondAnchor: fpc.fp_safeAreaLayoutGuide.bottomAnchor)),
(anchor: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 0.5, referenceGuide: .safeArea),
result: (#line, constant: -210, firstAnchor: fpc.surfaceView.topAnchor, secondAnchor: fpc.fp_safeAreaLayoutGuide.bottomAnchor)),
(anchor: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 1.0, referenceGuide: .safeArea),
result: (#line, constant: 0, firstAnchor: fpc.surfaceView.topAnchor, secondAnchor: fpc.fp_safeAreaLayoutGuide.bottomAnchor)),
(anchor: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 0.0, referenceGuide: .superview),
result: (#line, constant: -420, firstAnchor: fpc.surfaceView.topAnchor, secondAnchor: fpc.view.bottomAnchor)),
(anchor: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 0.5, referenceGuide: .superview),
result: (#line, constant: -210, firstAnchor: fpc.surfaceView.topAnchor, secondAnchor: fpc.view.bottomAnchor)),
(anchor: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 1.0, referenceGuide: .superview),
result: (#line, constant: 0, firstAnchor: fpc.surfaceView.topAnchor, secondAnchor: fpc.view.bottomAnchor)),
] {
let c = prop.anchor.layoutConstraints(fpc, for: position)[0]
XCTAssertEqual(c.constant, CGFloat(prop.result.constant), line: UInt(prop.result.0))
XCTAssertEqual(c.firstAnchor, prop.result.firstAnchor, line: UInt(prop.result.0))
XCTAssertEqual(c.secondAnchor, prop.result.secondAnchor, line: UInt(prop.result.0))
}
}
}
private typealias LayoutSegmentTestParameter = (UInt, pos: CGFloat, forwardY: Bool, lower: FloatingPanelState?, upper: FloatingPanelState?)
private func assertLayoutSegment(_ floatingPanel: Core, with params: [LayoutSegmentTestParameter]) {
params.forEach { (line, pos, forwardY, lowr, upper) in
let segment = floatingPanel.layoutAdapter.segment(at: pos, forward: forwardY)
XCTAssertEqual(segment.lower, lowr, line: line)
XCTAssertEqual(segment.upper, upper, line: line)
}
}
private class CustomSafeAreaFloatingPanelController: FloatingPanelController {
override var fp_safeAreaInsets: UIEdgeInsets {
return UIEdgeInsets(top: 64.0, left: 0.0, bottom: 0.0, right: 34.0)
}
}
+24
View File
@@ -0,0 +1,24 @@
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
import XCTest
@testable import FloatingPanel
class StateTests: XCTestCase {
override func setUp() { }
override func tearDown() { }
func test_nextAndPre() {
var positions: [FloatingPanelState]
positions = [.full, .half, .tip, .hidden]
XCTAssertEqual(FloatingPanelState.full.next(in: positions), .half)
XCTAssertEqual(FloatingPanelState.full.pre(in: positions), .full)
XCTAssertEqual(FloatingPanelState.hidden.next(in: positions), .hidden)
XCTAssertEqual(FloatingPanelState.hidden.pre(in: positions), .tip)
positions = [.full, .hidden]
XCTAssertEqual(FloatingPanelState.full.next(in: positions), .hidden)
XCTAssertEqual(FloatingPanelState.full.pre(in: positions), .full)
XCTAssertEqual(FloatingPanelState.hidden.next(in: positions), .hidden)
XCTAssertEqual(FloatingPanelState.hidden.pre(in: positions), .full)
}
}
+230
View File
@@ -0,0 +1,230 @@
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
import XCTest
@testable import FloatingPanel
class SurfaceViewTests: XCTestCase {
override func setUp() {}
override func tearDown() {}
func test_surfaceView() {
let surface = SurfaceView(frame: CGRect(x: 0.0, y: 0.0, width: 320.0, height: 480.0))
XCTAssertTrue(SurfaceView.requiresConstraintBasedLayout)
XCTAssert(surface.contentView == nil)
surface.layoutIfNeeded()
XCTAssert(surface.grabberHandle.frame.minY == 6.0)
XCTAssert(surface.grabberHandle.frame.width == surface.grabberHandleSize.width)
XCTAssert(surface.grabberHandle.frame.height == surface.grabberHandleSize.height)
surface.backgroundColor = .red
surface.layoutIfNeeded()
XCTAssert(surface.backgroundColor == surface.containerView.backgroundColor)
}
func test_surfaceView_containerView() {
XCTContext.runActivity(named: "Bottom sheet") { _ in
let surface = SurfaceView(frame: CGRect(x: 0.0, y: 0.0, width: 320.0, height: 480.0))
XCTAssertNil(surface.contentView)
surface.layoutIfNeeded()
let height = surface.bounds.height * 2
surface.containerOverflow = height
surface.setNeedsLayout()
surface.layoutIfNeeded()
XCTAssertEqual(surface.containerView.frame, CGRect(x: 0.0, y: 0.0, width: 320.0, height: 480.0 * 3))
}
XCTContext.runActivity(named: "Top sheet") { _ in
let surface = SurfaceView(frame: CGRect(x: 0.0, y: 0.0, width: 320.0, height: 480.0))
surface.position = .top
XCTAssertNil(surface.contentView)
surface.layoutIfNeeded()
let height = surface.bounds.height * 2
surface.containerOverflow = height
surface.setNeedsLayout()
surface.layoutIfNeeded()
XCTAssertEqual(surface.containerView.frame, CGRect(x: 0.0, y: -height, width: 320.0, height: 480.0 * 3))
}
}
func test_surfaceView_contentView() {
XCTContext.runActivity(named: "Bottom sheet") { _ in
let surface = SurfaceView(frame: CGRect(x: 0.0, y: 0.0, width: 320.0, height: 480.0))
surface.layoutIfNeeded()
let contentView = UIView()
surface.set(contentView: contentView)
let height = surface.bounds.height * 2
surface.containerOverflow = height
surface.setNeedsLayout()
surface.layoutIfNeeded()
XCTAssertEqual(surface.contentView?.frame ?? .zero, surface.bounds)
}
XCTContext.runActivity(named: "Top sheet") { _ in
let surface = SurfaceView(frame: CGRect(x: 0.0, y: 0.0, width: 320.0, height: 480.0))
surface.position = .top
surface.layoutIfNeeded()
let contentView = UIView()
surface.set(contentView: contentView)
let height = surface.bounds.height * 2
surface.containerOverflow = height
surface.setNeedsLayout()
surface.layoutIfNeeded()
XCTAssertEqual(surface.containerView.frame, CGRect(x: 0.0, y: -height, width: 320.0, height: 480.0 * 3))
XCTAssertEqual(surface.convert(surface.contentView?.frame ?? .zero, from: surface.containerView),
surface.bounds)
}
}
func test_surfaceView_grabberHandle() {
XCTContext.runActivity(named: "Bottom sheet") { _ in
let surface = SurfaceView(frame: CGRect(x: 0.0, y: 0.0, width: 320.0, height: 480.0))
XCTAssertNil(surface.contentView)
surface.layoutIfNeeded()
XCTAssertEqual(surface.grabberHandle.frame.minY, 6.0)
XCTAssertEqual(surface.grabberHandle.frame.width, surface.grabberHandleSize.width)
XCTAssertEqual(surface.grabberHandle.frame.height, surface.grabberHandleSize.height)
surface.grabberHandlePadding = 10.0
surface.grabberHandleSize = CGSize(width: 44.0, height: 12.0)
surface.setNeedsLayout()
surface.layoutIfNeeded()
XCTAssertEqual(surface.grabberHandle.frame.minY, surface.grabberHandlePadding)
XCTAssertEqual(surface.grabberHandle.frame.width, surface.grabberHandleSize.width)
XCTAssertEqual(surface.grabberHandle.frame.height, surface.grabberHandleSize.height)
}
XCTContext.runActivity(named: "Top sheet") { _ in
let surface = SurfaceView(frame: CGRect(x: 0.0, y: 0.0, width: 320.0, height: 480.0))
surface.position = .top
XCTAssertNil(surface.contentView)
surface.layoutIfNeeded()
XCTAssertEqual(surface.grabberHandle.frame.maxY, (surface.bounds.maxY - 6.0))
XCTAssertEqual(surface.grabberHandle.frame.width, surface.grabberHandleSize.width)
XCTAssertEqual(surface.grabberHandle.frame.height, surface.grabberHandleSize.height)
surface.grabberHandlePadding = 10.0
surface.setNeedsLayout()
surface.layoutIfNeeded()
XCTAssertEqual(surface.grabberHandle.frame.maxY, surface.bounds.maxY - surface.grabberHandlePadding)
}
}
func test_surfaceView_contentMargins() {
XCTContext.runActivity(named: "Top sheet") { _ in
let surface = SurfaceView(frame: CGRect(x: 0.0, y: 0.0, width: 320.0, height: 480.0))
surface.position = .top
surface.layoutIfNeeded()
XCTAssertEqual(surface.containerView.frame, surface.bounds)
surface.containerMargins = UIEdgeInsets(top: 16.0, left: 16.0, bottom: 16.0, right: 16.0)
surface.setNeedsLayout()
surface.layoutIfNeeded()
XCTAssertEqual(surface.containerView.frame, surface.bounds.inset(by: surface.containerMargins))
}
XCTContext.runActivity(named: "Bottom sheet") { _ in
let surface = SurfaceView(frame: CGRect(x: 0.0, y: 0.0, width: 320.0, height: 480.0))
surface.layoutIfNeeded()
XCTAssertEqual(surface.containerView.frame, surface.bounds)
surface.containerMargins = UIEdgeInsets(top: 16.0, left: 16.0, bottom: 16.0, right: 16.0)
surface.setNeedsLayout()
surface.layoutIfNeeded()
XCTAssertEqual(surface.containerView.frame, surface.bounds.inset(by: surface.containerMargins))
}
}
func test_surfaceView_contentInsets() {
XCTContext.runActivity(named: "Top sheet") { _ in
let surface = SurfaceView(frame: CGRect(x: 0.0, y: 0.0, width: 320.0, height: 480.0))
surface.position = .top
let contentView = UIView()
surface.set(contentView: contentView)
surface.layoutIfNeeded()
XCTAssertEqual(surface.contentView?.frame ?? .zero, surface.bounds)
surface.contentPadding = UIEdgeInsets(top: 16.0, left: 16.0, bottom: 16.0, right: 16.0)
surface.setNeedsLayout()
surface.layoutIfNeeded()
XCTAssertEqual(surface.contentView?.frame ?? .zero, surface.bounds.inset(by: surface.contentPadding))
}
XCTContext.runActivity(named: "Bottom sheet") { _ in
let surface = SurfaceView(frame: CGRect(x: 0.0, y: 0.0, width: 320.0, height: 480.0))
let contentView = UIView()
surface.set(contentView: contentView)
surface.layoutIfNeeded()
XCTAssertEqual(surface.contentView?.frame ?? .zero, surface.bounds)
surface.contentPadding = UIEdgeInsets(top: 16.0, left: 16.0, bottom: 16.0, right: 16.0)
surface.setNeedsLayout()
surface.layoutIfNeeded()
XCTAssertEqual(surface.contentView?.frame ?? .zero, surface.bounds.inset(by: surface.contentPadding))
}
}
func test_surfaceView_containerMargins_and_contentInsets() {
let surface = SurfaceView(frame: CGRect(x: 0.0, y: 0.0, width: 320.0, height: 480.0))
let contentView = UIView()
surface.set(contentView: contentView)
surface.layoutIfNeeded()
XCTAssertEqual(surface.contentView?.frame ?? .zero, surface.bounds)
surface.containerMargins = UIEdgeInsets(top: 16.0, left: 16.0, bottom: 16.0, right: 16.0)
surface.contentPadding = UIEdgeInsets(top: 16.0, left: 16.0, bottom: 16.0, right: 16.0)
surface.setNeedsLayout()
surface.layoutIfNeeded()
XCTAssertEqual(surface.containerView.frame, surface.bounds.inset(by: surface.containerMargins))
XCTAssertEqual(surface.contentView?.frame ?? .zero, surface.containerView.bounds.inset(by: surface.contentPadding))
}
func test_surfaceView_cornderRaduis() {
let surface = SurfaceView(frame: CGRect(x: 0.0, y: 0.0, width: 320.0, height: 480.0))
XCTAssert(surface.containerView.layer.cornerRadius == 0.0)
XCTAssert(surface.containerView.layer.masksToBounds == false)
let appearance = SurfaceAppearance()
appearance.cornerRadius = 10.0
surface.appearance = appearance
surface.layoutIfNeeded()
XCTAssert(surface.containerView.layer.cornerRadius == 10.0)
XCTAssert(surface.containerView.layer.masksToBounds == true)
surface.containerView.layer.cornerRadius = 12.0
surface.layoutIfNeeded()
XCTAssert(surface.containerView.layer.cornerRadius == 12.0)
XCTAssert(surface.containerView.layer.masksToBounds == true)
appearance.cornerRadius = 0.0
surface.appearance = appearance
surface.layoutIfNeeded()
XCTAssert(surface.containerView.layer.cornerRadius == 0.0)
XCTAssert(surface.containerView.layer.masksToBounds == false)
surface.containerView.layer.cornerRadius = 12.0 // Don't change it directly
XCTAssert(surface.containerView.layer.cornerRadius == 12.0)
XCTAssertFalse(surface.containerView.layer.masksToBounds == true)
surface.setNeedsLayout()
surface.layoutIfNeeded()
// Reset corner radius by the current appearance
XCTAssert(surface.containerView.layer.cornerRadius == 0.0)
XCTAssert(surface.containerView.layer.masksToBounds == false)
}
func test_surfaceView_border() {
let surface = SurfaceView(frame: CGRect(x: 0.0, y: 0.0, width: 320.0, height: 480.0))
XCTAssert(surface.containerView.layer.borderWidth == 0.0)
let appearance = SurfaceAppearance()
appearance.borderColor = .red
appearance.borderWidth = 3.0
surface.appearance = appearance
surface.layoutIfNeeded()
XCTAssert(surface.containerView.layer.borderColor == UIColor.red.cgColor)
XCTAssert(surface.containerView.layer.borderWidth == 3.0)
}
}
+79
View File
@@ -0,0 +1,79 @@
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
import Foundation
@testable import FloatingPanel
func waitRunLoop(secs: TimeInterval = 0) {
RunLoop.main.run(until: Date(timeIntervalSinceNow: secs))
}
extension FloatingPanelController {
func showForTest() {
loadViewIfNeeded()
view.frame = CGRect(x: 0, y: 0, width: 375, height: 667)
show(animated: false, completion: nil)
}
}
class FloatingPanelTestDelegate: FloatingPanelControllerDelegate {
var position: FloatingPanelState = .hidden
var didMoveCallback: ((FloatingPanelController) -> Void)?
func floatingPanelDidChangePosition(_ vc: FloatingPanelController) {
position = vc.state
}
func floatingPanelDidMove(_ vc: FloatingPanelController) {
didMoveCallback?(vc)
}
}
class FloatingPanelTestLayout: FloatingPanelLayout {
let fullInset: CGFloat = 20.0
let halfInset: CGFloat = 250.0
let tipInset: CGFloat = 60.0
var initialState: FloatingPanelState {
return .half
}
var position: FloatingPanelPosition {
return .bottom
}
var referenceGuide: FloatingPanelLayoutReferenceGuide {
return .superview
}
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
return [
.full: FloatingPanelLayoutAnchor(absoluteInset: fullInset, edge: .top, referenceGuide: referenceGuide),
.half: FloatingPanelLayoutAnchor(absoluteInset: halfInset, edge: .bottom, referenceGuide: referenceGuide),
.tip: FloatingPanelLayoutAnchor(absoluteInset: tipInset, edge: .bottom, referenceGuide: referenceGuide),
]
}
}
class FloatingPanelTop2BottomTestLayout: FloatingPanelLayout {
let fullInset: CGFloat = 0.0
let halfInset: CGFloat = 250.0
let tipInset: CGFloat = 60.0
var initialState: FloatingPanelState {
return .half
}
var position: FloatingPanelPosition {
return .top
}
var referenceGuide: FloatingPanelLayoutReferenceGuide {
return .superview
}
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
return [
.full: FloatingPanelLayoutAnchor(absoluteInset: fullInset, edge: .bottom, referenceGuide: referenceGuide),
.half: FloatingPanelLayoutAnchor(absoluteInset: halfInset, edge: .top, referenceGuide: referenceGuide),
.tip: FloatingPanelLayoutAnchor(absoluteInset: tipInset, edge: .top, referenceGuide: referenceGuide),
]
}
}
class FloatingPanelProjectableBehavior: FloatingPanelBehavior {
func shouldProjectMomentum(_ fpc: FloatingPanelController, to proposedTargetPosition: FloatingPanelState) -> Bool {
return true
}
}
@@ -1,7 +1,4 @@
//
// Created by Shin Yamamoto on 2018/11/01.
// Copyright © 2018 Shin Yamamoto. All rights reserved.
//
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
import UIKit
@UIApplicationMain
+12
View File
@@ -0,0 +1,12 @@
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
import XCTest
@testable import FloatingPanel
class UtilTests: XCTestCase {
func test_displayTrunc() {
XCTAssertEqual(displayTrunc(333.222, by: 3), 333.3333333333333)
XCTAssertNotEqual(displayTrunc(333.5, by: 3), 333.66666666666674)
XCTAssertTrue(displayEqual(333.5, 333.66666666666674, by: 3))
}
}