Compare commits

...

254 Commits

Author SHA1 Message Date
Shin Yamamoto 0461c49d23 Release v1.6.6 2019-09-27 23:47:36 +09:00
Shin Yamamoto c4f7fa5332 Remove target-action for an untracked scroll view 2019-09-27 23:47:36 +09:00
Shin Yamamoto 3a9f304735 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-27 23:47:36 +09:00
Shin Yamamoto 1d5cb1744f 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-27 23:47:36 +09:00
Shin Yamamoto b61b0b5451 Update framework links of Example apps
Try to resolve an issue to need a clean-build of framework as following
this site,
https://developer.apple.com/library/archive/technotes/tn2435/_index.html
2019-09-27 23:47:36 +09:00
Shin Yamamoto e3a4631e44 Remove unused delegate conformances 2019-09-27 23:47:36 +09:00
Shin Yamamoto d7f798e9a0 Fix layout of the root table view in Samples 2019-09-27 23:47:36 +09:00
Shin Yamamoto ae2c83e32b Modify modal style of Samples.app for iOS 13 2019-09-27 23:47:36 +09:00
Shin Yamamoto 059b2ed4f0 Fix File 'Utils.swift' target 2019-09-27 23:47:36 +09:00
Shin Yamamoto 338658cd9f Clean up some tests
* test_surfaceView_constraintsUpdate()
* test_moveTo()
2019-09-25 22:16:45 +09:00
Shin Yamamoto 5325e707e6 Release v1.6.5 2019-08-31 12:51:01 +09:00
Nikolay Derkach 1dc0a6b76a 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:49:59 +09:00
David Hart 2689d68bab Improve floatingPanelDidChangePosition and tigger it on removal 2019-08-31 12:49:57 +09:00
Shin Yamamoto 218a12962f Don't unregister safeAreaInsetsObservation in hide()
Because the observation can't be active when a user makes a panel visible
by move(to:) instead of show().
2019-08-24 15:37:15 +09:00
Shin Yamamoto 916d2ec76a Add move-to-hidden tests 2019-08-24 15:37:15 +09:00
Shin Yamamoto 1b1ba5deef Update README for UISearchController issue 2019-08-24 15:37:15 +09:00
Shin Yamamoto 58b2df4996 Fix UISearchBar's _searchField access 2019-08-24 09:54:54 +09:00
Shin Yamamoto 3b812be84e Return true for FloatingPanelSurfaceView.requiresConstraintBasedLayout 2019-08-13 15:27:27 +09:00
Shin Yamamoto 5d86bd5d02 Release v1.6.4 2019-08-09 09:55:04 +09:00
Shin Yamamoto 3b6271c4f4 Fix stopping a panel b/w anchors after an interruption
The panel(surface view) could stop b/w anchors if the pan gesture doesn't
pass through `.changed` state after an interruptible animator is interrupted.

The possible reason is the constraints have never changed since the last animation
is committed so that `surfaceView.superview!.layoutIfNeeded()` doesn't trigger
a layout update by the constraint-based layout system in
`FloatingPanelLayoutAdapter.activateLayout(of:)`.

Thus the inserted code changes a panel interactive constraint by the least
positive number. It allows the constraint-based layout system to update the
surface layout expectedly.
2019-08-08 10:52:51 +09:00
Shin Yamamoto 1671a3d50f Always call startInteraction before endInteraction 2019-08-07 22:27:13 +09:00
Shin Yamamoto 0ab318e804 Fix not calling floatingPanelDidEndDecelerating delegate after interruption
If the decelerating animation is interrupted and
floatingPanelShouldBeginDragging delegate method returns false,
floatingPanelDidEndDecelerating delegate method will not be called
after calling floatingPanelWillBeginDecelerating method.

A panel have to run an animation after the interruption and also
floatingPanelDidEndDecelerating(_:) delegate should be called always
after calling floatingPanelWillBeginDecelerating method.

Therefore floatingPanelShouldBeginDragging delegate method shouldn't be
called in the panel decelerating.
2019-08-07 22:07:35 +09:00
Shin Yamamoto 7df352a44b Release v1.6.3 2019-07-26 19:03:53 +09:00
Shin Yamamoto 1443d377ad ci: reorder build stages
CocoaPods stage can be failed at `pod spec lint` job if the git tag hasn't
existing yet. We can restart the job after the tag is pushed so it's
best to run it as the last job to ensure other builds are passed.

It's helpful on the release workflow.
2019-07-26 16:23:37 +09:00
Shin Yamamoto e0bca25411 Fix scroll lock just before/after dragging down in the grabber area 2019-07-26 15:57:45 +09:00
Shin Yamamoto e94d47b1a5 Fix SafeArea insets update
According to 5c0ed4c commit, `floatingPanel.isDecelerating` is needed
only on iOS 10. The flag causes a problem of the safe area update with
the parent view controller containing a large title navigation bar.

And then the large title navigation bar has been introduced since iOS 11.

So the guard condition should be working only for iOS 10.
2019-07-25 14:15:02 +09:00
Shin Yamamoto 9d3a1674c4 Fix scroll unlock 2019-07-22 10:56:17 +09:00
Shin Yamamoto 24d81a4153 Merge pull request #239 from SCENEE/fix-regressions
Fix regressions
2019-07-22 09:41:51 +09:00
Shin Yamamoto 5723a8017b Stop the edge bouncing when a tracking scroll is decelerating 2019-07-20 16:47:37 +09:00
Shin Yamamoto 72055cd998 Add an Attention comment 2019-07-20 10:51:43 +09:00
Shin Yamamoto 9cd8b4d960 Fix preserveContentVCLayoutIfNeeded() 2019-07-20 00:07:13 +09:00
Shin Yamamoto f39b368c1e Fix bottomMaxY 2019-07-19 23:39:55 +09:00
Shin Yamamoto a4543351fe Modify the guard of an animation interruption
The previous condition disturb the quick redirect action of a panel
around the top most position. However removing the condition causes a
buggy behavior by an interruption over the top buffer.

After the consideration, I decided to allow an interruption under the
top interaction buffer.
2019-07-19 23:39:40 +09:00
Shin Yamamoto 88ac013166 Fix scroll unlock again
`2ef096b` commit isn't correct. `self.animator` must be set to nil on
the animation interruption, or the interruption occurs repeatedly. To
unlock the scroll correctly, an unlock operation needs to be added in
the scroll pan gesture's callback.
2019-07-19 23:29:43 +09:00
Shin Yamamoto 5d336b9090 Merge pull request #238 from SCENEE/revert-interruptible-animator
* Revert isInterruptible property of the default animator
* Fix a scroll unlock on an animation interruption
* Remove the velocity vector limit
* Fix the bottom buffer of a removable panel
2019-07-19 19:38:05 +09:00
Shin Yamamoto 45b3209b9b Escape pod warnings
This warning blocks CI build,
> url: The URL (https://twitter.com/scenee) is not reachable.
2019-07-19 18:51:34 +09:00
Shin Yamamoto da16cf6ada Fix the bottom buffer of a removable panel 2019-07-19 18:51:34 +09:00
Shin Yamamoto 774a841fb5 Fix Tab3 sample 2019-07-19 18:51:34 +09:00
Shin Yamamoto 020ffdaa84 Remove the velocity vector limit 2019-07-19 18:51:33 +09:00
Shin Yamamoto 2ef096b3a0 Fix a scroll unlock on an animation interruption
Fix 2 cases on an animation interruption

1. A user interrupts a panel animation below the top Y.
    - On the case, a scroll indicator must not appear.
2. A user interrupts a pane animation and soon swipes it up to scroll a
content in a tracking scroll view.
    - On the case, a scroll indicator must appear.

NOTE: A UIViewPropertyAnimator which isn't interruptible doesn't stop
the animation even if `self.animator` is set to nil. As a result, the
completion block is called after an interruption and a panel is moving
a bit like going against a user's dragging.

According to the behavior, the scroll unlock wasn't be able to work
expectedly if `self.animator` was set to nil on the interruption and then
I allow a delay until a scroll view is unlocked on the animation completion.
2019-07-19 18:51:33 +09:00
Shin Yamamoto 69bde3e80d Revert isInterruptible property of the default animator
Because it causes an unexpected propagation of the spring animation to
the content view. The propagation is reproduced on `fitToVisible` mode.
2019-07-19 13:25:54 +09:00
Shin Yamamoto e6aa7db35a Merge pull request #234 from SCENEE/fix-hidden-position
Fix hidden position and animation interruption
2019-07-15 12:59:55 +09:00
Shin Yamamoto 0124d98111 Remove a unecessary file ref 2019-07-15 11:12:14 +09:00
Shin Yamamoto c00a3836a5 Add tests for LayoutSegment 2019-07-15 11:12:14 +09:00
Shin Yamamoto 66f9118e78 Revise comments 2019-07-15 11:12:14 +09:00
Shin Yamamoto f261b90a73 Fix the removal interaction trigger 2019-07-13 14:15:37 +09:00
Shin Yamamoto a1602e0221 Fix fit-to-bounds behaviour
FloatingPanel.{fitToBounds,settle}(scrollView:) don't work because the
surface frame isn't updated expectedly by AutoLayout. Instead of that,
I update FloatingPanelLayoutAdapter.startInteraction(at:) to fit a
surface frame to a scroll offset content.
2019-07-12 11:07:46 +09:00
Shin Yamamoto b4e9ce8478 Fix scroll unlocking at the top position in dragging 2019-07-12 00:18:50 +09:00
Shin Yamamoto 35d7cbb1d3 Fix the animation interruption and scroll locking 2019-07-11 12:58:12 +09:00
Shin Yamamoto 6ab678bb18 Add SwiftPM section in README 2019-07-10 19:45:32 +09:00
Shin Yamamoto 14ec9cf0a1 Merge pull request #236 from SCENEE/release-1.6.2
Release v1.6.2
2019-07-10 19:44:20 +09:00
Shin Yamamoto a225bf2cf1 Release v1.6.2 2019-07-10 18:55:09 +09:00
Shin Yamamoto 9b904cd895 Merge branch 'master' into fix-hidden-position 2019-07-09 22:03:18 +09:00
Shin Yamamoto 11a16092a7 Merge pull request #231 from SCENEE/prevent-found-nil-error
Prevent 'unexpectedly found nil' fatal error
2019-07-09 21:53:40 +09:00
Shin Yamamoto b9b7f940b9 Prevent 'unexpectedly found nil' fatal error
Use FloatingPanel.viewcontroller as an optional value instead of
an implicitly unwrapping optional one.
2019-07-09 20:06:16 +09:00
Shin Yamamoto e542728ff6 Fix build break on Swift 4.1 2019-07-09 20:00:54 +09:00
Shin Yamamoto 1eeb6e2d73 Refactor FloatingPanelLayoutAdapter.{top,bottom}Y 2019-07-09 19:30:48 +09:00
Shin Yamamoto cf9d53aca2 Add test_updateInteractiveTopConstraint() 2019-07-09 19:30:48 +09:00
Shin Yamamoto 83463c792c Remove FloatingPanelLayoutAdapter.middleY 2019-07-09 19:30:31 +09:00
Shin Yamamoto d5c7571a97 Remove FloatingPanel.getPosition(at:with:directional:) 2019-07-09 19:27:18 +09:00
Shin Yamamoto 75c27bc232 Add test_getBackdropAlpha() 2019-07-06 16:16:22 +09:00
Shin Yamamoto cbcc35268d Add FloatingPanelPositionTests 2019-07-06 16:16:20 +09:00
Shin Yamamoto 11ba247ac4 Fix .hidden position's support
* Refactor FloatingPanel.targetPosition()
* Add test_targetPosition tests
* Fix bottomY
* Call shouldProjectMomentum(_:for:) only when a projection occurs on next
or pre segment. It means the delegate method not called for redirection.
* Improve all projection
2019-07-06 16:15:32 +09:00
Shin Yamamoto f411e81949 Add FloatingPanelControllerTests.test_moveTo() 2019-07-06 16:15:32 +09:00
Shin Yamamoto 45d7cb7218 Add FloatingPanelController.swhoForTest() 2019-07-06 16:15:32 +09:00
Shin Yamamoto 81f42d3951 Add LayoutSegment 2019-07-06 16:15:32 +09:00
Shin Yamamoto 2f7aed3e34 Add FloatingPanelPosition.{next,pre}(in:) 2019-07-06 16:15:32 +09:00
Shin Yamamoto 01f8261f0b Add an assertion to check an invalid move
- Add FloatingPanelLayoutAdapter.isValid(_:)
2019-07-06 16:15:32 +09:00
Shin Yamamoto 489d7696cc Add test_originSurfaceY 2019-07-06 16:15:32 +09:00
Shin Yamamoto 0661f08a07 Fix FloatingPanelLayoutTests 2019-07-03 14:25:57 +09:00
Shin Yamamoto 206475e6ab Merge pull request #232 from SCENEE/refactor-layout-adapter
Refactor layout adapter
2019-07-03 14:24:47 +09:00
Shin Yamamoto a4a68e5b39 Add test_surfaceView_constraintsUpdate() 2019-07-03 11:46:45 +09:00
Shin Yamamoto de7ab0e0cb Rename FloatingPanelViewTests to FloatingPanelSurfaceViewTests 2019-07-03 11:46:45 +09:00
Shin Yamamoto 5f7b5ce81c Add FloatingPanelLayoutTests & Utils 2019-07-03 11:46:45 +09:00
Shin Yamamoto 36d7ea5100 Improve testing speed 2019-07-03 11:34:21 +09:00
Shin Yamamoto 33f8cf3802 Modify FloatingPanel.distance(to:) 2019-07-03 11:34:21 +09:00
Shin Yamamoto f6da876fdf Add botomMostState prop 2019-07-03 11:34:21 +09:00
Shin Yamamoto 96c5dc7b74 Add FloatingPanelLayoutTests 2019-07-03 11:34:02 +09:00
Shin Yamamoto a37931b62d Merge pull request #230 from SCENEE/fix-scrollindicator
Fix the scroll indicator lock on a contentVC reset
2019-07-03 09:55:32 +09:00
Shin Yamamoto 5c848d9bf5 Fix the scroll indicator lock on a contentVC reset
The locking logic couldn't take care of the case where a content view
controller of a FloatingPanelController object is replaced.
2019-07-02 19:12:58 +09:00
Shin Yamamoto 265b805fa9 No more need FloatingPanel to conform UIScrollViewDelegate 2019-07-02 14:21:10 +09:00
Shin Yamamoto c4dfe33a5e Merge pull request #229 from SCENEE/release-1.6.1
Release v1.6.1
2019-06-29 09:31:17 +09:00
Shin Yamamoto 999eeb47ba Release v1.6.1 2019-06-29 08:33:24 +09:00
Shin Yamamoto a5bf02cfec Merge pull request #228 from SCENEE/fix-unexpected-layout-update
Fix an unexpected layout update on iOS13
2019-06-29 08:32:48 +09:00
Shin Yamamoto c10186e50a Prevent an unexpected layout update on iOS13
On iOS13, UITraitCollection.userInterfaceStyle can be changed
from .light to .dark when an app transitions to the background.
2019-06-29 07:41:52 +09:00
Shin Yamamoto 7a1cbf99d4 Rename setUpLayout to activateLayout 2019-06-28 20:23:10 +09:00
Shin Yamamoto c9c4000536 Merge pull request #225 from SCENEE/fix-seamless-scrolling
Remove workaround for tableView(_:didSelectRowAt:) issue
2019-06-19 10:34:57 +09:00
Shin Yamamoto 656bbc1b1c Remove workaround for tableView(_:didSelectRowAt:) issue
The workaround was added to avoid `tableView(_:didSelectRowAt:)` not
being called on first tap after the moving animation. However, it
doesn't only resolved the issue, but also has side effects.

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

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

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

> xctest (86533) encountered an error (Failed to load the test bundle. (Underlying error: The bundle “FloatingPanelTests” couldn’t be loaded because it is damaged or missing necessary resources. The bundle is damaged or missing necessary resources. dlopen_preflight(..omitted../Build/Products/Test-iphonesimulator/FloatingPanelTests.xctest/FloatingPanelTests): no suitable image found.  Did find:
>	    ..omitted../Build/Products/Test-iphonesimulator/FloatingPanelTests.xctest/FloatingPanelTests: mach-o, but not built for iOS simulator))
2019-05-25 16:06:56 +09:00
Shin Yamamoto 2f23520330 Improve ViewTests.test_WarningRetainCycle() 2019-05-25 16:06:56 +09:00
Shin Yamamoto a95694cbfc Add testing jobs in Travic CI 2019-05-25 16:06:56 +09:00
Shin Yamamoto 6cfba6495f Add TestingApp 2019-05-25 16:06:52 +09:00
Shin Yamamoto b9f3de1c64 Merge pull request #207 from SCENEE/improve-surface-w-lazy-props
Improve surface w lazy props
2019-05-21 22:56:02 +09:00
Shin Yamamoto c67b56e7af Clean up code 2019-05-17 22:54:47 +09:00
Shin Yamamoto bf39f07691 Use lazy properties in the surface view 2019-05-16 10:43:41 +09:00
Shin Yamamoto a9e46f0de6 Merge pull request #204 from SCENEE/fix-scroll-lock-2
Avoid calling {lock,unlock}ScrollView() unexpectedly
2019-05-16 10:41:04 +09:00
Shin Yamamoto 05478fa8fa Avoid calling {lock,unlock}ScrollView() unexpectedly 2019-05-11 15:16:58 +09:00
Shin Yamamoto d123afc3f7 Fix a crash 2019-05-11 13:40:58 +09:00
Shin Yamamoto b1b3c15300 Merge pull request #201 from SCENEE/fix-scroll-lock
Fix scroll lock
2019-05-11 12:51:53 +09:00
Shin Yamamoto 49bae50739 Merge pull request #203 from SCENEE/patch-pr194
Improve surface container API
2019-05-11 12:49:48 +09:00
Shin Yamamoto 9b5459af8e Fix the content height changed by a container inset 2019-05-11 12:11:04 +09:00
Shin Yamamoto 96d2ea57f5 Clean up prop naming 2019-05-11 11:53:05 +09:00
Shin Yamamoto b78c5f4ece Decouple between grabberTopPadding and containerTopInset 2019-05-11 11:02:06 +09:00
Shin Yamamoto 341522ccaa Fix grabberhandle corner 2019-05-11 11:02:06 +09:00
Shin Yamamoto 833628e42f Update the content view height by containerTopInset 2019-05-11 11:02:06 +09:00
Shin Yamamoto 50c1c6fdc9 Fix the dependency on ordering prop updates 2019-05-11 11:02:06 +09:00
Shin Yamamoto 213386e822 Fix the sign of containerTopInset 2019-05-11 11:02:06 +09:00
Shin Yamamoto 17317ed274 Merge pull request #194 from nderkach/master
Grabber handle and top offset customizations
2019-05-11 10:58:25 +09:00
Shin Yamamoto 652ae8c967 Remove a condition to prevent animation cancel
Because I can't confirm any effect to fix that selecting a table view cell
sometimes isn't working after flicking to half from full.
2019-05-04 16:15:10 +09:00
Shin Yamamoto ec0e8cbdaf Avoid any tap gesture recognition while dragging a panel 2019-05-04 16:03:19 +09:00
Shin Yamamoto c15d4c9035 Fix the moving animation's interruption 2019-05-04 16:03:19 +09:00
Shin Yamamoto 39dfdd0ef0 Remove an unused code 2019-05-04 16:03:19 +09:00
Shin Yamamoto d25bc58249 Add a sample for tap-to-move 2019-05-04 16:03:18 +09:00
Shin Yamamoto 194a197e83 Fix a scroll lock after moving a panel 2019-05-04 14:46:39 +09:00
Shin Yamamoto bd02f34bcf Merge pull request #196 from SCENEE/release-1.5.1
Release v1.5.1
2019-04-26 13:43:54 +09:00
Shin Yamamoto 7d5f03bb6e Release v1.5.1 2019-04-25 16:18:59 +09:00
Nikolay Derkach 60f41e168f configure grabber handle and top container offset 2019-04-24 14:07:46 +02:00
Shin Yamamoto 680b16aa25 Merge pull request #185 from SCENEE/fix-tap-abort
Fix a touch abort on the interaction interrupted
2019-04-24 14:54:20 +09:00
Shin Yamamoto 2394c03dca Polish a doc comment 2019-04-20 16:00:22 +09:00
Shin Yamamoto c8f211f2bf fix typo 2019-04-19 08:56:54 +09:00
Shin Yamamoto 9076ba8933 Prevent an UIScrollViewDelayedTouchesBeganGestureRecognizer failed
It causes tableView(_:didSelectRowAt:) not being called on first tap
after an nimation. The change hasn't fixed the problem completely, but
it works better.
2019-04-19 08:46:51 +09:00
Shin Yamamoto 08e79bfc5c Merge pull request #191 from SCENEE/add-pages-sample
Add pages sample
2019-04-17 19:22:40 +09:00
Shin Yamamoto e4808516aa Merge pull request #190 from SCENEE/fix-surface-mask
Fix surface mask
2019-04-17 19:21:56 +09:00
Shin Yamamoto 5888104e98 Remove obsolete SWIFT_WHOLE_MODULE_OPTIMIZATION setting
Removing this setting will fix a warning emitted by Xcode to update
project settings.
2019-04-17 09:36:36 +09:00
Shin Yamamoto 2b8d29759a Rename the remaining backgroundView variable 2019-04-16 22:53:06 +09:00
Shin Yamamoto 8e4b56ff17 Fix the doc comment 2019-04-16 07:51:27 +09:00
Shin Yamamoto 23c5761c14 Update README 2019-04-15 21:05:46 +09:00
Shin Yamamoto 835ec0c3a0 Replace 'backgroundView' with 'containerView' 2019-04-15 21:05:16 +09:00
Shin Yamamoto d2dce0b6f8 Add a doc comment 2019-04-15 21:00:19 +09:00
Shin Yamamoto 3f8628af01 Fix typo 2019-04-11 22:52:49 +09:00
Shin Yamamoto 40c6fae07c Add a sample for panels in UIPageViewController 2019-04-11 22:52:29 +09:00
Shin Yamamoto e1185fda93 Merge pull request #188 from SCENEE/allow-subclassing
Allow FloatingPanelController subclassing
2019-04-11 22:31:37 +09:00
Shin Yamamoto 458ed903c5 Update README 2019-04-11 11:28:35 +09:00
Shin Yamamoto 4b640f4f01 Add updateBorder() 2019-04-11 11:25:11 +09:00
Shin Yamamoto 8743c5efd0 Fix the mask bounds of the surface view
- Move `contentView` into `backgroundView` to fix the mask
- Also fix the mask problem on iOS 10-11. See for detail,
https://github.com/SCENEE/FloatingPanel/issues/109.
2019-04-11 11:21:16 +09:00
Shin Yamamoto 9bd9d31d40 Merge pull request #186 from zntfdr/patch-1
Fix FloatingPanelBehavior.swift typos
2019-04-08 21:24:48 +09:00
Federico Zanetello 7e3d720720 Fix FloatingPanelBehavior.swift typos 2019-04-08 08:26:30 +07:00
Shin Yamamoto 7a512191ab Fix a touch abort on the interaction interrupted
Because touch events seem to be cancelled when an animator is released.
2019-04-06 11:25:38 +09:00
Shin Yamamoto af767863bb Merge pull request #177 from SCENEE/support-swift5
[Release v1.5.0] Support both of Swift 5 and 4.2
2019-04-06 10:54:54 +09:00
Shin Yamamoto 1b233f4f87 Update README 2019-04-05 08:21:49 +09:00
Shin Yamamoto 3a840df79e Release v1.5.0 2019-04-04 12:08:27 +09:00
Shin Yamamoto 6851e3b072 Support both of Swift 5 and 4.2
The default Swift version leaves 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.

With regard to CocoaPods, the pod spec is going to support
`swift_versions` introduced in CocoaPods v1.7.0. For now, a user needs
to override `SWIFT_VERSION` appropriately in Podfile.
2019-04-04 12:08:27 +09:00
Shin Yamamoto 5a2b079872 Merge pull request #183 from SCENEE/release-1.4.1
Release 1.4.1
2019-04-04 09:40:10 +09:00
Shin Yamamoto f683f987d8 Release v1.4.1 2019-04-03 23:14:56 +09:00
Shin Yamamoto 3626621e87 Fix the umbrella header name 2019-04-03 23:14:56 +09:00
Shin Yamamoto cc2d1eb002 Merge pull request #178 from SCENEE/notify-retain-cycle
Notify a retain cycle
2019-04-03 23:13:20 +09:00
Shin Yamamoto 5ba19bcf8b Add an attention comment in Samples app 2019-04-03 21:23:49 +09:00
Shin Yamamoto 8391686e28 Clean up Logger
`.fault` log is removed because the level are intended for capturing
system-level or multi-process errors only in `os_log`.
2019-04-03 21:23:49 +09:00
Shin Yamamoto 38327c917f Add tests to quick-check warnings 2019-04-03 21:23:49 +09:00
Shin Yamamoto fca0f399b2 Merge pull request #181 from SCENEE/fix-loop-crash
Fix an infinite loop crash in iOS 10
2019-04-03 21:17:39 +09:00
Shin Yamamoto e3b7ac0e99 Add 'Test' configuration 2019-04-03 08:21:35 +09:00
Shin Yamamoto 04cd357f68 Print retain cycle warnings 2019-04-03 08:21:35 +09:00
Shin Yamamoto 68f48f714d Fix an infinite loop crash in iOS 10 2019-04-02 21:47:56 +09:00
Shin Yamamoto fa586c494f Merge pull request #180 from SCENEE/fix-scroll-fitting
Fix scroll fitting
2019-04-02 09:43:37 +09:00
Shin Yamamoto b886a0da64 Fix scroll fitting
This is to prevent a surface frame jump on scroll fitting. In fact,
a scroll offset range for fitting should be -10..<0.
2019-03-30 18:52:48 +09:00
Shin Yamamoto 0616aec3d2 Prevent an unexpected layout update
The KVO for `safeAreaInset` can be invoked even when new and old values
are the same value so that the surface of a floating panel sometimes
jumps in dragging by an unexpected layout update called from a
`safeAreaInset` update.
2019-03-30 15:45:41 +09:00
Shin Yamamoto 5df36a6601 Allow FloatingPanelController subclassing 2019-03-29 21:20:02 +09:00
Shin Yamamoto 7d6f295e72 Merge pull request #169 from SCENEE/fix-ambiguous-cornerradius
Open FloatingPanelSurfaceView.backgroundView
2019-03-29 21:05:25 +09:00
Shin Yamamoto 1c952b6dcb Open FloatingPanelSurfaceView.backgroundView
This change lets a user be able to set up a corner radius and shadow of
the surface view, and has the library not update them unnecessarily.

As a result, a user can escape "Ambiguous use of 'cornerRadius'" error
as below, if it's used with a `cornerRadius` property of `UIView`
extension defined by the user.

```swift
extension UIView {
    @objc dynamic var cornerRadius: CGFloat {
        get { return self.layer.cornerRadius }
        set(cornerRadius) {
            self.layer.masksToBounds = true
            self.layer.cornerRadius = cornerRadius
        }
    }
}

...

public extension FloatingPanelSurfaceView {
    @objc public dynamic var fp_cornerRadius: CGFloat {
        get { return backgroundView.layer.cornerRadius }
        set {
             if #available(iOS 11.0, *) {
                 contentView?.layer.masksToBounds = true
                 contentView?.layer.cornerRadius = newValue
                 contentView?.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
             }
             backgroundView.layer.masksToBounds = true
             backgroundView.layer.cornerRadius = newValue
       }
    }
}
```

squash! Open FloatingPanelSurfaceView.backgroundView
2019-03-29 20:34:11 +09:00
Shin Yamamoto 7e7c2a0fd7 Merge pull request #170 from SCENEE/fix-top-dragging-boundary
Fix top dragging boundary
2019-03-27 22:41:56 +09:00
Shin Yamamoto a15444d237 Modify the height calc with a top-most position
According to the previous change, `.half` position meaning is
also changed so that the height calc logic must be modified.

Previously, for example, `.half` position has 2 meaning. One is
a position at half of a screen. Another is a position when a
content is visible half. `.tip` position is also same.

Now `.half`/`.tip` position can display a full content if it's
top most. In the case, the surface height should fit to a height
for `.half`/`.tip` position.
2019-03-26 22:23:27 +09:00
Shin Yamamoto 5b100f3b22 Add FloatingPanelLayout.topMostState
The previous implementation has an implicit pre-condition where .full
position is supposed to be top most. It also means .full position should
be included in a set of the supported positions. But it's not
appropriate because there is a case when a user wants to configure a top
most position of a layout with a bottom inset. This commit lets a panel
work well even if .half/.tip position is top most.

According to the change, .full position means a position where a content
is not just visible fully, but also filled in a full screen almost.
2019-03-26 22:23:16 +09:00
Shin Yamamoto 9fa8a48c56 Fix top panning boundary 2019-03-26 11:41:13 +09:00
Shin Yamamoto cb54a2a7e1 Merge pull request #151 from SCENEE/remove-swapping-scroll-delegate
Remove swapping delegate
2019-03-26 09:55:47 +09:00
Shin Yamamoto ed02713ccc Merge pull request #168 from SCENEE/fix-safeareainsets
Change FloatingPanelLayoutAdapter.safeAreaInsets as a read-only property
2019-03-25 09:50:46 +09:00
Shin Yamamoto 68e3fd2093 ChangeFloatingPanelLayoutAdapter.safeAreaInsets as a read-only property
This solves the problem that a panel's layout and dragging can be
inappropriate. The root cause is that a value of the `safeAreaInsets`
is sometimes different from an actual value referenced from Auto Layout.
It's led by the design that the `safeAreaInsets` is defined as a stored
property. Therefore this commit resolves the issue.
2019-03-23 18:36:26 +09:00
Shin Yamamoto a43f73d7b1 Merge pull request #159 from SCENEE/fix-interruption
Fix interruption
2019-03-20 21:39:20 +09:00
Shin Yamamoto 61e0c4ed0a Merge pull request #165 from SCENEE/fix-ui-extensions
Fix UI extensions
2019-03-20 21:38:28 +09:00
Shin Yamamoto 53e0629b1d Merge pull request #162 from zntfdr/patch-2
Fix typo
2019-03-19 09:31:13 +09:00
Shin Yamamoto 2b1d6a3d8a Fix 'Redundant conformance' error of UIGestureRecognizerState 2019-03-18 10:21:36 +09:00
Federico Zanetello 5ba1fb3d95 Add missing space 2019-03-16 12:42:51 +07:00
Federico Zanetello 1b7c15cdb5 Fix typo 2019-03-16 12:38:54 +07:00
Shin Yamamoto 81fd85e993 Prevent bouncing a scroll content in a panel animating
This issue happens when a panel interacts from tip to half
in 'Show Tab Bar' -> 'Tab 3'.
2019-03-09 10:41:26 +09:00
Shin Yamamoto 8f4c08d5b3 Fix TabBarContentViewController for Tab 3 2019-03-09 10:36:31 +09:00
Shin Yamamoto 61b6429851 Fix the buggy top buffer in scroll tracking
Should not handle a panel gesture even when a zero scroll velocity.
If not, it causes that a scroll view will be handled after a panel
moves a bit in a top buffer. As a result, a panel frame jumps from
a location in a top buffer to topY.
2019-03-09 10:04:18 +09:00
Shin Yamamoto bbc6b39c08 Fix issues on an animation interruption
* Adjust a final position to work scroll tracking well
* Fix issues on an animation cancellation
2019-03-09 10:04:18 +09:00
Shin Yamamoto 3f812f4d6d Merge pull request #156 from SCENEE/fix-layout-assertion
Fix the layout assertion
2019-03-09 10:03:44 +09:00
Shin Yamamoto c9b15e4239 Merge pull request #158 from bryansum/master
Fix retain cycle in FloatingPanel
2019-03-09 09:57:11 +09:00
Bryan Summersett 5fbdb3d481 Make floatingPanel weak 2019-03-08 15:02:33 -08:00
Shin Yamamoto c59e1cd7fc Fix the layout assertion
The assertion didn't allow `.hidden` initial position.
2019-03-08 19:39:10 +09:00
Shin Yamamoto ce891e47da Merge pull request #155 from kfinteractive/master
Adds support for respecting initial safeAreaInsets.
2019-03-08 19:23:58 +09:00
Gunnar Herzog 7160e4a42e Removes unnecessary guard statement. 2019-03-07 08:56:29 +01:00
Gunnar Herzog eba857a285 Adds support for respecting initial safeAreaInsets. 2019-03-07 08:45:05 +01:00
Shin Yamamoto a1dd02c780 Stop scroll deceleration without swapping delegate 2019-03-05 08:31:27 +09:00
Shin Yamamoto 87ff5d629b Merge pull request #143 from SCENEE/release-v1.4.0
Release v1.4.0
2019-02-27 10:00:47 +09:00
Shin Yamamoto 6b75523428 Release v1.4.0 2019-02-27 09:33:14 +09:00
Shin Yamamoto 773434d4f6 Merge pull request #119 from SCENEE/improve-operability
Improve the operability
2019-02-27 09:32:15 +09:00
Shin Yamamoto ad6dcd0314 Clean up sample 2019-02-27 09:04:42 +09:00
Shin Yamamoto b9e29ad87d Fix typo 2019-02-26 10:07:30 +09:00
Shin Yamamoto 32b965ba87 Merge pull request #142 from zntfdr/patch-1
Add README missing character
2019-02-26 09:22:30 +09:00
Federico Zanetello f1b315c9ea Add missing character 2019-02-25 12:50:37 +07:00
Shin Yamamoto 459fc75af3 Tidy up logs 2019-02-25 13:02:49 +09:00
Shin Yamamoto 9b0cd3511f Revamp scroll draw-down action
A scroll view offset often isn't around zero by a user action.
Therefore, rather than checking a offset value, it's working well
to fits a scroll view offset to the surface view bounds by
manipulating their frames a bit temporarily.
2019-02-25 13:02:49 +09:00
Shin Yamamoto af9b988507 Merge pull request #136 from SCENEE/release-v1.3.5
Release v1.3.5
2019-02-23 11:18:21 +09:00
Shin Yamamoto 36f297c35b Allow the grabber area just on touch began 2019-02-23 10:28:34 +09:00
Shin Yamamoto ff959f71a7 Prevent the unexpected scrolling on full state by the second finger 2019-02-23 10:28:34 +09:00
Shin Yamamoto 0a4312ada6 Reduce logger function calls 2019-02-23 10:28:34 +09:00
Shin Yamamoto 5411cdc07a Change time to call floatingPanelWillBeginDecelerating() 2019-02-23 10:28:34 +09:00
Shin Yamamoto a8c6fba3c1 Call floatingPanelDidEndDecelerating(_:) when an animation is interruptted 2019-02-23 10:28:34 +09:00
Shin Yamamoto 11b115b47b Tear down the active interaction in moving programmatically 2019-02-23 10:28:34 +09:00
Shin Yamamoto 22edf5ce46 Prevent unexpected crash
This assertion can be called unexpectedly by the gesture recognizer behavior
of UIScrollView
2019-02-23 10:28:34 +09:00
Shin Yamamoto f43f7df7f3 Prevent calling didMove delegate when it's not moving 2019-02-23 10:28:34 +09:00
Shin Yamamoto 3a2633d818 Must update a panel layout after it becomes a new state
To synchronize layout updates in floatingPanelDidChangePosition(_:) by
a user
2019-02-23 10:28:34 +09:00
Shin Yamamoto 04a62bcf74 Refactor FloatingPanelSurfaceView for AutoLayout backed animation 2019-02-23 10:28:34 +09:00
Shin Yamamoto 6c1320168c Fix a buggy scroll offset on a scroll inset change by a user
* The controller can bother a user who wants to adjust a scroll offset
for a position at `floatingPanelDidChangePosition()` delegate method.
* This prevents interrupting a tracking scroll offset at the end of interaction
2019-02-23 10:28:34 +09:00
Shin Yamamoto 8657c91002 Fix the boundary of the interactive top constraint 2019-02-23 10:28:34 +09:00
Shin Yamamoto bafe492009 Normalize the projected position
It resolves the behavior is determined by the screen size.
2019-02-23 10:28:34 +09:00
Shin Yamamoto c6197ef6a3 Modify the default momentum projection behavior 2019-02-23 10:28:34 +09:00
Shin Yamamoto 1b3f16bcd5 Fix disabling bottom constraints 2019-02-23 10:28:34 +09:00
Shin Yamamoto 28712fdeca Add ThreeTabBarPanelLayout as an advanced layout sample 2019-02-23 10:28:34 +09:00
Shin Yamamoto 0c30b68a9e Update FloatingPanelFullScreenLayout spec
It's useful that all insets(tip, half and full) indicates a inset
from the superview, not the safe area because a user can layout
a floating panel regardless of the safe area or layout guides(iOS10)
whose values depend on iOS system behavior.
2019-02-21 19:19:48 +09:00
Shin Yamamoto 30c4bee432 Remove FloatingPanel.getCurrentY(from:with) 2019-02-21 19:19:48 +09:00
Shin Yamamoto ece9ced085 Configure momentum project with FloatingPanelBehavior
* shouldProjectMomentum(_:for:)
* momentumProjectionRate(_:)
2019-02-21 19:19:48 +09:00
Shin Yamamoto f231105752 Enable to cancel tracking a scroll view 2019-02-21 19:19:48 +09:00
Shin Yamamoto 91dfc1e086 Open the default layout/behavior 2019-02-21 19:19:48 +09:00
Shin Yamamoto b2c59c17aa Modify setBackdropAlpha(of:) as private 2019-02-21 19:19:48 +09:00
Shin Yamamoto 10d1a920f0 Add floatingPanelShouldBeginDragging(_:) delegate method
* Update doc comments
2019-02-21 19:19:48 +09:00
Shin Yamamoto 4cb79a14fc Move the surface view with a top layout constraint 2019-02-21 19:19:48 +09:00
Shin Yamamoto 402b9bd8dc Release v1.3.5 2019-02-21 18:42:02 +09:00
Shin Yamamoto c39cc9d93b Fix a regression of the interaction 2019-02-21 18:42:02 +09:00
Shin Yamamoto aad56ab0a7 Merge pull request #135 from SCENEE/fix-interaction
Fix interaction
2019-02-16 14:51:56 +09:00
Shin Yamamoto fe18e493a9 Fix dragging outside safe area 2019-02-16 10:31:17 +09:00
Shin Yamamoto 5d14166508 Fix an interruptive animator handling
* A velocity parameter passed by an animator had a wrong sign so that an
interruptive animator could be buggy.
* Improve the default pan gesture recognizer to interrupt an animation smoothly.
2019-02-16 10:29:11 +09:00
Shin Yamamoto e1a745e3b5 Clean up 2019-02-15 15:02:10 +09:00
Shin Yamamoto a0cac28ed0 Improve {re}directional position calc 2019-02-15 15:02:10 +09:00
Shin Yamamoto c205dc8672 Improve animator handling 2019-02-15 15:02:10 +09:00
Shin Yamamoto 5c0ed4cf7d Fix the wrong layout update on iOS10
On iOS 10, there is a case when a floating panel is updated by a
different position(the previous position) from the target position in
animating. This is because `FloatingPanelController` calls
`update(safeAreaInsets:)` in `viewDidLayoutSubviews()` unexpectedly.
2019-02-15 10:29:01 +09:00
Shin Yamamoto 780472a17f Stop unexpected scrolling by decelerating on tip and half 2019-02-15 10:15:34 +09:00
Shin Yamamoto 0264db3d54 Should always recognize tap/long press gestures in parallel 2019-02-15 09:55:36 +09:00
Shin Yamamoto c117594669 Merge pull request #132 from SCENEE/release-v1.3.4
Release v1.3.4
2019-02-14 09:45:31 +09:00
34 changed files with 3139 additions and 858 deletions
+1
View File
@@ -22,6 +22,7 @@ xcuserdata/
*.moved-aside
*.xccheckout
*.xcscmblueprint
*.xcsettings
## Obj-C/Swift specific
*.hmap
+40 -29
View File
@@ -1,8 +1,7 @@
language: swift
language: objective-c
branches:
only:
- master
- next
cache:
directories:
- /usr/local/Homebrew
@@ -13,43 +12,55 @@ env:
global:
- LANG=en_US.UTF-8
- LC_ALL=en_US.UTF-8
skip_cleanup: true
jobs:
include:
- stage: Build framework(swift 4.1)
- stage: "Builds"
osx_image: xcode9.4
script:
- xcodebuild -scheme FloatingPanel clean build
- stage: Build framework(swift 4.2)
script: xcodebuild -scheme FloatingPanel SWIFT_VERSION=4.1 clean build
name: "Swift 4.1"
- script: xcodebuild -scheme FloatingPanel SWIFT_VERSION=4.2 clean build
osx_image: xcode10
script:
- xcodebuild -scheme FloatingPanel clean build
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
# SUPPORTS_MACCATALYST=NO because Xcode 11 runs on macOS 10.14 in Travis CI
osx_image: xcode11
name: "Swift 5.1"
- stage: "Tests"
osx_image: xcode10.2
script: xcodebuild clean test -scheme FloatingPanel -workspace FloatingPanel.xcworkspace -destination 'platform=iOS Simulator,OS=10.3.1,name=iPhone SE'
name: "iPhone SE (iOS 10.3)"
- script: xcodebuild clean test -scheme FloatingPanel -workspace FloatingPanel.xcworkspace -destination 'platform=iOS Simulator,OS=11.4,name=iPhone 7'
osx_image: xcode10.2
name: "iPhone 7 (iOS 11.4)"
- script: xcodebuild clean test -scheme FloatingPanel -workspace FloatingPanel.xcworkspace -destination 'platform=iOS Simulator,OS=12.2,name=iPhone X'
osx_image: xcode10.2
name: "iPhone X (iOS 12.2)"
- stage: Build examples
osx_image: xcode10.2
script: xcodebuild -scheme Maps -sdk iphonesimulator clean build
name: "Maps"
- script: xcodebuild -scheme Stocks -sdk iphonesimulator clean build
osx_image: xcode10.2
name: "Stocks"
- script: xcodebuild -scheme Samples -sdk iphonesimulator clean build
osx_image: xcode10.2
name: "Samples"
- stage: Carthage
osx_image: xcode10
osx_image: xcode10.2
before_install:
- brew update
- brew outdated carthage || brew upgrade carthage
script:
- carthage build --no-skip-current
- stage: Podspec
osx_image: xcode10
- stage: CocoaPods
osx_image: xcode10.2
script:
- pod spec lint
- stage: Build maps example
osx_image: xcode10
script:
- xcodebuild -scheme Maps -sdk iphonesimulator clean build
- stage: Build stocks example
osx_image: xcode10
script:
- xcodebuild -scheme Stocks -sdk iphonesimulator clean build
- stage: Build samples example
osx_image: xcode10
script:
- xcodebuild -scheme Samples -sdk iphonesimulator clean build
- pod spec lint --allow-warnings
- pod lib lint --allow-warnings
+8 -8
View File
@@ -7,13 +7,13 @@
objects = {
/* Begin PBXBuildFile section */
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 */; };
54B5112C216C3D840033A6F3 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B5112B216C3D840033A6F3 /* ViewController.swift */; };
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 */; };
54B5113F216C407F0033A6F3 /* FloatingPanel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 54B5113E216C407F0033A6F3 /* FloatingPanel.framework */; };
54B51140216C407F0033A6F3 /* FloatingPanel.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 54B5113E216C407F0033A6F3 /* FloatingPanel.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
@@ -23,7 +23,7 @@
dstPath = "";
dstSubfolderSpec = 10;
files = (
54B51140216C407F0033A6F3 /* FloatingPanel.framework in Embed Frameworks */,
549D23D3233C77D5008EF4D7 /* FloatingPanel.framework in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
@@ -31,6 +31,7 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
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>"; };
54B5112B216C3D840033A6F3 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
@@ -38,7 +39,6 @@
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>"; };
54B5113E216C407F0033A6F3 /* FloatingPanel.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = FloatingPanel.framework; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -46,7 +46,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
54B5113F216C407F0033A6F3 /* FloatingPanel.framework in Frameworks */,
549D23D2233C77D5008EF4D7 /* FloatingPanel.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -56,7 +56,7 @@
54B5111D216C3D840033A6F3 = {
isa = PBXGroup;
children = (
54B5113E216C407F0033A6F3 /* FloatingPanel.framework */,
549D23D1233C77D5008EF4D7 /* FloatingPanel.framework */,
54B51128216C3D840033A6F3 /* Maps */,
54B51127216C3D840033A6F3 /* Products */,
);
@@ -312,7 +312,7 @@
);
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.Maps;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 4.2;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
@@ -331,7 +331,7 @@
);
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.Maps;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 4.2;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
+38 -6
View File
@@ -21,7 +21,11 @@ class ViewController: UIViewController, MKMapViewDelegate, UISearchBarDelegate,
// Initialize FloatingPanelController and add the view
fpc.surfaceView.backgroundColor = .clear
fpc.surfaceView.cornerRadius = 9.0
if #available(iOS 11, *) {
fpc.surfaceView.cornerRadius = 9.0
} else {
fpc.surfaceView.cornerRadius = 0.0
}
fpc.surfaceView.shadowHidden = false
searchVC = storyboard?.instantiateViewController(withIdentifier: "SearchPanel") as? SearchPanelViewController
@@ -135,24 +139,40 @@ class SearchPanelViewController: UIViewController, UITableViewDataSource, UITabl
@IBOutlet weak var tableView: UITableView!
@IBOutlet weak var searchBar: UISearchBar!
@IBOutlet weak var visualEffectView: UIVisualEffectView!
// For iOS 10 only
private lazy var shadowLayer: CAShapeLayer = CAShapeLayer()
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
tableView.delegate = self
searchBar.placeholder = "Search for a place or address"
let textField = searchBar.value(forKey: "_searchField") as! UITextField
textField.font = UIFont(name: textField.font!.fontName, size: 15.0)
searchBar.setSearchText(fontSize: 15.0)
hideHeader()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if #available(iOS 10, *) {
if #available(iOS 11, *) {
} else {
// Exmaple: Add rounding corners on iOS 10
visualEffectView.layer.cornerRadius = 9.0
visualEffectView.clipsToBounds = true
// Exmaple: Add shadow manually on iOS 10
view.layer.insertSublayer(shadowLayer, at: 0)
let rect = visualEffectView.frame
let path = UIBezierPath(roundedRect: rect,
byRoundingCorners: [.topLeft, .topRight],
cornerRadii: CGSize(width: 9.0, height: 9.0))
shadowLayer.frame = visualEffectView.frame
shadowLayer.shadowPath = path.cgPath
shadowLayer.shadowColor = UIColor.black.cgColor
shadowLayer.shadowOffset = CGSize(width: 0.0, height: 1.0)
shadowLayer.shadowOpacity = 0.2
shadowLayer.shadowRadius = 3.0
}
}
@@ -252,3 +272,15 @@ class SearchHeaderView: UIView {
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
}
}
@@ -14,9 +14,9 @@
545DB9F821511E6400CA77B8 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 545DB9F621511E6400CA77B8 /* LaunchScreen.storyboard */; };
545DBA0321511E6400CA77B8 /* SampleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DBA0221511E6400CA77B8 /* SampleTests.swift */; };
545DBA0E21511E6400CA77B8 /* SampleUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DBA0D21511E6400CA77B8 /* SampleUITests.swift */; };
549D23CB233C7779008EF4D7 /* FloatingPanel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 549D23CA233C7779008EF4D7 /* FloatingPanel.framework */; };
549D23CC233C7779008EF4D7 /* FloatingPanel.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 549D23CA233C7779008EF4D7 /* FloatingPanel.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
54B51116216AFE5F0033A6F3 /* UIExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B51115216AFE5F0033A6F3 /* UIExtensions.swift */; };
54B5113C216C40670033A6F3 /* FloatingPanel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 54B5113B216C40670033A6F3 /* FloatingPanel.framework */; };
54B5113D216C40670033A6F3 /* FloatingPanel.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 54B5113B216C40670033A6F3 /* FloatingPanel.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
54CDC5D8215BBE23007D205C /* UIComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CDC5D7215BBE23007D205C /* UIComponents.swift */; };
/* End PBXBuildFile section */
@@ -38,13 +38,13 @@
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
54B5111C216C3B300033A6F3 /* Embed Frameworks */ = {
549D23CD233C7779008EF4D7 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
54B5113D216C40670033A6F3 /* FloatingPanel.framework in Embed Frameworks */,
549D23CC233C7779008EF4D7 /* FloatingPanel.framework in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
@@ -65,8 +65,8 @@
545DBA0921511E6400CA77B8 /* SamplesUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SamplesUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
545DBA0D21511E6400CA77B8 /* SampleUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SampleUITests.swift; sourceTree = "<group>"; };
545DBA0F21511E6400CA77B8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
549D23CA233C7779008EF4D7 /* FloatingPanel.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = FloatingPanel.framework; sourceTree = BUILT_PRODUCTS_DIR; };
54B51115216AFE5F0033A6F3 /* UIExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIExtensions.swift; sourceTree = "<group>"; };
54B5113B216C40670033A6F3 /* FloatingPanel.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = FloatingPanel.framework; sourceTree = BUILT_PRODUCTS_DIR; };
54CDC5D7215BBE23007D205C /* UIComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIComponents.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
@@ -75,7 +75,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
54B5113C216C40670033A6F3 /* FloatingPanel.framework in Frameworks */,
549D23CB233C7779008EF4D7 /* FloatingPanel.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -99,12 +99,11 @@
545DB9E121511E6300CA77B8 = {
isa = PBXGroup;
children = (
54B5113B216C40670033A6F3 /* FloatingPanel.framework */,
549D23CA233C7779008EF4D7 /* FloatingPanel.framework */,
545DB9EC21511E6300CA77B8 /* Sources */,
545DBA0121511E6400CA77B8 /* Tests */,
545DBA0C21511E6400CA77B8 /* UITests */,
545DB9EB21511E6300CA77B8 /* Products */,
545DBA1B2151CC1000CA77B8 /* Frameworks */,
);
sourceTree = "<group>";
};
@@ -151,13 +150,6 @@
path = UITests;
sourceTree = "<group>";
};
545DBA1B2151CC1000CA77B8 /* Frameworks */ = {
isa = PBXGroup;
children = (
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@@ -169,7 +161,7 @@
545DB9E621511E6300CA77B8 /* Sources */,
545DB9E721511E6300CA77B8 /* Frameworks */,
545DB9E821511E6300CA77B8 /* Resources */,
54B5111C216C3B300033A6F3 /* Embed Frameworks */,
549D23CD233C7779008EF4D7 /* Embed Frameworks */,
);
buildRules = (
);
@@ -499,7 +491,7 @@
);
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingPanelSample;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 4.2;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
@@ -518,7 +510,7 @@
);
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingPanelSample;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 4.2;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
@@ -35,18 +35,18 @@
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="7IS-PU-x0P">
<rect key="frame" x="0.0" y="0.0" width="375" height="778"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="Cell" textLabel="M0G-C8-hAO" style="IBUITableViewCellStyleDefault" id="ySY-oA-g81">
<rect key="frame" x="0.0" y="28" width="375" height="44"/>
<rect key="frame" x="0.0" y="28" width="375" height="43.666667938232422"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="ySY-oA-g81" id="sXB-nH-2g2">
<rect key="frame" x="0.0" y="0.0" width="375" height="43.666666666666664"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="43.666667938232422"/>
<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.666666666666664"/>
<rect key="frame" x="15" y="0.0" width="345" height="43.666667938232422"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
@@ -62,7 +62,7 @@
<constraints>
<constraint firstItem="7IS-PU-x0P" firstAttribute="top" secondItem="Smh-Bd-AAc" secondAttribute="top" id="6yd-jv-ey3"/>
<constraint firstItem="7IS-PU-x0P" firstAttribute="leading" secondItem="39L-Nq-qfp" secondAttribute="leading" id="Z6Y-Dc-cei"/>
<constraint firstItem="7IS-PU-x0P" firstAttribute="bottom" secondItem="39L-Nq-qfp" secondAttribute="bottom" id="fNW-DP-lhV"/>
<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"/>
@@ -87,35 +87,35 @@
<objects>
<viewController storyboardIdentifier="SettingsViewController" id="C1X-9Z-TyQ" customClass="SettingsViewController" customModule="Samples" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="af9-Zr-Ppc">
<rect key="frame" x="0.0" y="0.0" width="375" height="197.33000000000001"/>
<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="147.33333333333334"/>
<rect key="frame" x="32" y="16" width="311" height="181.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="17"/>
<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="33" width="130" height="20.333333333333329"/>
<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="69.333333333333329" width="264.33333333333326" height="31"/>
<rect key="frame" x="23.333333333333343" y="78" width="264.66666666666663" height="38"/>
<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.3333333333333428" width="89.333333333333329" height="20.333333333333332"/>
<rect key="frame" x="0.0" y="8.9999999999999982" width="89.666666666666671" height="20.333333333333329"/>
<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="js8-Qv-lUC">
<rect key="frame" x="215.33333333333334" y="0.0" width="51.000000000000028" height="31"/>
<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>
@@ -123,16 +123,16 @@
</subviews>
</stackView>
<stackView opaque="NO" contentMode="scaleToFill" spacing="126" translatesAutoresizingMaskIntoConstraints="NO" id="ZtZ-Dz-4cC">
<rect key="frame" x="23.333333333333343" y="116.33333333333334" width="264.66666666666663" height="31"/>
<rect key="frame" x="23.333333333333343" y="132" width="264.66666666666663" height="49.333333333333343"/>
<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="31"/>
<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="31"/>
<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>
@@ -309,22 +309,22 @@
<objects>
<viewController storyboardIdentifier="ModalViewController" id="bYI-y3-Rzb" customClass="ModalViewController" customModule="Samples" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="qwo-GK-p1U">
<rect key="frame" x="0.0" y="0.0" width="375" height="778"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="724"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="vut-mK-Y4t" customClass="SafeAreaView" customModule="Samples" customModuleProvider="target">
<rect key="frame" x="0.0" y="744" width="375" height="34"/>
<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">
<rect key="frame" x="20" y="44" width="39" height="30"/>
<rect key="frame" x="20" y="0.0" width="39" height="30"/>
<state key="normal" title="Close"/>
<connections>
<action selector="closeWithSender:" destination="bYI-y3-Rzb" eventType="touchUpInside" id="MSC-ch-YJK"/>
</connections>
</button>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="top" spacing="44" translatesAutoresizingMaskIntoConstraints="NO" id="9p4-06-y2T">
<rect key="frame" x="139.66666666666666" y="132" width="96" height="252"/>
<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">
<rect key="frame" x="0.0" y="0.0" width="80" height="30"/>
@@ -347,8 +347,15 @@
<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">
<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">
<rect key="frame" x="0.0" y="222" width="96" height="30"/>
<rect key="frame" x="0.0" y="296" width="96" height="30"/>
<state key="normal" title="Update layout"/>
<connections>
<action selector="updateLayout:" destination="bYI-y3-Rzb" eventType="touchUpInside" id="Woz-a7-YMJ"/>
@@ -517,7 +524,7 @@
</constraints>
</view>
<view alpha="0.5" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Kva-Z7-0qY" customClass="OnSafeAreaView" customModule="Samples" customModuleProvider="target">
<rect key="frame" x="0.0" y="44" width="375" height="700"/>
<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">
+367 -74
View File
@@ -9,7 +9,7 @@
import UIKit
import FloatingPanel
class SampleListViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, FloatingPanelControllerDelegate, FloatingPanelLayout {
class SampleListViewController: UIViewController {
@IBOutlet weak var tableView: UITableView!
enum Menu: Int, CaseIterable {
@@ -19,9 +19,11 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable
case showModal
case showFloatingPanelModal
case showTabBar
case showPageView
case showNestedScrollView
case showRemovablePanel
case showIntrinsicView
case showContentInset
var name: String {
switch self {
@@ -31,9 +33,11 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable
case .showModal: return "Show Modal"
case .showFloatingPanelModal: return "Show Floating Panel Modal"
case .showTabBar: return "Show Tab Bar"
case .showPageView: return "Show Page View"
case .showNestedScrollView: return "Show Nested ScrollView"
case .showRemovablePanel: return "Show Removable Panel"
case .showIntrinsicView: return "Show Intrinsic View"
case .showContentInset: return "Show with ContentInset"
}
}
@@ -45,9 +49,11 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable
case .showModal: return "ModalViewController"
case .showFloatingPanelModal: return nil
case .showTabBar: return "TabBarViewController"
case .showPageView: return nil
case .showNestedScrollView: return "NestedScrollViewController"
case .showRemovablePanel: return "DetailViewController"
case .showIntrinsicView: return "IntrinsicViewController"
case .showContentInset: return nil
}
}
}
@@ -61,6 +67,19 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable
var mainPanelObserves: [NSKeyValueObservation] = []
var settingsObserves: [NSKeyValueObservation] = []
lazy var pages: [UIViewController] = {
let page1 = FloatingPanelController(delegate: self)
page1.view.backgroundColor = .blue
page1.show()
let page2 = FloatingPanelController(delegate: self)
page2.view.backgroundColor = .red
page2.show()
let page3 = FloatingPanelController(delegate: self)
page3.view.backgroundColor = .green
page3.show()
return [page1, page2, page3]
}()
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
@@ -113,6 +132,11 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable
// Enable tap-to-hide and removal interaction
switch currentMenu {
case .trackingTableView:
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleSurface(tapGesture:)))
tapGesture.cancelsTouchesInView = false
tapGesture.numberOfTapsRequired = 2
mainPanelVC.surfaceView.addGestureRecognizer(tapGesture)
case .showRemovablePanel, .showIntrinsicView:
mainPanelVC.isRemovalInteractionEnabled = true
@@ -143,8 +167,14 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable
mainPanelVC.addPanel(toParent: self, belowView: nil, animated: true)
}
@objc func dismissDetailPanelVC() {
detailPanelVC.removePanelFromParent(animated: true, completion: nil)
@objc
func handleSurface(tapGesture: UITapGestureRecognizer) {
switch mainPanelVC.position {
case .full:
mainPanelVC.move(to: .half, animated: true)
default:
mainPanelVC.move(to: .full, animated: true)
}
}
@objc func handleBackdrop(tapGesture: UITapGestureRecognizer) {
@@ -159,31 +189,6 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable
}
}
// MARK:- TableViewDatasource
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if #available(iOS 11.0, *) {
if navigationController?.navigationBar.prefersLargeTitles == true {
return Menu.allCases.count + 30
} else {
return Menu.allCases.count
}
} else {
return Menu.allCases.count
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
if Menu.allCases.count > indexPath.row {
let menu = Menu.allCases[indexPath.row]
cell.textLabel?.text = menu.name
} else {
cell.textLabel?.text = "\(indexPath.row) row"
}
return cell
}
// MARK:- Actions
@IBAction func showDebugMenu(_ sender: UIBarButtonItem) {
guard settingsPanelVC == nil else { return }
@@ -208,9 +213,34 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable
// Add FloatingPanel to self.view
settingsPanelVC.addPanel(toParent: self, belowView: nil, animated: true)
}
}
// MARK:- TableViewDelegate
extension SampleListViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if #available(iOS 11.0, *) {
if navigationController?.navigationBar.prefersLargeTitles == true {
return Menu.allCases.count + 30
} else {
return Menu.allCases.count
}
} else {
return Menu.allCases.count
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
if Menu.allCases.count > indexPath.row {
let menu = Menu.allCases[indexPath.row]
cell.textLabel?.text = menu.name
} else {
cell.textLabel?.text = "\(indexPath.row) row"
}
return cell
}
}
extension SampleListViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard Menu.allCases.count > indexPath.row else { return }
let menu = Menu.allCases[indexPath.row]
@@ -240,7 +270,25 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable
detailPanelVC.addPanel(toParent: self, belowView: nil, animated: true)
case .showModal, .showTabBar:
let modalVC = contentVC
modalVC.modalPresentationStyle = .fullScreen
present(modalVC, animated: true, completion: nil)
case .showPageView:
let pageVC = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: [:])
let closeButton = UIButton(type: .custom)
pageVC.view.addSubview(closeButton)
closeButton.setTitle("Close", for: .normal)
closeButton.translatesAutoresizingMaskIntoConstraints = false
closeButton.addTarget(self, action: #selector(dismissPresentedVC), for: .touchUpInside)
NSLayoutConstraint.activate([
closeButton.topAnchor.constraint(equalTo: pageVC.layoutGuide.topAnchor, constant: 16.0),
closeButton.leftAnchor.constraint(equalTo: pageVC.view.leftAnchor, constant: 16.0),
])
pageVC.dataSource = self
pageVC.setViewControllers([pages[0]], direction: .forward, animated: false, completion: nil)
pageVC.modalPresentationStyle = .fullScreen
present(pageVC, animated: true, completion: nil)
case .showFloatingPanelModal:
let fpc = FloatingPanelController()
let contentVC = self.storyboard!.instantiateViewController(withIdentifier: "DetailViewController")
@@ -253,6 +301,18 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable
fpc.isRemovalInteractionEnabled = true
self.present(fpc, animated: true, completion: nil)
case .showContentInset:
let contentViewController = UIViewController()
contentViewController.view.backgroundColor = .green
let fpc = FloatingPanelController()
fpc.set(contentViewController: contentViewController)
fpc.surfaceView.contentInsets = .init(top: 20, left: 20, bottom: 20, right: 20)
fpc.delegate = self
fpc.isRemovalInteractionEnabled = true
self.present(fpc, animated: true, completion: nil)
default:
detailPanelVC?.removePanelFromParent(animated: true, completion: nil)
mainPanelVC?.removePanelFromParent(animated: true) {
@@ -261,6 +321,12 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable
}
}
@objc func dismissPresentedVC() {
self.presentedViewController?.dismiss(animated: true, completion: nil)
}
}
extension SampleListViewController: FloatingPanelControllerDelegate {
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? {
if vc == settingsPanelVC {
return IntrinsicPanelLayout()
@@ -276,8 +342,10 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable
return ModalPanelLayout()
}
fallthrough
case .showContentInset:
return NoInteractionBufferPanelLayout()
default:
return (newCollection.verticalSizeClass == .compact) ? nil : self
return (newCollection.verticalSizeClass == .compact) ? nil : self
}
}
@@ -285,6 +353,9 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable
switch currentMenu {
case .showNestedScrollView:
return (vc.contentViewController as? NestedScrollViewController)?.nestedScrollView.gestureRecognizers?.contains(gestureRecognizer) ?? false
case .showPageView:
// Tips: Need to allow recognizing the pan gesture of UIPageViewController simultaneously.
return true
default:
return false
}
@@ -298,7 +369,14 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable
break
}
}
}
/**
- Attention: `FloatingPanelLayout` must not be applied by the parent view
controller of a floating panel. But here `SampleListViewController` adopts it
purposely to check if the library prints an appropriate warning.
*/
extension SampleListViewController: FloatingPanelLayout {
var initialPosition: FloatingPanelPosition {
return .half
}
@@ -313,8 +391,48 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable
}
}
extension SampleListViewController: UIPageViewControllerDataSource {
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
guard
let index = pages.firstIndex(of: viewController),
index + 1 < pages.count
else { return nil }
return pages[index + 1]
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
guard
let index = pages.firstIndex(of: viewController),
index - 1 >= 0
else { return nil }
return pages[index - 1]
}
}
class IntrinsicPanelLayout: FloatingPanelIntrinsicLayout { }
class NoInteractionBufferPanelLayout: FloatingPanelLayout {
var initialPosition: FloatingPanelPosition {
return .full
}
func insetFor(position: FloatingPanelPosition) -> CGFloat? {
switch position {
case .full: return 0
case .half: return 216
case .tip: return 60
case .hidden: return nil
}
}
var topInteractionBuffer: CGFloat {
return 0.0
}
var bottomInteractionBuffer: CGFloat {
return 0.0
}
}
class RemovablePanelLayout: FloatingPanelIntrinsicLayout {
var supportedPositions: Set<FloatingPanelPosition> {
return [.full, .half]
@@ -468,7 +586,7 @@ class InspectableViewController: UIViewController {
}
}
class DebugTableViewController: InspectableViewController, UITableViewDataSource, UITableViewDelegate {
class DebugTableViewController: InspectableViewController {
weak var tableView: UITableView!
var items: [String] = []
var itemHeight: CGFloat = 66.0
@@ -587,6 +705,12 @@ class DebugTableViewController: InspectableViewController, UITableViewDataSource
(self.parent as! FloatingPanelController).removePanelFromParent(animated: true, completion: nil)
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
print("TableView --- ", scrollView.contentOffset, scrollView.contentInset)
}
}
extension DebugTableViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
@@ -600,6 +724,12 @@ class DebugTableViewController: InspectableViewController, UITableViewDataSource
cell.textLabel?.text = items[indexPath.row]
return cell
}
}
extension DebugTableViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print("DebugTableViewController -- select row \(indexPath.row)")
}
func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
return [
@@ -696,7 +826,9 @@ class ModalViewController: UIViewController, FloatingPanelControllerDelegate {
@IBAction func moveToTip(sender: UIButton) {
fpc.move(to: .tip, animated: true)
}
@IBAction func moveToHidden(sender: UIButton) {
fpc.move(to: .hidden, animated: true)
}
@IBAction func updateLayout(_ sender: Any) {
isNewlayout = !isNewlayout
UIView.animate(withDuration: 0.5) {
@@ -726,10 +858,24 @@ class ModalSecondLayout: FloatingPanelLayout {
class TabBarViewController: UITabBarController {}
class TabBarContentViewController: UIViewController, FloatingPanelControllerDelegate {
class TabBarContentViewController: UIViewController {
enum Tab3Mode {
case changeOffset
case changeAutoLayout
var label: String {
switch self {
case .changeAutoLayout: return "Use AutoLayout(OK)"
case .changeOffset: return "Use ContentOffset(NG)"
}
}
}
var fpc: FloatingPanelController!
var consoleVC: DebugTextViewController!
var threeLayout: ThreeTabBarPanelLayout!
var tab3Mode: Tab3Mode = .changeAutoLayout
var switcherLabel: UILabel!
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// Initialize FloatingPanelController
@@ -743,11 +889,47 @@ class TabBarContentViewController: UIViewController, FloatingPanelControllerDele
// Set a content view controller and track the scroll view
let consoleVC = storyboard?.instantiateViewController(withIdentifier: "ConsoleViewController") as! DebugTextViewController
fpc.set(contentViewController: consoleVC)
consoleVC.textView.delegate = self // MUST call it before fpc.track(scrollView:)
fpc.track(scrollView: consoleVC.textView)
self.consoleVC = consoleVC
// Add FloatingPanel to self.view
fpc.addPanel(toParent: self)
if tabBarItem.tag == 2 {
let switcher = UISwitch()
fpc.view.addSubview(switcher)
switcher.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
switcher.bottomAnchor.constraint(equalTo: fpc.surfaceView.topAnchor, constant: -16.0),
switcher.rightAnchor.constraint(equalTo: fpc.surfaceView.rightAnchor, constant: -16.0),
])
switcher.isOn = true
switcher.tintColor = .white
switcher.backgroundColor = .white
switcher.layer.cornerRadius = 16.0
switcher.addTarget(self,
action: #selector(changeTab3Mode(_:)),
for: .valueChanged)
let label = UILabel()
label.text = tab3Mode.label
fpc.view.addSubview(label)
switcherLabel = label
label.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
label.centerYAnchor.constraint(equalTo: switcher.centerYAnchor, constant: 0.0),
label.rightAnchor.constraint(equalTo: switcher.leftAnchor, constant: -16.0),
])
// Turn off the mask instead of content inset change
consoleVC.textView.clipsToBounds = false
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
fpc.updateLayout()
}
override func viewWillDisappear(_ animated: Bool) {
@@ -756,6 +938,43 @@ class TabBarContentViewController: UIViewController, FloatingPanelControllerDele
fpc.removePanelFromParent(animated: false)
}
// MARK: - Action
@IBAction func close(sender: UIButton) {
dismiss(animated: true, completion: nil)
}
// MARK: - Private
@objc
private func changeTab3Mode(_ sender: UISwitch) {
if sender.isOn {
tab3Mode = .changeAutoLayout
} else {
tab3Mode = .changeOffset
}
switcherLabel.text = tab3Mode.label
}
}
extension TabBarContentViewController: UITextViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard self.tabBarItem.tag == 2 else { return }
// Reset an invalid content offset by a user after updating the layout
// of `consoleVC.textView`.
// NOTE: FloatingPanel doesn't implicitly reset the offset(i.e.
// Using KVO of `scrollView.contentOffset`). Because it can lead to an
// infinite loop if a user also resets a content offset as below and,
// in the situation, a user has to modify the library.
if fpc.position != .full, fpc.surfaceView.frame.minY > fpc.originYOfSurface(for: .full) {
scrollView.contentOffset = .zero
}
}
}
extension TabBarContentViewController: FloatingPanelControllerDelegate {
// MARK: - FloatingPanel
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? {
switch self.tabBarItem.tag {
case 0:
@@ -763,7 +982,17 @@ class TabBarContentViewController: UIViewController, FloatingPanelControllerDele
case 1:
return TwoTabBarPanelLayout()
case 2:
return ThreeTabBarPanelLayout()
threeLayout = ThreeTabBarPanelLayout(parent: self)
return threeLayout
default:
return nil
}
}
func floatingPanel(_ vc: FloatingPanelController, behaviorFor newCollection: UITraitCollection) -> FloatingPanelBehavior? {
switch self.tabBarItem.tag {
case 1:
return TwoTabBarPanelBehavior()
default:
return nil
}
@@ -772,51 +1001,80 @@ class TabBarContentViewController: UIViewController, FloatingPanelControllerDele
func floatingPanelDidMove(_ vc: FloatingPanelController) {
guard self.tabBarItem.tag == 2 else { return }
/* Solution 1: Manipulate scoll content inset */
/*
guard let scrollView = consoleVC.textView else { return }
var insets = vc.adjustedContentInsets
if vc.surfaceView.frame.minY < vc.layoutInsets.top {
insets.top = vc.layoutInsets.top - vc.surfaceView.frame.minY
} else {
insets.top = 0.0
}
scrollView.contentInset = insets
*/
switch tab3Mode {
case .changeAutoLayout:
/* Good solution: Manipulate top constraint */
assert(consoleVC.textViewTopConstraint != nil)
if vc.surfaceView.frame.minY + threeLayout.topPadding < vc.layoutInsets.top {
consoleVC.textViewTopConstraint?.constant = vc.layoutInsets.top - vc.surfaceView.frame.minY
} else {
consoleVC.textViewTopConstraint?.constant = threeLayout.topPadding
}
case .changeOffset:
/*
Bad solution: Manipulate scroll content inset
// Solution 2: Manipulate top constraint
assert(consoleVC.textViewTopConstraint != nil)
if vc.surfaceView.frame.minY + 17.0 < vc.layoutInsets.top {
consoleVC.textViewTopConstraint?.constant = vc.layoutInsets.top - vc.surfaceView.frame.minY
} else {
consoleVC.textViewTopConstraint?.constant = 17.0
FloatingPanelController keeps a content offset in moving a panel
so that changing content inset or offset causes a buggy behavior.
*/
guard let scrollView = consoleVC.textView else { return }
var insets = vc.adjustedContentInsets
if vc.surfaceView.frame.minY < vc.layoutInsets.top {
insets.top = vc.layoutInsets.top - vc.surfaceView.frame.minY
} else {
insets.top = 0.0
}
scrollView.contentInset = insets
if vc.surfaceView.frame.minY > 0 {
scrollView.contentOffset = CGPoint(x: 0.0,
y: 0.0 - scrollView.contentInset.top)
}
}
consoleVC.view.layoutIfNeeded()
if vc.surfaceView.frame.minY > vc.originYOfSurface(for: .half) {
let progress = (vc.surfaceView.frame.minY - vc.originYOfSurface(for: .half)) / (vc.originYOfSurface(for: .tip) - vc.originYOfSurface(for: .half))
threeLayout.leftConstraint.constant = max(min(progress, 1.0), 0.0) * threeLayout.sideMargin
threeLayout.rightConstraint.constant = -max(min(progress, 1.0), 0.0) * threeLayout.sideMargin
} else {
threeLayout.leftConstraint.constant = 0.0
threeLayout.rightConstraint.constant = 0.0
}
vc.view.layoutIfNeeded() // MUST
}
func floatingPanelDidChangePosition(_ vc: FloatingPanelController) {
guard self.tabBarItem.tag == 2 else { return }
/* Solution 1: Manipulate scoll content inset */
/*
guard let scrollView = consoleVC.textView else { return }
var insets = vc.adjustedContentInsets
insets.top = (vc.position == .full) ? vc.layoutInsets.top : 0.0
scrollView.contentInset = insets
if scrollView.contentOffset.y - scrollView.contentInset.top < 0.0 {
scrollView.contentOffset = CGPoint(x: 0.0,
y: 0.0 - scrollView.contentInset.top)
switch tab3Mode {
case .changeAutoLayout:
/* Good Solution: Manipulate top constraint */
assert(consoleVC.textViewTopConstraint != nil)
consoleVC.textViewTopConstraint?.constant = (vc.position == .full) ? vc.layoutInsets.top : 17.0
case .changeOffset:
/* Bad Solution: Manipulate scroll content inset */
guard let scrollView = consoleVC.textView else { return }
var insets = vc.adjustedContentInsets
insets.top = (vc.position == .full) ? vc.layoutInsets.top : 0.0
scrollView.contentInset = insets
if scrollView.contentOffset.y - scrollView.contentInset.top < 0.0 {
scrollView.contentOffset = CGPoint(x: 0.0,
y: 0.0 - scrollView.contentInset.top)
}
}
*/
// Solution 2: Manipulate top constraint
assert(consoleVC.textViewTopConstraint != nil)
consoleVC.textViewTopConstraint?.constant = (vc.position == .full) ? vc.layoutInsets.top : 17.0
consoleVC.view.layoutIfNeeded()
}
@IBAction func close(sender: UIButton) {
dismiss(animated: true, completion: nil)
if vc.position == .tip {
threeLayout.leftConstraint.constant = threeLayout.sideMargin
threeLayout.rightConstraint.constant = -threeLayout.sideMargin
} else {
threeLayout.leftConstraint.constant = 0.0
threeLayout.rightConstraint.constant = 0.0
}
// Can call it, but it's not necessary because it will be also called
// by FloatingPanelController after the delegate method
vc.view.layoutIfNeeded()
}
}
@@ -860,36 +1118,71 @@ class TwoTabBarPanelLayout: FloatingPanelLayout {
var supportedPositions: Set<FloatingPanelPosition> {
return [.full, .half]
}
var topInteractionBuffer: CGFloat {
return 100.0
}
var bottomInteractionBuffer: CGFloat {
return 261.0 - 22.0
}
func insetFor(position: FloatingPanelPosition) -> CGFloat? {
switch position {
case .full: return 16.0
case .full: return 100.0
case .half: return 261.0
default: return nil
}
}
}
class TwoTabBarPanelBehavior: FloatingPanelBehavior {
func allowsRubberBanding(for edge: UIRectEdge) -> Bool {
return (edge == .bottom || edge == .top)
}
}
class ThreeTabBarPanelLayout: FloatingPanelFullScreenLayout {
weak var parentVC: UIViewController!
var leftConstraint: NSLayoutConstraint!
var rightConstraint: NSLayoutConstraint!
let topPadding: CGFloat = 17.0
let sideMargin: CGFloat = 16.0
init(parent: UIViewController) {
parentVC = parent
}
var bottomInteractionBuffer: CGFloat = 44.0
var initialPosition: FloatingPanelPosition {
return .half
}
var supportedPositions: Set<FloatingPanelPosition> {
return [.full, .half]
return [.full, .half, .tip]
}
func insetFor(position: FloatingPanelPosition) -> CGFloat? {
switch position {
case .full: return 0.0
case .half: return 261.0
case .half: return 261.0 + parentVC.layoutInsets.bottom
case .tip: return 88.0 + parentVC.layoutInsets.bottom
default: return nil
}
}
func backdropAlphaFor(position: FloatingPanelPosition) -> CGFloat {
return 0.3
}
func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint] {
if #available(iOS 11.0, *) {
leftConstraint = surfaceView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 0.0)
rightConstraint = surfaceView.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor, constant: 0.0)
} else {
leftConstraint = surfaceView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 0.0)
rightConstraint = surfaceView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 0.0)
}
return [ leftConstraint, rightConstraint ]
}
}
class SettingsViewController: InspectableViewController {
@@ -900,7 +1193,7 @@ class SettingsViewController: InspectableViewController {
override func viewDidLoad() {
versionLabel.text = "Version: \(Bundle.main.infoDictionary?["CFBundleVersion"] ?? "--")"
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if #available(iOS 11.0, *) {
@@ -7,13 +7,13 @@
objects = {
/* Begin PBXBuildFile section */
5433F24B21717EA300BDAA5D /* FloatingPanel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5433F24A21717EA300BDAA5D /* FloatingPanel.framework */; };
5433F24C21717EA300BDAA5D /* FloatingPanel.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 5433F24A21717EA300BDAA5D /* FloatingPanel.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
548DF95421705BE00041922A /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 548DF95321705BE00041922A /* AppDelegate.swift */; };
548DF95621705BE00041922A /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 548DF95521705BE00041922A /* ViewController.swift */; };
548DF95921705BE00041922A /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 548DF95721705BE00041922A /* Main.storyboard */; };
548DF95B21705BE10041922A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 548DF95A21705BE10041922A /* Assets.xcassets */; };
548DF95E21705BE10041922A /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 548DF95C21705BE10041922A /* LaunchScreen.storyboard */; };
549D23CF233C77CF008EF4D7 /* FloatingPanel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 549D23CE233C77CF008EF4D7 /* FloatingPanel.framework */; };
549D23D0233C77CF008EF4D7 /* FloatingPanel.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 549D23CE233C77CF008EF4D7 /* FloatingPanel.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
@@ -23,7 +23,7 @@
dstPath = "";
dstSubfolderSpec = 10;
files = (
5433F24C21717EA300BDAA5D /* FloatingPanel.framework in Embed Frameworks */,
549D23D0233C77CF008EF4D7 /* FloatingPanel.framework in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
@@ -31,7 +31,6 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
5433F24A21717EA300BDAA5D /* FloatingPanel.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = FloatingPanel.framework; sourceTree = BUILT_PRODUCTS_DIR; };
548DF95021705BE00041922A /* Stocks.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Stocks.app; sourceTree = BUILT_PRODUCTS_DIR; };
548DF95321705BE00041922A /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
548DF95521705BE00041922A /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
@@ -39,6 +38,7 @@
548DF95A21705BE10041922A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
548DF95D21705BE10041922A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
548DF95F21705BE10041922A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
549D23CE233C77CF008EF4D7 /* FloatingPanel.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = FloatingPanel.framework; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -46,7 +46,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
5433F24B21717EA300BDAA5D /* FloatingPanel.framework in Frameworks */,
549D23CF233C77CF008EF4D7 /* FloatingPanel.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -56,7 +56,7 @@
548DF94721705BE00041922A = {
isa = PBXGroup;
children = (
5433F24A21717EA300BDAA5D /* FloatingPanel.framework */,
549D23CE233C77CF008EF4D7 /* FloatingPanel.framework */,
548DF95221705BE00041922A /* Stocks */,
548DF95121705BE00041922A /* Products */,
);
@@ -312,7 +312,7 @@
);
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.Stocks;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 4.2;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
@@ -331,7 +331,7 @@
);
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.Stocks;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 4.2;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
+1 -2
View File
@@ -1,7 +1,7 @@
Pod::Spec.new do |s|
s.name = "FloatingPanel"
s.version = "1.3.4"
s.version = "1.6.6"
s.summary = "FloatingPanel is a clean and easy-to-use UI component of a floating panel interface."
s.description = <<-DESC
FloatingPanel is a clean and easy-to-use UI component for a new interface introduced in Apple Maps, Shortcuts and Stocks app.
@@ -14,7 +14,6 @@ The new interface displays the related contents and utilities in parallel as a u
s.source = { :git => "https://github.com/SCENEE/FloatingPanel.git", :tag => "v#{s.version}" }
s.source_files = "Framework/Sources/*.swift"
s.swift_version = "4.0"
s.pod_target_xcconfig = { 'SWIFT_WHOLE_MODULE_OPTIMIZATION' => 'YES', 'APPLICATION_EXTENSION_API_ONLY' => 'YES' }
s.framework = "UIKit"
+296 -11
View File
@@ -7,20 +7,27 @@
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 */; };
545DB9CB2151169500CA77B8 /* FloatingPanel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 545DB9C12151169500CA77B8 /* FloatingPanel.framework */; };
545DB9D02151169500CA77B8 /* ViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DB9CF2151169500CA77B8 /* ViewTests.swift */; };
545DB9D22151169500CA77B8 /* FloatingPanelController.h in Headers */ = {isa = PBXBuildFile; fileRef = 545DB9C42151169500CA77B8 /* FloatingPanelController.h */; settings = {ATTRIBUTES = (Public, ); }; };
545DB9D02151169500CA77B8 /* FloatingPanelControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DB9CF2151169500CA77B8 /* FloatingPanelControllerTests.swift */; };
545DB9D22151169500CA77B8 /* FloatingPanel.h in Headers */ = {isa = PBXBuildFile; fileRef = 545DB9C42151169500CA77B8 /* FloatingPanel.h */; settings = {ATTRIBUTES = (Public, ); }; };
545DB9DE215118C800CA77B8 /* UIExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DB9DD215118C800CA77B8 /* UIExtensions.swift */; };
545DB9E021511AC100CA77B8 /* FloatingPanelController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DB9DF21511AC100CA77B8 /* FloatingPanelController.swift */; };
545DBA2B2152383100CA77B8 /* GrabberHandleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DBA2A2152383100CA77B8 /* GrabberHandleView.swift */; };
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 */; };
54ABD7AF216CCFF7002E6C13 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54ABD7AE216CCFF7002E6C13 /* Logger.swift */; };
54CDC5D3215B6D5A007D205C /* FloatingPanelSurfaceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CDC5D2215B6D5A007D205C /* FloatingPanelSurfaceView.swift */; };
54CDC5D5215B6D8D007D205C /* FloatingPanelBackdropView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CDC5D4215B6D8D007D205C /* FloatingPanelBackdropView.swift */; };
54CFBFC3215CD045006B5735 /* FloatingPanelLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CFBFC2215CD045006B5735 /* FloatingPanelLayout.swift */; };
54CFBFC5215CD09C006B5735 /* FloatingPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CFBFC4215CD09C006B5735 /* FloatingPanel.swift */; };
54E740CD218AFD67005C1A34 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54E740CC218AFD67005C1A34 /* AppDelegate.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -31,26 +38,42 @@
remoteGlobalIDString = 545DB9C02151169500CA77B8;
remoteInfo = FloatingModalController;
};
54E740DC218AFE9F005C1A34 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 545DB9B82151169500CA77B8 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 54E740C9218AFD67005C1A34;
remoteInfo = TestingHost;
};
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
542753C522C49A6E00D17955 /* FloatingPanelLayoutTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelLayoutTests.swift; sourceTree = "<group>"; };
542753C722C49A8F00D17955 /* Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utils.swift; sourceTree = "<group>"; };
54352E9521A51A2500CBCA08 /* FloatingPanelTransitioning.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelTransitioning.swift; sourceTree = "<group>"; };
54352E9721A521CA00CBCA08 /* FloatingPanelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelView.swift; sourceTree = "<group>"; };
5450EEE321646DF500135936 /* FloatingPanelBehavior.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelBehavior.swift; sourceTree = "<group>"; };
545DB9C12151169500CA77B8 /* FloatingPanel.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = FloatingPanel.framework; sourceTree = BUILT_PRODUCTS_DIR; };
545DB9C42151169500CA77B8 /* FloatingPanelController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FloatingPanelController.h; sourceTree = "<group>"; };
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 /* ViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewTests.swift; sourceTree = "<group>"; };
545DB9CF2151169500CA77B8 /* FloatingPanelControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelControllerTests.swift; sourceTree = "<group>"; };
545DB9D12151169500CA77B8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
545DB9DD215118C800CA77B8 /* UIExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIExtensions.swift; sourceTree = "<group>"; };
545DB9DF21511AC100CA77B8 /* FloatingPanelController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelController.swift; sourceTree = "<group>"; };
545DBA2A2152383100CA77B8 /* GrabberHandleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GrabberHandleView.swift; sourceTree = "<group>"; };
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>"; };
54ABD7AE216CCFF7002E6C13 /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; };
54CDC5D2215B6D5A007D205C /* FloatingPanelSurfaceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelSurfaceView.swift; sourceTree = "<group>"; };
54CDC5D4215B6D8D007D205C /* FloatingPanelBackdropView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelBackdropView.swift; sourceTree = "<group>"; };
54CFBFC2215CD045006B5735 /* FloatingPanelLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelLayout.swift; sourceTree = "<group>"; };
54CFBFC4215CD09C006B5735 /* FloatingPanel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanel.swift; sourceTree = "<group>"; };
54E740CA218AFD67005C1A34 /* FloatingPanelTesting.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FloatingPanelTesting.app; sourceTree = BUILT_PRODUCTS_DIR; };
54E740CC218AFD67005C1A34 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
54E740D8218AFD6A005C1A34 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -69,6 +92,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
54E740C7218AFD67005C1A34 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
@@ -77,6 +107,7 @@
children = (
545DB9C32151169500CA77B8 /* Sources */,
545DB9CE2151169500CA77B8 /* Tests */,
54E740CB218AFD67005C1A34 /* TestingApp */,
545DB9C22151169500CA77B8 /* Products */,
);
sourceTree = "<group>";
@@ -86,6 +117,7 @@
children = (
545DB9C12151169500CA77B8 /* FloatingPanel.framework */,
545DB9CA2151169500CA77B8 /* FloatingPanelTests.xctest */,
54E740CA218AFD67005C1A34 /* FloatingPanelTesting.app */,
);
name = Products;
sourceTree = "<group>";
@@ -94,7 +126,7 @@
isa = PBXGroup;
children = (
545DB9C52151169500CA77B8 /* Info.plist */,
545DB9C42151169500CA77B8 /* FloatingPanelController.h */,
545DB9C42151169500CA77B8 /* FloatingPanel.h */,
545DB9DF21511AC100CA77B8 /* FloatingPanelController.swift */,
54352E9521A51A2500CBCA08 /* FloatingPanelTransitioning.swift */,
54CFBFC4215CD09C006B5735 /* FloatingPanel.swift */,
@@ -113,12 +145,27 @@
545DB9CE2151169500CA77B8 /* Tests */ = {
isa = PBXGroup;
children = (
545DB9CF2151169500CA77B8 /* ViewTests.swift */,
54A6B6B022968B530077F348 /* FloatingPanelTests.swift */,
545DB9CF2151169500CA77B8 /* FloatingPanelControllerTests.swift */,
542753C522C49A6E00D17955 /* FloatingPanelLayoutTests.swift */,
54A6B6B72296A8520077F348 /* FloatingPanelSurfaceViewTests.swift */,
549E944422CF295D0050AECF /* FloatingPanelPositionTests.swift */,
542753C722C49A8F00D17955 /* Utils.swift */,
545DB9D12151169500CA77B8 /* Info.plist */,
);
path = Tests;
sourceTree = "<group>";
};
54E740CB218AFD67005C1A34 /* TestingApp */ = {
isa = PBXGroup;
children = (
54E740CC218AFD67005C1A34 /* AppDelegate.swift */,
54E740D8218AFD6A005C1A34 /* Info.plist */,
54A6B6B522968F710077F348 /* LaunchScreen.storyboard */,
);
path = TestingApp;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXHeadersBuildPhase section */
@@ -126,7 +173,7 @@
isa = PBXHeadersBuildPhase;
buildActionMask = 2147483647;
files = (
545DB9D22151169500CA77B8 /* FloatingPanelController.h in Headers */,
545DB9D22151169500CA77B8 /* FloatingPanel.h in Headers */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -163,19 +210,37 @@
);
dependencies = (
545DB9CD2151169500CA77B8 /* PBXTargetDependency */,
54E740DD218AFE9F005C1A34 /* PBXTargetDependency */,
);
name = FloatingPanelTests;
productName = FloatingModalControllerTests;
productReference = 545DB9CA2151169500CA77B8 /* FloatingPanelTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
54E740C9218AFD67005C1A34 /* TestingApp */ = {
isa = PBXNativeTarget;
buildConfigurationList = 54E740D9218AFD6A005C1A34 /* Build configuration list for PBXNativeTarget "TestingApp" */;
buildPhases = (
54E740C6218AFD67005C1A34 /* Sources */,
54E740C7218AFD67005C1A34 /* Frameworks */,
54E740C8218AFD67005C1A34 /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = TestingApp;
productName = TestingHost;
productReference = 54E740CA218AFD67005C1A34 /* FloatingPanelTesting.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
545DB9B82151169500CA77B8 /* Project object */ = {
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 1000;
LastSwiftUpdateCheck = 1010;
LastUpgradeCheck = 1000;
ORGANIZATIONNAME = scenee;
TargetAttributes = {
@@ -185,6 +250,10 @@
};
545DB9C92151169500CA77B8 = {
CreatedOnToolsVersion = 10.0;
TestTargetID = 54E740C9218AFD67005C1A34;
};
54E740C9218AFD67005C1A34 = {
CreatedOnToolsVersion = 10.1;
};
};
};
@@ -194,6 +263,7 @@
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 545DB9B72151169500CA77B8;
productRefGroup = 545DB9C22151169500CA77B8 /* Products */;
@@ -202,6 +272,7 @@
targets = (
545DB9C02151169500CA77B8 /* FloatingPanel */,
545DB9C92151169500CA77B8 /* FloatingPanelTests */,
54E740C9218AFD67005C1A34 /* TestingApp */,
);
};
/* End PBXProject section */
@@ -221,6 +292,14 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
54E740C8218AFD67005C1A34 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
54A6B6B622968F710077F348 /* LaunchScreen.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
@@ -246,7 +325,20 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
545DB9D02151169500CA77B8 /* ViewTests.swift in Sources */,
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 */,
);
runOnlyForDeploymentPostprocessing = 0;
};
54E740C6218AFD67005C1A34 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
54E740CD218AFD67005C1A34 /* AppDelegate.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -258,6 +350,11 @@
target = 545DB9C02151169500CA77B8 /* FloatingPanel */;
targetProxy = 545DB9CC2151169500CA77B8 /* PBXContainerItemProxy */;
};
54E740DD218AFE9F005C1A34 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 54E740C9218AFD67005C1A34 /* TestingApp */;
targetProxy = 54E740DC218AFE9F005C1A34 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin XCBuildConfiguration section */
@@ -386,6 +483,7 @@
545DB9D62151169500CA77B8 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
APPLICATION_EXTENSION_API_ONLY = YES;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "";
CODE_SIGN_STYLE = Automatic;
@@ -405,6 +503,7 @@
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 4.0;
TARGETED_DEVICE_FAMILY = "1,2";
@@ -414,6 +513,7 @@
545DB9D72151169500CA77B8 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
APPLICATION_EXTENSION_API_ONLY = YES;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "";
CODE_SIGN_STYLE = Automatic;
@@ -432,6 +532,7 @@
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingPanel;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SKIP_INSTALL = YES;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_VERSION = 4.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
@@ -442,7 +543,9 @@
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = "";
INFOPLIST_FILE = Tests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -450,8 +553,9 @@
);
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingModalControllerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 4.2;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/FloatingPanelTesting.app/FloatingPanelTesting";
};
name = Debug;
};
@@ -460,7 +564,9 @@
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = "";
INFOPLIST_FILE = Tests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -468,11 +574,177 @@
);
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingModalControllerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 4.2;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/FloatingPanelTesting.app/FloatingPanelTesting";
};
name = Release;
};
54E740DA218AFD6A005C1A34 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = "";
INFOPLIST_FILE = TestingApp/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingPanelTesting;
PRODUCT_NAME = FloatingPanelTesting;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
54E740DB218AFD6A005C1A34 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = "";
INFOPLIST_FILE = TestingApp/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingPanelTesting;
PRODUCT_NAME = FloatingPanelTesting;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
54E79ADF224F6C9800717BC6 /* Test */ = {
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;
CURRENT_PROJECT_VERSION = 1;
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.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
VERSIONING_SYSTEM = "apple-generic";
VERSION_INFO_PREFIX = "";
};
name = Test;
};
54E79AE0224F6C9800717BC6 /* Test */ = {
isa = XCBuildConfiguration;
buildSettings = {
APPLICATION_EXTENSION_API_ONLY = YES;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "";
CODE_SIGN_STYLE = Automatic;
DEFINES_MODULE = YES;
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
DYLIB_INSTALL_NAME_BASE = "@rpath";
INFOPLIST_FILE = Sources/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingPanel;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "TEST DEBUG __FP_LOG";
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 4.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Test;
};
54E79AE1224F6C9800717BC6 /* Test */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = "";
INFOPLIST_FILE = Tests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingModalControllerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/FloatingPanelTesting.app/FloatingPanelTesting";
};
name = Test;
};
54E8AC6A2286CFB6000C5A12 /* Test */ = {
isa = XCBuildConfiguration;
buildSettings = {
DEVELOPMENT_TEAM = "";
INFOPLIST_FILE = TestingApp/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingPanelTesting;
PRODUCT_NAME = FloatingPanelTesting;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Test;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
@@ -480,6 +752,7 @@
isa = XCConfigurationList;
buildConfigurations = (
545DB9D32151169500CA77B8 /* Debug */,
54E79ADF224F6C9800717BC6 /* Test */,
545DB9D42151169500CA77B8 /* Release */,
);
defaultConfigurationIsVisible = 0;
@@ -489,6 +762,7 @@
isa = XCConfigurationList;
buildConfigurations = (
545DB9D62151169500CA77B8 /* Debug */,
54E79AE0224F6C9800717BC6 /* Test */,
545DB9D72151169500CA77B8 /* Release */,
);
defaultConfigurationIsVisible = 0;
@@ -498,11 +772,22 @@
isa = XCConfigurationList;
buildConfigurations = (
545DB9D92151169500CA77B8 /* Debug */,
54E79AE1224F6C9800717BC6 /* Test */,
545DB9DA2151169500CA77B8 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
54E740D9218AFD6A005C1A34 /* Build configuration list for PBXNativeTarget "TestingApp" */ = {
isa = XCConfigurationList;
buildConfigurations = (
54E740DA218AFD6A005C1A34 /* Debug */,
54E740DB218AFD6A005C1A34 /* Release */,
54E8AC6A2286CFB6000C5A12 /* Test */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 545DB9B82151169500CA77B8 /* Project object */;
@@ -23,12 +23,33 @@
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
buildConfiguration = "Test"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES"
testExecutionOrdering = "random">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "545DB9C92151169500CA77B8"
BuildableName = "FloatingPanelTests.xctest"
BlueprintName = "FloatingPanelTests"
ReferencedContainer = "container:FloatingPanel.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "545DB9C02151169500CA77B8"
BuildableName = "FloatingPanel.framework"
BlueprintName = "FloatingPanel"
ReferencedContainer = "container:FloatingPanel.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
File diff suppressed because it is too large Load Diff
+51 -5
View File
@@ -6,12 +6,28 @@
import UIKit
public protocol FloatingPanelBehavior {
/// Returns the progress to redirect to the previous position
/// Asks the behavior if the floating panel should project a momentum of a user interaction to move the proposed position.
///
/// The progress is represented by a floating-point value between 0.0 and 1.0, inclusive, where 1.0 indicates the floating panel is impossible to move to the next posiiton. The default value is 0.5. Values less than 0.0 and greater than 1.0 are pinned to those limits.
/// The 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.
@@ -46,13 +62,35 @@ public protocol FloatingPanelBehavior {
///
/// Default is a spring animator with 1.0 damping ratio. This method is called when FloatingPanelController.isRemovalInteractionEnabled is true.
func removalInteractionAnimator(_ fpc: FloatingPanelController, with velocity: CGVector) -> UIViewPropertyAnimator
/// Asks the behavior whether the rubber band effect is enabled in moving over a given edge of the surface view.
///
/// This method allows the behavior to activate the rubber band effect to a given edge of the surface view. By default, the effect is disabled.
func allowsRubberBanding(for edge: UIRectEdge) -> Bool
}
public extension FloatingPanelBehavior {
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)
}
@@ -80,13 +118,21 @@ public extension FloatingPanelBehavior {
initialVelocity: velocity)
return UIViewPropertyAnimator(duration: 0, timingParameters: timing)
}
func allowsRubberBanding(for edge: UIRectEdge) -> Bool {
return false
}
}
class FloatingPanelDefaultBehavior: FloatingPanelBehavior {
func interactionAnimator(_ fpc: FloatingPanelController, to targetPosition: FloatingPanelPosition, with velocity: CGVector) -> UIViewPropertyAnimator {
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
animator.isInterruptible = false // Prevent a propagation of the animation(spring etc) to a content view
return animator
}
+154 -61
View File
@@ -12,9 +12,14 @@ public protocol FloatingPanelControllerDelegate: class {
// if it returns nil, FloatingPanelController uses the default behavior
func floatingPanel(_ vc: FloatingPanelController, behaviorFor newCollection: UITraitCollection) -> FloatingPanelBehavior?
func floatingPanelDidChangePosition(_ vc: FloatingPanelController) // changed the settled position in the model layer
/// 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)
func floatingPanelDidMove(_ vc: FloatingPanelController) // any offset changes
/// 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)
@@ -28,7 +33,10 @@ public protocol FloatingPanelControllerDelegate: class {
// called when its views are removed from a parent view controller
func floatingPanelDidEndRemove(_ vc: FloatingPanelController)
func floatingPanel(_ vc: FloatingPanelController, shouldRecognizeSimultaneouslyWith gestureRecognizer: UIGestureRecognizer) -> Bool
/// 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 {
@@ -39,6 +47,9 @@ public extension FloatingPanelControllerDelegate {
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) {}
@@ -48,7 +59,9 @@ public extension FloatingPanelControllerDelegate {
func floatingPanelDidEndDraggingToRemove(_ vc: FloatingPanelController, withVelocity velocity: CGPoint) {}
func floatingPanelDidEndRemove(_ vc: FloatingPanelController) {}
func floatingPanel(_ vc: FloatingPanelController, shouldRecognizeSimultaneouslyWith gestureRecognizer: UIGestureRecognizer) -> Bool { return false }
func floatingPanel(_ vc: FloatingPanelController, shouldRecognizeSimultaneouslyWith gestureRecognizer: UIGestureRecognizer) -> Bool {
return false
}
}
@@ -57,12 +70,46 @@ public enum FloatingPanelPosition: Int {
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.
///
public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UIGestureRecognizerDelegate {
open class FloatingPanelController: UIViewController {
/// Constants indicating how safe area insets are added to the adjusted content inset.
public enum ContentInsetAdjustmentBehavior: Int {
case always
@@ -134,7 +181,8 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
}
private var _contentViewController: UIViewController?
private var floatingPanel: FloatingPanel!
private(set) var floatingPanel: FloatingPanel!
private var preSafeAreaInsets: UIEdgeInsets = .zero // Capture the latest one
private var safeAreaInsetsObservation: NSKeyValueObservation?
private let modalTransition = FloatingPanelModalTransition()
@@ -169,7 +217,7 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
// MARK:- Overrides
/// Creates the view that the controller manages.
override public func loadView() {
open override func loadView() {
assert(self.storyboard == nil, "Storyboard isn't supported")
let view = FloatingPanelPassThroughView()
@@ -184,16 +232,19 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
self.view = view as UIView
}
public override func viewDidLayoutSubviews() {
open override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if #available(iOS 11.0, *) {}
else {
// Because {top,bottom}LayoutGuide is managed as a view
self.update(safeAreaInsets: layoutInsets)
if preSafeAreaInsets != layoutInsets,
floatingPanel.isDecelerating == false {
self.update(safeAreaInsets: layoutInsets)
}
}
}
public override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
open override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
if view.translatesAutoresizingMaskIntoConstraints {
@@ -202,21 +253,25 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
}
}
public override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) {
open override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) {
super.willTransition(to: newCollection, with: coordinator)
// Change layout for a new trait collection
reloadLayout(for: newCollection)
setUpLayout()
floatingPanel.behavior = fetchBehavior(for: newCollection)
self.prepare(for: newCollection)
}
public override func viewWillDisappear(_ animated: Bool) {
open override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
safeAreaInsetsObservation = nil
}
// MARK:- Internals
func prepare(for newCollection: UITraitCollection) {
guard newCollection.shouldUpdateLayout(from: traitCollection) else { return }
// Change a layout & behavior for a new trait collection
reloadLayout(for: newCollection)
activateLayout()
floatingPanel.behavior = fetchBehavior(for: newCollection)
}
// MARK:- Privates
private func fetchLayout(for traitCollection: UITraitCollection) -> FloatingPanelLayout {
@@ -233,17 +288,16 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
}
private func update(safeAreaInsets: UIEdgeInsets) {
// Don't re-layout the surface on SafeArea.Bottom enabled/disabled in interaction progress
guard
floatingPanel.layoutAdapter.safeAreaInsets != safeAreaInsets,
self.floatingPanel.interactionInProgress == false
else { return }
preSafeAreaInsets != safeAreaInsets
else { return }
log.debug("Update safeAreaInsets", safeAreaInsets)
floatingPanel.layoutAdapter.safeAreaInsets = safeAreaInsets
// Prevent an infinite loop on iOS 10: setUpLayout() -> viewDidLayoutSubviews() -> setUpLayout()
preSafeAreaInsets = safeAreaInsets
setUpLayout()
activateLayout()
switch contentInsetAdjustmentBehavior {
case .always:
@@ -257,9 +311,18 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
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 setUpLayout() {
private func activateLayout() {
// preserve the current content offset
let contentOffset = scrollView?.contentOffset
@@ -275,7 +338,7 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
public func show(animated: Bool = false, completion: (() -> Void)? = nil) {
// Must apply the current layout here
reloadLayout(for: traitCollection)
setUpLayout()
activateLayout()
if #available(iOS 11.0, *) {
// Must track the safeAreaInsets of `self.view` to update the layout.
@@ -284,9 +347,9 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
// 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) { [weak self] (vc, chaneg) in
guard let `self` = self else { return }
self.update(safeAreaInsets: vc.layoutInsets)
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.
@@ -300,7 +363,6 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
/// Hides the surface view to the hidden position
public func hide(animated: Bool = false, completion: (() -> Void)? = nil) {
safeAreaInsetsObservation = nil
move(to: .hidden,
animated: animated,
completion: completion)
@@ -328,7 +390,11 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
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
@@ -341,7 +407,11 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
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
}
}
@@ -357,9 +427,20 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
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?()
}
}
@@ -374,39 +455,53 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
floatingPanel.move(to: to, animated: animated, completion: completion)
}
/// Sets the view controller responsible for the content portion of the floating panel..
/// 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()
if let scrollView = floatingPanel.scrollView,
let delegate = floatingPanel.userScrollViewDelegate,
vc.view.subviews.contains(scrollView) {
scrollView.delegate = delegate
}
#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:)")
public override func show(_ vc: UIViewController, sender: Any?) {
open override func show(_ vc: UIViewController, sender: Any?) {
if let target = self.parent?.targetViewController(forAction: #selector(UIViewController.show(_:sender:)), sender: sender) {
target.show(vc, sender: sender)
}
}
@available(*, unavailable, renamed: "set(contentViewController:)")
public override func showDetailViewController(_ vc: UIViewController, sender: Any?) {
open override func showDetailViewController(_ vc: UIViewController, sender: Any?) {
if let target = self.parent?.targetViewController(forAction: #selector(UIViewController.showDetailViewController(_:sender:)), sender: sender) {
target.showDetailViewController(vc, sender: sender)
}
@@ -416,23 +511,30 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
/// Tracks the specified scroll view to correspond with the scroll.
///
/// - Attention:
/// The specified scroll view must be already assigned to the delegate property because the controller intermediates between the various delegate methods.
///
public func track(scrollView: UIScrollView) {
floatingPanel.scrollView = scrollView
if scrollView.delegate !== floatingPanel {
floatingPanel.userScrollViewDelegate = scrollView.delegate
scrollView.delegate = floatingPanel
/// - 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
@@ -450,21 +552,12 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
/// animation block.
public func updateLayout() {
reloadLayout(for: traitCollection)
setUpLayout()
activateLayout()
}
/// Returns the y-coordinate of the point at the origin of the surface view
/// Returns the y-coordinate of the point at the origin of the surface view.
public func originYOfSurface(for pos: FloatingPanelPosition) -> CGFloat {
switch pos {
case .full:
return floatingPanel.layoutAdapter.topY
case .half:
return floatingPanel.layoutAdapter.middleY
case .tip:
return floatingPanel.layoutAdapter.bottomY
case .hidden:
return floatingPanel.layoutAdapter.hiddenY
}
return floatingPanel.layoutAdapter.positionY(for: pos)
}
}
@@ -486,10 +579,10 @@ extension FloatingPanelController {
}
public extension UIViewController {
@objc public func fp_original_dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
@objc func fp_original_dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
// Implementation will be replaced by IMP of self.dismiss(animated:completion:)
}
@objc public func fp_dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
@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 {
+231 -73
View File
@@ -7,7 +7,7 @@ import UIKit
/// FloatingPanelFullScreenLayout
///
/// Use the layout protocol if you want to configure a full inset from Superview.Top, not SafeArea.Top.
/// 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 { }
@@ -43,14 +43,16 @@ public protocol FloatingPanelLayout: class {
/// Returns a set of FloatingPanelPosition objects to tell the applicable
/// positions of the floating panel controller.
///
/// By default, it returns all position except for `hidden` position. Because
/// it's always supported by `FloatingPanelController` so you don't need to return it.
/// 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).
@@ -95,6 +97,8 @@ public extension FloatingPanelLayout {
}
public class FloatingPanelDefaultLayout: FloatingPanelLayout {
public init() { }
public var initialPosition: FloatingPanelPosition {
return .half
}
@@ -110,6 +114,8 @@ public class FloatingPanelDefaultLayout: FloatingPanelLayout {
}
public class FloatingPanelDefaultLandscapeLayout: FloatingPanelLayout {
public init() { }
public var initialPosition: FloatingPanelPosition {
return .tip
}
@@ -126,9 +132,13 @@ public class FloatingPanelDefaultLandscapeLayout: FloatingPanelLayout {
}
}
struct LayoutSegment {
let lower: FloatingPanelPosition?
let upper: FloatingPanelPosition?
}
class FloatingPanelLayoutAdapter {
weak var vc: UIViewController!
weak var vc: FloatingPanelController!
private weak var surfaceView: FloatingPanelSurfaceView!
private weak var backdropView: FloatingPanelBackdropView!
@@ -138,14 +148,20 @@ class FloatingPanelLayoutAdapter {
}
}
var safeAreaInsets: UIEdgeInsets = .zero
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 heightConstraints: [NSLayoutConstraint] = []
private var interactiveTopConstraint: NSLayoutConstraint?
private var heightConstraint: NSLayoutConstraint?
private var fullInset: CGFloat {
if layout is FloatingPanelIntrinsicLayout {
@@ -165,50 +181,32 @@ class FloatingPanelLayoutAdapter {
}
var supportedPositions: Set<FloatingPanelPosition> {
var supportedPositions = layout.supportedPositions
supportedPositions.remove(.hidden)
return supportedPositions
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 {
if supportedPositions.contains(.full) {
switch layout {
case is FloatingPanelIntrinsicLayout:
return surfaceView.superview!.bounds.height - surfaceView.bounds.height
case is FloatingPanelFullScreenLayout:
return fullInset
default:
return (safeAreaInsets.top + fullInset)
}
} else {
return middleY
}
}
var middleY: CGFloat {
return surfaceView.superview!.bounds.height - (safeAreaInsets.bottom + halfInset)
return positionY(for: topMostState)
}
var bottomY: CGFloat {
if supportedPositions.contains(.tip) {
return surfaceView.superview!.bounds.height - (safeAreaInsets.bottom + tipInset)
} else {
return middleY
}
}
var hiddenY: CGFloat {
return surfaceView.superview!.bounds.height
}
var safeAreaBottomY: CGFloat {
return surfaceView.superview!.bounds.height - (safeAreaInsets.bottom + hiddenInset)
return positionY(for: bottomMostState)
}
var topMaxY: CGFloat {
return layout is FloatingPanelFullScreenLayout ? 0.0 : -safeAreaInsets.top
return topY - layout.topInteractionBuffer
}
var bottomMaxY: CGFloat {
return bottomY + layout.bottomInteractionBuffer
}
var bottomMaxY: CGFloat { return safeAreaBottomY }
var adjustedContentInsets: UIEdgeInsets {
return UIEdgeInsets(top: 0.0,
@@ -220,13 +218,30 @@ class FloatingPanelLayoutAdapter {
func positionY(for pos: FloatingPanelPosition) -> CGFloat {
switch pos {
case .full:
return topY
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:
return middleY
switch layout {
case is FloatingPanelFullScreenLayout:
return surfaceView.superview!.bounds.height - halfInset
default:
return surfaceView.superview!.bounds.height - (safeAreaInsets.bottom + halfInset)
}
case .tip:
return bottomY
switch layout {
case is FloatingPanelFullScreenLayout:
return surfaceView.superview!.bounds.height - tipInset
default:
return surfaceView.superview!.bounds.height - (safeAreaInsets.bottom + tipInset)
}
case .hidden:
return hiddenY
return surfaceView.superview!.bounds.height - hiddenInset
}
}
@@ -239,7 +254,11 @@ class FloatingPanelLayoutAdapter {
}
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, *) {
@@ -256,7 +275,7 @@ class FloatingPanelLayoutAdapter {
", content safe area(bottom) =", safeAreaBottom)
}
func prepareLayout(in vc: UIViewController) {
func prepareLayout(in vc: FloatingPanelController) {
self.vc = vc
NSLayoutConstraint.deactivate(fixedConstraints + fullConstraints + halfConstraints + tipConstraints + offConstraints)
@@ -275,63 +294,97 @@ class FloatingPanelLayoutAdapter {
fixedConstraints = surfaceConstraints + backdropConstraints
// Flexible surface constarints for full, half, tip and off
// 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
case is FloatingPanelFullScreenLayout:
fullConstraints = [
surfaceView.topAnchor.constraint(equalTo: vc.view.topAnchor,
constant: fullInset),
]
default:
fullConstraints = [
surfaceView.topAnchor.constraint(equalTo: vc.layoutGuide.topAnchor,
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: vc.layoutGuide.bottomAnchor,
surfaceView.topAnchor.constraint(equalTo: bottomAnchor,
constant: -halfInset),
]
tipConstraints = [
surfaceView.topAnchor.constraint(equalTo: vc.layoutGuide.bottomAnchor,
surfaceView.topAnchor.constraint(equalTo: bottomAnchor,
constant: -tipInset),
]
offConstraints = [
surfaceView.topAnchor.constraint(equalTo: vc.view.bottomAnchor,
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 }
NSLayoutConstraint.deactivate(heightConstraints)
if let const = self.heightConstraint {
NSLayoutConstraint.deactivate([const])
}
let heightConstraint: NSLayoutConstraint
switch layout {
case is FloatingPanelIntrinsicLayout:
updateIntrinsicHeight()
heightConstraints = [
surfaceView.heightAnchor.constraint(equalToConstant: intrinsicHeight + safeAreaInsets.bottom),
]
case is FloatingPanelFullScreenLayout:
heightConstraints = [
surfaceView.heightAnchor.constraint(equalTo: vc.view.heightAnchor,
constant: -fullInset),
]
heightConstraint = surfaceView.heightAnchor.constraint(equalToConstant: intrinsicHeight + safeAreaInsets.bottom)
default:
heightConstraints = [
surfaceView.heightAnchor.constraint(equalTo: vc.view.heightAnchor,
constant: -(safeAreaInsets.top + fullInset)),
]
let const = -(positionY(for: topMostState))
heightConstraint = surfaceView.heightAnchor.constraint(equalTo: vc.view.heightAnchor,
constant: const)
}
NSLayoutConstraint.activate(heightConstraints)
NSLayoutConstraint.activate([heightConstraint])
self.heightConstraint = heightConstraint
surfaceView.bottomOverflow = vc.view.bounds.height + layout.topInteractionBuffer
@@ -344,18 +397,78 @@ class FloatingPanelLayoutAdapter {
}
}
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 {
surfaceView.superview!.layoutIfNeeded()
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 supportedPositions.union([.hidden]).contains(state) == false {
if isValid(state) == false {
state = layout.initialPosition
}
@@ -372,7 +485,18 @@ class FloatingPanelLayoutAdapter {
}
}
func setBackdropAlpha(of target: FloatingPanelPosition) {
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 {
@@ -384,7 +508,7 @@ class FloatingPanelLayoutAdapter {
// Verify layout configurations
assert(supportedPositions.count > 0)
assert(supportedPositions.contains(layout.initialPosition),
"Does not include an initial potision(\(layout.initialPosition)) in supportedPositions(\(supportedPositions))")
"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")
@@ -402,4 +526,38 @@ class FloatingPanelLayoutAdapter {
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)
}
}
}
+136 -61
View File
@@ -5,24 +5,43 @@
import UIKit
class FloatingPanelSurfaceContentView: UIView {}
/// 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 appropirate coordinates.
public var grabberHandle: GrabberHandleView!
/// 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 static var topGrabberBarHeight: CGFloat {
return Default.grabberTopPadding * 2 + GrabberHandleView.Default.height // 17.0
public var topGrabberBarHeight: CGFloat {
return grabberTopPadding * 2 + grabberHandleHeight
}
/// Grabber view width and height
public var grabberHandleWidth: CGFloat = 36.0 { didSet {
setNeedsUpdateConstraints()
} }
public var grabberHandleHeight: CGFloat = 5.0 { didSet {
setNeedsUpdateConstraints()
} }
/// A root view of a content view controller
public weak var contentView: UIView!
/// The content insets specifying the insets around the content view.
public var contentInsets: UIEdgeInsets = .zero {
didSet {
// Needs update constraints
self.setNeedsUpdateConstraints()
}
}
private var color: UIColor? = .white { didSet { setNeedsLayout() } }
var bottomOverflow: CGFloat = 0.0 // Must not call setNeedsLayout()
@@ -36,7 +55,10 @@ public class FloatingPanelSurfaceView: UIView {
///
/// `self.contentView` is masked with the top rounded corners automatically on iOS 11 and later.
/// On iOS 10, they are not automatically masked because of a UIVisualEffectView issue. See https://forums.developer.apple.com/thread/50854
public var cornerRadius: CGFloat = 0.0 { didSet { setNeedsLayout() } }
public var cornerRadius: CGFloat {
set { containerView.layer.cornerRadius = newValue; setNeedsLayout() }
get { return containerView.layer.cornerRadius }
}
/// A Boolean indicating whether the surface shadow is displayed.
public var shadowHidden: Bool = false { didSet { setNeedsLayout() } }
@@ -59,103 +81,156 @@ public class FloatingPanelSurfaceView: UIView {
/// The color of the surface border.
public var borderWidth: CGFloat = 0.0 { didSet { setNeedsLayout() } }
private var backgroundLayer: CAShapeLayer! { didSet { setNeedsLayout() } }
/// Offset of the container view from the top
public var containerTopInset: CGFloat = 0.0 { didSet {
setNeedsUpdateConstraints()
} }
private struct Default {
public static let grabberTopPadding: CGFloat = 6.0
}
/// The view presents an actual surface shape.
///
/// It renders the background color, border line and top rounded corners,
/// specified by other properties. The reason why they're not be applied to
/// a content view directly is because it avoids any side-effects to the
/// content view.
public let containerView: UIView = UIView()
@available(*, unavailable, renamed: "containerView")
public var backgroundView: UIView!
private lazy var containerViewTopInsetConstraint: NSLayoutConstraint = containerView.topAnchor.constraint(equalTo: topAnchor, constant: containerTopInset)
private lazy var containerViewHeightConstraint: NSLayoutConstraint = containerView.heightAnchor.constraint(equalTo: heightAnchor, multiplier: 1.0)
/// The content view top constraint
private var contentViewTopConstraint: NSLayoutConstraint?
/// The content view left constraint
private var contentViewLeftConstraint: NSLayoutConstraint?
/// The content right constraint
private var contentViewRightConstraint: NSLayoutConstraint?
/// The content height constraint
private var contentViewHeightConstraint: NSLayoutConstraint?
private lazy var grabberHandleWidthConstraint: NSLayoutConstraint = grabberHandle.widthAnchor.constraint(equalToConstant: grabberHandleWidth)
private lazy var grabberHandleHeightConstraint: NSLayoutConstraint = grabberHandle.heightAnchor.constraint(equalToConstant: grabberHandleHeight)
private lazy var grabberHandleTopConstraint: NSLayoutConstraint = grabberHandle.topAnchor.constraint(equalTo: topAnchor, constant: grabberTopPadding)
public override class var requiresConstraintBasedLayout: Bool { return true }
override init(frame: CGRect) {
super.init(frame: frame)
render()
addSubViews()
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
render()
addSubViews()
}
private func render() {
private func addSubViews() {
super.backgroundColor = .clear
self.clipsToBounds = false
let backgroundLayer = CAShapeLayer()
layer.insertSublayer(backgroundLayer, at: 0)
self.backgroundLayer = backgroundLayer
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,
])
let grabberHandle = GrabberHandleView()
addSubview(grabberHandle)
self.grabberHandle = grabberHandle
grabberHandle.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
grabberHandle.topAnchor.constraint(equalTo: topAnchor, constant: Default.grabberTopPadding),
grabberHandle.widthAnchor.constraint(equalToConstant: grabberHandle.frame.width),
grabberHandle.heightAnchor.constraint(equalToConstant: grabberHandle.frame.height),
grabberHandleWidthConstraint,
grabberHandleHeightConstraint,
grabberHandleTopConstraint,
grabberHandle.centerXAnchor.constraint(equalTo: centerXAnchor),
])
}
public override func updateConstraints() {
containerViewTopInsetConstraint.constant = containerTopInset
containerViewHeightConstraint.constant = bottomOverflow
contentViewTopConstraint?.constant = contentInsets.top
contentViewLeftConstraint?.constant = contentInsets.left
contentViewRightConstraint?.constant = contentInsets.right
contentViewHeightConstraint?.constant = -(containerTopInset + contentInsets.top + contentInsets.bottom)
grabberHandleTopConstraint.constant = grabberTopPadding
grabberHandleWidthConstraint.constant = grabberHandleWidth
grabberHandleHeightConstraint.constant = grabberHandleHeight
super.updateConstraints()
}
public override func layoutSubviews() {
super.layoutSubviews()
log.debug("SurfaceView frame", frame)
log.debug("surface view frame = \(frame)")
updateLayers()
updateContentViewMask()
containerView.backgroundColor = color
contentView?.layer.borderColor = borderColor?.cgColor
contentView?.layer.borderWidth = borderWidth
contentView?.frame = bounds
updateShadow()
updateCornerRadius()
updateBorder()
}
private func updateLayers() {
log.debug("SurfaceView bounds", bounds)
var rect = bounds
rect.size.height += bottomOverflow // Expand the height for overflow buffer
let path = UIBezierPath(roundedRect: rect,
byRoundingCorners: [.topLeft, .topRight],
cornerRadii: CGSize(width: cornerRadius, height: cornerRadius))
backgroundLayer.path = path.cgPath
backgroundLayer.fillColor = color?.cgColor
private func updateShadow() {
if shadowHidden == false {
layer.shadowColor = shadowColor.cgColor
layer.shadowOffset = shadowOffset
layer.shadowOpacity = shadowOpacity
layer.shadowRadius = shadowRadius
if #available(iOS 11, *) {
// For clear background. See also, https://github.com/SCENEE/FloatingPanel/pull/51.
layer.shadowColor = shadowColor.cgColor
layer.shadowOffset = shadowOffset
layer.shadowOpacity = shadowOpacity
layer.shadowRadius = shadowRadius
} else {
// Can't update `layer.shadow*` directly because of a UIVisualEffectView issue in iOS 10, https://forums.developer.apple.com/thread/50854
// Instead, a user should display shadow appropriately.
}
}
}
private func updateContentViewMask() {
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.storyborad of Example/Maps.
// 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.
let maskLayer = CAShapeLayer()
var rect = bounds
rect.size.height += bottomOverflow
let path = UIBezierPath(roundedRect: rect,
byRoundingCorners: [.topLeft, .topRight],
cornerRadii: CGSize(width: cornerRadius, height: cornerRadius))
maskLayer.path = path.cgPath
contentView?.layer.mask = maskLayer
containerView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
} else {
// Don't use `contentView.layer.mask` because of a UIVisualEffectView issue in iOS 10, https://forums.developer.apple.com/thread/50854
// Instead, a user can mask the content view manually in an application.
// Can't use `containerView.layer.mask` because of a UIVisualEffectView issue in iOS 10, https://forums.developer.apple.com/thread/50854
// Instead, a user should display rounding corners appropriately.
}
}
private func updateBorder() {
containerView.layer.borderColor = borderColor?.cgColor
containerView.layer.borderWidth = borderWidth
}
func add(contentView: UIView) {
insertSubview(contentView, belowSubview: grabberHandle)
containerView.addSubview(contentView)
self.contentView = contentView
/* contentView.frame = bounds */ // MUST NOT: Because the top safe area inset of a content VC will be incorrect.
contentView.translatesAutoresizingMaskIntoConstraints = false
let topConstraint = contentView.topAnchor.constraint(equalTo: topAnchor, constant: contentInsets.top)
let leftConstraint = contentView.leftAnchor.constraint(equalTo: leftAnchor, constant: contentInsets.left)
let rightConstraint = rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: contentInsets.right)
let heightConstraint = contentView.heightAnchor.constraint(equalTo: heightAnchor, constant: -(containerTopInset + contentInsets.top + contentInsets.bottom))
NSLayoutConstraint.activate([
contentView.topAnchor.constraint(equalTo: topAnchor, constant: 0.0),
contentView.leftAnchor.constraint(equalTo: leftAnchor, constant: 0.0),
contentView.rightAnchor.constraint(equalTo: rightAnchor, constant: 0.0),
contentView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 0.0),
topConstraint,
leftConstraint,
rightConstraint,
heightConstraint,
])
self.contentViewTopConstraint = topConstraint
self.contentViewLeftConstraint = leftConstraint
self.contentViewRightConstraint = rightConstraint
self.contentViewHeightConstraint = heightConstraint
}
}
@@ -24,7 +24,7 @@ class FloatingPanelModalTransition: NSObject, UIViewControllerTransitioningDeleg
class FloatingPanelPresentationController: UIPresentationController {
override func presentationTransitionWillBegin() {
// Must call here even if duplicating on in containerViewWillLayoutSubviews()
// Because it let the floating panel present correclty with the presentation animation
// Because it let the floating panel present correctly with the presentation animation
addFloatingPanel()
}
@@ -52,14 +52,14 @@ class FloatingPanelPresentationController: UIPresentationController {
/*
* Layout the views managed by `FloatingPanelController` here for the
* sake of the presentation and disimissal modally from the controller.
* sake of the presentation and dismissal modally from the controller.
*/
addFloatingPanel()
// Forward touch events to the presenting view controller
(fpc.view as? FloatingPanelPassThroughView)?.eventForwardingView = presentingViewController.view
// Set tap-to-dimiss in the backdrop view
// Set tap-to-dismiss in the backdrop view
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleBackdrop(tapGesture:)))
fpc.backdropView.addGestureRecognizer(tapGesture)
}
+12 -14
View File
@@ -6,32 +6,30 @@
import UIKit
public class GrabberHandleView: UIView {
public struct Default {
public static let width: CGFloat = 36.0
public static let height: CGFloat = 5.0
public static let barColor = UIColor(displayP3Red: 0.76, green: 0.77, blue: 0.76, alpha: 1.0)
}
public var barColor = UIColor(displayP3Red: 0.76, green: 0.77, blue: 0.76, alpha: 1.0) { didSet { backgroundColor = barColor } }
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
render()
}
init() {
let size = CGSize(width: Default.width,
height: Default.height)
super.init(frame: CGRect(origin: .zero, size: size))
self.backgroundColor = Default.barColor
render()
super.init(frame: .zero)
backgroundColor = barColor
}
private func render() {
self.layer.masksToBounds = true
self.layer.cornerRadius = frame.size.height * 0.5
public override func layoutSubviews() {
super.layoutSubviews()
render()
}
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let view = super.hitTest(point, with: event)
return view == self ? nil : view
}
private func render() {
self.layer.masksToBounds = true
self.layer.cornerRadius = frame.size.height * 0.5
}
}
+1 -1
View File
@@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.1.0</string>
<string>1.6.6</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
</dict>
+21 -26
View File
@@ -19,29 +19,17 @@ struct Logger {
case info = 1
case warning = 2
case error = 3
case fault = 4
var name: String {
switch self {
case .debug: return "DEBUG"
case .info: return "INFO"
case .warning: return "WARNING"
case .error: return "ERROR"
case .fault: return "FAULT"
}
}
var shortName: String {
var displayName: String {
switch self {
case .debug:
return "D/"
case .info:
return "I/"
case .warning:
return "W/"
return "Warning:"
case .error:
return "E/"
case .fault:
return "F/"
return "Error:"
}
}
@available(iOS 10.0, *)
@@ -49,31 +37,40 @@ struct Logger {
switch self {
case .debug: return .debug
case .info: return .info
case .warning: return .info
case .warning: return .default
case .error: return .error
case .fault: return .fault
}
}
public static func < (lhs: Logger.Level, rhs: Logger.Level) -> Bool {
static func < (lhs: Logger.Level, rhs: Logger.Level) -> Bool {
return lhs.rawValue < rhs.rawValue
}
}
init() {
typealias Hook = ((String, Level) -> Void)
var hook: Hook?
fileprivate init() {
osLog = OSLog(subsystem: "com.scenee.FloatingPanel", category: "FloatingPanel")
}
private func log(_ level: Level, _ message: Any, _ arguments: [Any], function: String, line: UInt) {
#if __FP_LOG
_ = s.wait(timeout: .now() + 0.033)
defer { s.signal() }
let extraMessage: String = arguments.map({ String(describing: $0) }).joined(separator: " ")
let log = "\(level.shortName) \(message) \(extraMessage) (\(function):\(line))"
let log: String = {
switch level {
case .debug:
return "\(level.displayName) \(message) \(extraMessage) (\(function):\(line))"
default:
return "\(level.displayName) \(message) \(extraMessage)"
}
}()
hook?(log, level)
os_log("%@", log: osLog, type: level.osLogType, log)
#endif
}
private func getPrettyFunction(_ function: String, _ file: String) -> String {
@@ -85,7 +82,9 @@ struct Logger {
}
func debug(_ log: Any, _ arguments: Any..., function: String = #function, file: String = #file, line: UInt = #line) {
#if __FP_LOG
self.log(.debug, log, arguments, function: getPrettyFunction(function, file), line: line)
#endif
}
func info(_ log: Any, _ arguments: Any..., function: String = #function, file: String = #file, line: UInt = #line) {
@@ -99,8 +98,4 @@ struct Logger {
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)
}
func fault(_ log: Any, _ arguments: Any..., function: String = #function, file: String = #file, line: UInt = #line) {
self.log(.fault, log, arguments, function: getPrettyFunction(function, file), line: line)
}
}
+47 -8
View File
@@ -60,6 +60,10 @@ extension UIView {
return self
}
}
var presentationFrame: CGRect {
return layer.presentation()?.frame ?? frame
}
}
extension UIView {
@@ -73,24 +77,43 @@ extension UIView {
}
}
extension UIGestureRecognizerState: CustomDebugStringConvertible {
#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"
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 {
@@ -101,3 +124,19 @@ extension UISpringTimingParameters {
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
}
}
+22
View File
@@ -0,0 +1,22 @@
//
// Created by Shin Yamamoto on 2018/11/01.
// Copyright © 2018 Shin Yamamoto. All rights reserved.
//
import UIKit
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
let rootVC = UIViewController(nibName: nil, bundle: nil)
rootVC.view.backgroundColor = .gray
let window = UIWindow()
window.rootViewController = rootVC
window.makeKeyAndVisible()
self.window = window
return true
}
}
+43
View File
@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>
@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13142" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12042"/>
<capability name="Constraints with non-1.0 multipliers" minToolsVersion="5.1"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Copyright © 2019 scenee. All rights reserved." textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumFontSize="9" translatesAutoresizingMaskIntoConstraints="NO" id="obG-Y5-kRd">
<rect key="frame" x="0.0" y="626.5" width="375" height="20.5"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="TestingApp" textAlignment="center" lineBreakMode="middleTruncation" baselineAdjustment="alignBaselines" minimumFontSize="18" translatesAutoresizingMaskIntoConstraints="NO" id="GJd-Yh-RWb">
<rect key="frame" x="0.0" y="202" width="375" height="43"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="36"/>
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="Bcu-3y-fUS" firstAttribute="centerX" secondItem="obG-Y5-kRd" secondAttribute="centerX" id="5cz-MP-9tL"/>
<constraint firstItem="Bcu-3y-fUS" firstAttribute="centerX" secondItem="GJd-Yh-RWb" secondAttribute="centerX" id="Q3B-4B-g5h"/>
<constraint firstItem="obG-Y5-kRd" firstAttribute="leading" secondItem="Bcu-3y-fUS" secondAttribute="leading" constant="20" symbolic="YES" id="SfN-ll-jLj"/>
<constraint firstAttribute="bottom" secondItem="obG-Y5-kRd" secondAttribute="bottom" constant="20" id="Y44-ml-fuU"/>
<constraint firstItem="GJd-Yh-RWb" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="bottom" multiplier="1/3" constant="1" id="moa-c2-u7t"/>
<constraint firstItem="GJd-Yh-RWb" firstAttribute="leading" secondItem="Bcu-3y-fUS" secondAttribute="leading" constant="20" symbolic="YES" id="x7j-FC-K8j"/>
</constraints>
<viewLayoutGuide key="safeArea" id="Bcu-3y-fUS"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
</document>
@@ -0,0 +1,147 @@
//
// 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
}
}
}
@@ -0,0 +1,206 @@
//
// 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)
}
}
@@ -0,0 +1,27 @@
//
// 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)
}
}
@@ -0,0 +1,82 @@
//
// 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)
}
}
+537
View File
@@ -0,0 +1,537 @@
//
// Created by Shin Yamamoto on 2019/05/23.
// Copyright © 2019 Shin Yamamoto. All rights reserved.
//
import XCTest
@testable import FloatingPanel
class FloatingPanelTests: XCTestCase {
override func setUp() {}
override func tearDown() {}
func test_scrolllock() {
let fpc = FloatingPanelController()
let contentVC1 = UITableViewController(nibName: nil, bundle: nil)
XCTAssertEqual(contentVC1.tableView.showsVerticalScrollIndicator, true)
XCTAssertEqual(contentVC1.tableView.bounces, true)
fpc.set(contentViewController: contentVC1)
fpc.track(scrollView: contentVC1.tableView)
fpc.showForTest()
XCTAssertEqual(fpc.position, .half)
XCTAssertEqual(contentVC1.tableView.showsVerticalScrollIndicator, false)
XCTAssertEqual(contentVC1.tableView.bounces, false)
fpc.move(to: .full, animated: false)
XCTAssertEqual(contentVC1.tableView.showsVerticalScrollIndicator, true)
XCTAssertEqual(contentVC1.tableView.bounces, true)
fpc.move(to: .tip, animated: false)
XCTAssertEqual(contentVC1.tableView.showsVerticalScrollIndicator, false)
XCTAssertEqual(contentVC1.tableView.bounces, false)
let exp1 = expectation(description: "move to full with animation")
fpc.move(to: .full, animated: true) {
XCTAssertEqual(contentVC1.tableView.showsVerticalScrollIndicator, true)
XCTAssertEqual(contentVC1.tableView.bounces, true)
exp1.fulfill()
}
wait(for: [exp1], timeout: 1.0)
let exp2 = expectation(description: "move to tip with animation")
fpc.move(to: .tip, animated: false) {
XCTAssertEqual(contentVC1.tableView.showsVerticalScrollIndicator, false)
XCTAssertEqual(contentVC1.tableView.bounces, false)
exp2.fulfill()
}
wait(for: [exp2], timeout: 1.0)
// Reset the content vc
let contentVC2 = UITableViewController(nibName: nil, bundle: nil)
XCTAssertEqual(contentVC2.tableView.showsVerticalScrollIndicator, true)
XCTAssertEqual(contentVC2.tableView.bounces, true)
fpc.set(contentViewController: contentVC2)
fpc.track(scrollView: contentVC2.tableView)
fpc.show(animated: false, completion: nil)
XCTAssertEqual(fpc.position, .half)
XCTAssertEqual(contentVC2.tableView.showsVerticalScrollIndicator, false)
XCTAssertEqual(contentVC2.tableView.bounces, false)
}
func test_getBackdropAlpha_1positions() {
class FloatingPanelLayout1Positions: FloatingPanelTestLayout {
let initialPosition: FloatingPanelPosition = .full
let supportedPositions: Set<FloatingPanelPosition> = [.full]
}
let delegate = FloatingPanelTestDelegate()
delegate.layout = FloatingPanelLayout1Positions()
let fpc = FloatingPanelController(delegate: delegate)
fpc.showForTest()
let fullPos = fpc.originYOfSurface(for: .full)
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??
}
func test_getBackdropAlpha_2positions() {
class FloatingPanelLayout2Positions: FloatingPanelTestLayout {
let initialPosition: FloatingPanelPosition = .half
let supportedPositions: Set<FloatingPanelPosition> = [.half, .full]
}
let delegate = FloatingPanelTestDelegate()
delegate.layout = FloatingPanelLayout2Positions()
let fpc = FloatingPanelController(delegate: delegate)
fpc.showForTest()
let fullPos = fpc.originYOfSurface(for: .full)
let halfPos = fpc.originYOfSurface(for: .half)
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)
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)
}
func test_getBackdropAlpha_2positionsWithHidden() {
class FloatingPanelLayout2Positions: FloatingPanelTestLayout {
let initialPosition: FloatingPanelPosition = .hidden
let supportedPositions: Set<FloatingPanelPosition> = [.hidden, .full]
}
let delegate = FloatingPanelTestDelegate()
delegate.layout = FloatingPanelLayout2Positions()
let fpc = FloatingPanelController(delegate: delegate)
fpc.showForTest()
let fullPos = fpc.originYOfSurface(for: .full)
let hiddenPos = fpc.originYOfSurface(for: .hidden)
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)
}
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 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)
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)
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)
}
func test_targetPosition_1positions() {
class FloatingPanelLayout1Positions: FloatingPanelTestLayout {
let initialPosition: FloatingPanelPosition = .full
let supportedPositions: Set<FloatingPanelPosition> = [.full]
}
let delegate = FloatingPanelTestDelegate()
delegate.layout = FloatingPanelLayout1Positions()
let fpc = FloatingPanelController(delegate: delegate)
fpc.showForTest()
let fullPos = fpc.originYOfSurface(for: .full)
fpc.move(to: .full, animated: false)
assertTargetPosition(fpc.floatingPanel, with: [
(#line, fullPos - 10.0, CGPoint(x: 0.0, y: 1000.0), .full), // redirect
(#line, fullPos, CGPoint(x: 0.0, y: -1000.0), .full), // redirect
(#line, fullPos, CGPoint(x: 0.0, y: -100.0), .full), // redirect
(#line, fullPos, CGPoint(x: 0.0, y: 0.0), .full),
(#line, fullPos, CGPoint(x: 0.0, y: 100.0), .full), // redirect
(#line, fullPos, CGPoint(x: 0.0, y: 1000.0), .full), // redirect
(#line, fullPos + 10.0, CGPoint(x: 0.0, y: 100.0), .full), // redirect
])
}
func test_targetPosition_2positions() {
class FloatingPanelLayout2Positions: FloatingPanelTestLayout {
let initialPosition: FloatingPanelPosition = .half
let supportedPositions: Set<FloatingPanelPosition> = [.half, .full]
}
let delegate = FloatingPanelTestDelegate()
delegate.layout = FloatingPanelLayout2Positions()
let fpc = FloatingPanelController(delegate: delegate)
fpc.showForTest()
let fullPos = fpc.originYOfSurface(for: .full)
let halfPos = fpc.originYOfSurface(for: .half)
fpc.move(to: .full, animated: false)
assertTargetPosition(fpc.floatingPanel, with: [
(#line, fullPos - 10.0, CGPoint(x: 0.0, y: 1000.0), .half), // project to half
(#line, fullPos, CGPoint(x: 0.0, y: -1000.0), .full), // redirect
(#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, CGPoint(x: 0.0, y: 1000.0), .half), // project to half
(#line, fullPos + 10.0, CGPoint(x: 0.0, y: 100.0), .full), // redirect
(#line, halfPos - 10.0, CGPoint(x: 0.0, y: -100.0), .half), // redirect
(#line, halfPos, CGPoint(x: 0.0, y: -1000.0), .full), // 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), .half), // redirect
(#line, halfPos + 10.0, CGPoint(x: 0.0, y: -1000.0), .full), // project to full
])
fpc.move(to: .half, animated: false)
assertTargetPosition(fpc.floatingPanel, with: [
(#line, fullPos - 10.0, CGPoint(x: 0.0, y: 1000.0), .half), // project to half
(#line, fullPos, CGPoint(x: 0.0, y: -1000.0), .full), // redirect
(#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, CGPoint(x: 0.0, y: 1000.0), .half), // project to half
(#line, fullPos + 10.0, CGPoint(x: 0.0, y: 100.0), .full), // redirect
(#line, halfPos - 10.0, CGPoint(x: 0.0, y: -100.0), .half), // redirect
(#line, halfPos, CGPoint(x: 0.0, y: -1000.0), .full), // 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), .half), // redirect
(#line, halfPos + 10.0, CGPoint(x: 0.0, y: -1000.0), .full), // project to full
])
}
func test_targetPosition_2positionsWithHidden() {
class FloatingPanelLayout2Positions: FloatingPanelTestLayout {
let initialPosition: FloatingPanelPosition = .hidden
let supportedPositions: Set<FloatingPanelPosition> = [.hidden, .full]
}
let delegate = FloatingPanelTestDelegate()
delegate.layout = FloatingPanelLayout2Positions()
let fpc = FloatingPanelController(delegate: delegate)
fpc.showForTest()
let fullPos = fpc.originYOfSurface(for: .full)
let hiddenPos = fpc.originYOfSurface(for: .hidden)
fpc.move(to: .full, animated: false)
assertTargetPosition(fpc.floatingPanel, with: [
(#line, fullPos - 10.0, CGPoint(x: 0.0, y: 1000.0), .hidden), // project to hidden
(#line, fullPos, CGPoint(x: 0.0, y: -1000.0), .full), // redirect
(#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, CGPoint(x: 0.0, y: 1000.0), .hidden), // project to hidden
(#line, fullPos + 10.0, CGPoint(x: 0.0, y: 100.0), .full), // redirect
(#line, hiddenPos - 10.0, CGPoint(x: 0.0, y: -100.0), .hidden), // redirect
(#line, hiddenPos, CGPoint(x: 0.0, y: -1000.0), .full), // project to full
(#line, hiddenPos, CGPoint(x: 0.0, y: -100.0), .hidden),
(#line, hiddenPos, CGPoint(x: 0.0, y: 0.0), .hidden),
(#line, hiddenPos, CGPoint(x: 0.0, y: 100.0), .hidden),
(#line, hiddenPos, CGPoint(x: 0.0, y: 1000.0), .hidden), // redirect
(#line, hiddenPos + 10.0, CGPoint(x: 0.0, y: -1000.0), .full), // project to full
])
fpc.move(to: .hidden, animated: false)
assertTargetPosition(fpc.floatingPanel, with: [
(#line, fullPos - 10.0, CGPoint(x: 0.0, y: 1000.0), .hidden), // project to hidden
(#line, fullPos, CGPoint(x: 0.0, y: -1000.0), .full), // redirect
(#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, CGPoint(x: 0.0, y: 1000.0), .hidden), // project to hidden
(#line, fullPos + 10.0, CGPoint(x: 0.0, y: 100.0), .full), // redirect
(#line, hiddenPos - 10.0, CGPoint(x: 0.0, y: -100.0), .hidden), // redirect
(#line, hiddenPos, CGPoint(x: 0.0, y: -1000.0), .full), // project to full
(#line, hiddenPos, CGPoint(x: 0.0, y: -100.0), .hidden),
(#line, hiddenPos, CGPoint(x: 0.0, y: 0.0), .hidden),
(#line, hiddenPos, CGPoint(x: 0.0, y: 100.0), .hidden),
(#line, hiddenPos, CGPoint(x: 0.0, y: 1000.0), .hidden), // redirect
(#line, hiddenPos + 10.0, CGPoint(x: 0.0, y: -1000.0), .full), // project to full
])
}
func test_targetPosition_2positionsFromFull() {
let delegate = FloatingPanelTestDelegate()
delegate.layout = FloatingPanelLayout3Positions()
let fpc = FloatingPanelController(delegate: delegate)
fpc.showForTest()
let fullPos = fpc.originYOfSurface(for: .full)
let halfPos = fpc.originYOfSurface(for: .half)
let tipPos = fpc.originYOfSurface(for: .tip)
// From .full
fpc.move(to: .full, animated: false)
assertTargetPosition(fpc.floatingPanel, with: [
(#line, fullPos - 500.0, CGPoint(x: 0.0, y: -100.0), .full), // far from topMostState
(#line, fullPos - 500.0, CGPoint(x: 0.0, y: 0.0), .full), // far from topMostState
(#line, fullPos - 500.0, CGPoint(x: 0.0, y: 100.0), .full), // far from topMostState
(#line, fullPos - 10.0, CGPoint(x: 0.0, y: 3000.0), .half), // block projecting to tip at 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: 500.0), .half), // project to half
(#line, fullPos, CGPoint(x: 0.0, y: 1000.0), .half), // block projecting to tip at half
(#line, fullPos, CGPoint(x: 0.0, y: 3000.0), .half), // block projecting to tip at half
(#line, fullPos + 10.0, CGPoint(x: 0.0, y: 100.0), .full), // redirect
(#line, halfPos - 10.0, CGPoint(x: 0.0, y: -100.0), .half), // redirect
(#line, halfPos, CGPoint(x: 0.0, y: -1000.0), .full), //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), .tip), // project to tip
(#line, halfPos + 10.0, CGPoint(x: 0.0, y: 100.0), .half), // redirect
(#line, tipPos - 10.0, CGPoint(x: 0.0, y: -100.0), .tip), // redirect
(#line, tipPos, CGPoint(x: 0.0, y: -3000.0), .half), // block projecting to full at half
(#line, tipPos, CGPoint(x: 0.0, y: -1000.0), .half), // block projecting to full at half
(#line, tipPos, CGPoint(x: 0.0, y: -500.0), .half), // project to 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: 100.0), .tip),
(#line, tipPos + 10.0, CGPoint(x: 0.0, y: -3000.0), .half), // block projecting to full at half
(#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() {
let delegate = FloatingPanelTestDelegate()
delegate.layout = FloatingPanelLayout3Positions()
let fpc = FloatingPanelController(delegate: delegate)
fpc.showForTest()
let fullPos = fpc.originYOfSurface(for: .full)
let halfPos = fpc.originYOfSurface(for: .half)
let tipPos = fpc.originYOfSurface(for: .tip)
// From .half
fpc.move(to: .half, animated: false)
assertTargetPosition(fpc.floatingPanel, with: [
(#line, fullPos - 500.0, CGPoint(x: 0.0, y: -100.0), .full), // far from topMostState
(#line, fullPos - 500.0, CGPoint(x: 0.0, y: 0.0), .full), // far from topMostState
(#line, fullPos - 500.0, CGPoint(x: 0.0, y: 100.0), .full), // far from topMostState
(#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: 500.0), .half), // project to half
(#line, fullPos, CGPoint(x: 0.0, y: 1000.0), .half), // block projecting to tip at half
(#line, fullPos, CGPoint(x: 0.0, y: 3000.0), .half), // block projecting to tip at half
(#line, fullPos + 10.0, CGPoint(x: 0.0, y: 100.0), .full), // redirect
(#line, halfPos - 10.0, CGPoint(x: 0.0, y: -100.0), .half), // redirect
(#line, halfPos, CGPoint(x: 0.0, y: -1000.0), .full),// 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), .tip), // project to tip
(#line, halfPos + 10.0, CGPoint(x: 0.0, y: 100.0), .half), // redirect
(#line, tipPos - 10.0, CGPoint(x: 0.0, y: -100.0), .tip), // redirect
(#line, tipPos, CGPoint(x: 0.0, y: -3000.0), .half), // block projecting to full at half
(#line, tipPos, CGPoint(x: 0.0, y: -1000.0), .half), // block projecting to full at half
(#line, tipPos, CGPoint(x: 0.0, y: -500.0), .half), // project to 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: 100.0), .tip),
(#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() {
let delegate = FloatingPanelTestDelegate()
delegate.layout = FloatingPanelLayout3Positions()
let fpc = FloatingPanelController(delegate: delegate)
fpc.showForTest()
let fullPos = fpc.originYOfSurface(for: .full)
let halfPos = fpc.originYOfSurface(for: .half)
let tipPos = fpc.originYOfSurface(for: .tip)
// From .tip
fpc.move(to: .tip, animated: false)
assertTargetPosition(fpc.floatingPanel, with: [
(#line, fullPos - 500.0, CGPoint(x: 0.0, y: -100.0), .full), // far from topMostState
(#line, fullPos - 500.0, CGPoint(x: 0.0, y: 0.0), .full), // far from topMostState
(#line, fullPos - 500.0, CGPoint(x: 0.0, y: 100.0), .full), // far from topMostState
(#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: 500.0), .half), // project to half
(#line, fullPos, CGPoint(x: 0.0, y: 1000.0), .half), // block projecting to tip at half
(#line, fullPos, CGPoint(x: 0.0, y: 3000.0), .half), // block projecting to tip at half
(#line, fullPos + 10.0, CGPoint(x: 0.0, y: 100.0), .full), // redirect
(#line, halfPos - 10.0, CGPoint(x: 0.0, y: -100.0), .half), // redirect
(#line, halfPos, CGPoint(x: 0.0, y: -3000.0), .full), // 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), .tip), // project to tip
(#line, halfPos + 10.0, CGPoint(x: 0.0, y: 100.0), .half), // redirect
(#line, tipPos - 10.0, CGPoint(x: 0.0, y: -100.0), .tip), // redirect
(#line, tipPos, CGPoint(x: 0.0, y: -3000.0), .half), // block projecting to full at half
(#line, tipPos, CGPoint(x: 0.0, y: -1000.0), .half), // block projecting to full at half
(#line, tipPos, CGPoint(x: 0.0, y: -500.0), .half), // project to 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: 100.0), .tip),
(#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_3positionsAllProjection() {
let delegate = FloatingPanelTestDelegate()
delegate.layout = FloatingPanelLayout3Positions()
delegate.behavior = FloatingPanelProjectionalBehavior()
let fpc = FloatingPanelController(delegate: delegate)
fpc.showForTest()
let fullPos = fpc.originYOfSurface(for: .full)
let halfPos = fpc.originYOfSurface(for: .half)
let tipPos = fpc.originYOfSurface(for: .tip)
// From .full
fpc.move(to: .full, animated: false)
assertTargetPosition(fpc.floatingPanel, with: [
(#line, fullPos - 10.0, CGPoint(x: 0.0, y: 3000.0), .tip),
(#line, fullPos, CGPoint(x: 0.0, y: 1000.0), .tip),
(#line, fullPos, CGPoint(x: 0.0, y: 3000.0), .tip),
(#line, halfPos, CGPoint(x: 0.0, y: 1000.0), .tip),
(#line, halfPos, CGPoint(x: 0.0, y: -1000.0), .full),
(#line, tipPos, CGPoint(x: 0.0, y: -3000.0), .full),
(#line, tipPos, CGPoint(x: 0.0, y: -1000.0), .full),
(#line, tipPos + 10.0, CGPoint(x: 0.0, y: -3000.0), .full),
])
// From .half
fpc.move(to: .tip, animated: false)
assertTargetPosition(fpc.floatingPanel, with: [
(#line, fullPos, CGPoint(x: 0.0, y: 1000.0), .tip),
(#line, fullPos, CGPoint(x: 0.0, y: 3000.0), .tip),
(#line, tipPos, CGPoint(x: 0.0, y: -3000.0), .full),
(#line, tipPos, CGPoint(x: 0.0, y: -1000.0), .full),
])
// From .tip
fpc.move(to: .tip, animated: false)
assertTargetPosition(fpc.floatingPanel, with: [
(#line, fullPos - 10.0, CGPoint(x: 0.0, y: 3000.0), .tip),
(#line, fullPos, CGPoint(x: 0.0, y: 1000.0), .tip),
(#line, fullPos, CGPoint(x: 0.0, y: 3000.0), .tip),
(#line, halfPos, CGPoint(x: 0.0, y: 1000.0), .tip),
(#line, halfPos, CGPoint(x: 0.0, y: -1000.0), .full),
(#line, tipPos, CGPoint(x: 0.0, y: -3000.0), .full),
(#line, tipPos, CGPoint(x: 0.0, y: -1000.0), .full),
(#line, tipPos + 10.0, CGPoint(x: 0.0, y: -3000.0), .full),
])
}
func test_targetPosition_3positionsWithHidden() {
class FloatingPanelLayout3PositionsWithHidden: FloatingPanelTestLayout {
let initialPosition: FloatingPanelPosition = .hidden
let supportedPositions: Set<FloatingPanelPosition> = [.hidden, .half, .full]
}
let delegate = FloatingPanelTestDelegate()
delegate.layout = FloatingPanelLayout3PositionsWithHidden()
let fpc = FloatingPanelController(delegate: delegate)
fpc.showForTest()
XCTAssertEqual(fpc.position, .hidden)
fpc.move(to: .full, animated: false)
assertTargetPosition(fpc.floatingPanel, with: [
(#line, fpc.surfaceView.frame.minY, CGPoint(x: 0.0, y: 1000.0), .half),
])
fpc.move(to: .half, animated: false)
assertTargetPosition(fpc.floatingPanel, with: [
(#line, fpc.surfaceView.frame.minY, CGPoint(x: 0.0, y: -100.0), .half),
(#line, fpc.surfaceView.frame.minY, CGPoint(x: 0.0, y: -1000.0), .full),
(#line, fpc.surfaceView.frame.minY, CGPoint(x: 0.0, y: 0.0), .half),
(#line, fpc.surfaceView.frame.minY, CGPoint(x: 0.0, y: 1000.0), .hidden),
])
}
func test_targetPosition_3positionsWithHiddenWithoutFull() {
class FloatingPanelLayout3Positions: FloatingPanelTestLayout {
let initialPosition: FloatingPanelPosition = .hidden
let supportedPositions: Set<FloatingPanelPosition> = [.hidden, .tip, .half]
}
let delegate = FloatingPanelTestDelegate()
delegate.layout = FloatingPanelLayout3Positions()
delegate.behavior = FloatingPanelProjectionalBehavior()
let fpc = FloatingPanelController(delegate: delegate)
fpc.showForTest()
XCTAssertEqual(fpc.position, .hidden)
let halfPos = fpc.originYOfSurface(for: .half)
let tipPos = fpc.originYOfSurface(for: .tip)
//let hiddenPos = fpc.originYOfSurface(for: .hidden)
fpc.move(to: .half, animated: false)
assertTargetPosition(fpc.floatingPanel, with: [
(#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: 385.0), .tip), // projection
(#line, halfPos, CGPoint(x: 0.0, y: 1000.0), .hidden), // projection
(#line, halfPos + 10.0, CGPoint(x: 0.0, y: 100.0), .half), // redirection
(#line, tipPos - 10.0, CGPoint(x: 0.0, y: -100.0), .tip), // redirection
(#line, tipPos, CGPoint(x: 0.0, y: -3000.0), .half), //projection
(#line, tipPos, CGPoint(x: 0.0, y: -10.0), .tip),
(#line, tipPos, CGPoint(x: 0.0, y: 0.0), .tip),
(#line, tipPos, CGPoint(x: 0.0, y: 10.0), .tip),
(#line, tipPos, CGPoint(x: 0.0, y: 1000.0), .hidden), //projection
(#line, tipPos + 10.0, CGPoint(x: 0.0, y: 10.0), .tip), // redirection
(#line, tipPos - 10.0, CGPoint(x: 0.0, y: 10.0), .tip), // redirection
])
fpc.move(to: .tip, animated: false)
assertTargetPosition(fpc.floatingPanel, with: [
(#line, tipPos, CGPoint(x: 0.0, y: -100.0), .tip),
(#line, tipPos, CGPoint(x: 0.0, y: -1000.0), .half),
(#line, tipPos, CGPoint(x: 0.0, y: 0.0), .tip),
(#line, tipPos, CGPoint(x: 0.0, y: 1000.0), .hidden),
])
}
}
private class FloatingPanelLayout3Positions: FloatingPanelTestLayout {
let initialPosition: FloatingPanelPosition = .tip
let supportedPositions: Set<FloatingPanelPosition> = [.tip, .half, .full]
}
private typealias TestParameter = (UInt, CGFloat,CGPoint, FloatingPanelPosition)
private func assertTargetPosition(_ floatingPanel: FloatingPanel, 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
}
}
+46
View File
@@ -0,0 +1,46 @@
//
// 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
}
}
}
-14
View File
@@ -1,14 +0,0 @@
//
// Created by Shin Yamamoto on 2018/09/18.
// Copyright © 2018 Shin Yamamoto. All rights reserved.
//
import XCTest
@testable import FloatingPanelController
class ViewTests: XCTestCase {
override func setUp() {}
override func tearDown() {}
}
+27
View File
@@ -0,0 +1,27 @@
// swift-tools-version:5.0
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "FloatingPanel",
platforms: [
.iOS(.v10)
],
products: [
// Products define the executables and libraries produced by a package, and make them visible to other packages.
.library(
name: "FloatingPanel",
targets: ["FloatingPanel"]),
],
dependencies: [
// Dependencies declare other packages that this package depends on.
// .package(url: /* package url */, from: "1.0.0"),
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages which this package depends on.
.target(name: "FloatingPanel", path: "Framework/Sources"),
],
swiftLanguageVersions: [.version("5")]
)
+23 -5
View File
@@ -4,6 +4,8 @@
[![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
@@ -42,6 +44,7 @@ The new interface displays the related contents and utilities in parallel as a u
- [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)
- [License](#license)
@@ -67,7 +70,9 @@ Examples are here.
## Requirements
FloatingPanel is written in Swift. It can be built by Xcode 9.4.1 or later. Compatible with iOS 10.0+.
FloatingPanel is written in Swift 4.0+. It can be built by Xcode 9.4.1 or later. Compatible with iOS 10.0+.
✏️ The default Swift version is 4.0 because it avoids build errors with Carthage on each Xcode version from the source compatibility between Swift 4.0, 4.2 and 5.0.
## Installation
@@ -80,6 +85,8 @@ it, simply add the following line to your Podfile:
pod 'FloatingPanel'
```
✏️ To suppress "Swift Conversion" warnings in Xcode, please set a Swift version to `SWIFT_VERSION` for the project in your Podfile. It will be resolved in CocoaPods v1.7.0.
### Carthage
For [Carthage](https://github.com/Carthage/Carthage), add the following to your `Cartfile`:
@@ -88,6 +95,10 @@ For [Carthage](https://github.com/Carthage/Carthage), add the following to your
github "scenee/FloatingPanel"
```
### Swift Package Manager with Xcode 11
Follow [this doc](https://developer.apple.com/documentation/swift_packages/adding_package_dependencies_to_your_app).
## Getting Started
### Add a floating panel as a child view controller
@@ -141,7 +152,7 @@ self.present(fpc, animated: true, completion: nil)
You can show a floating panel over UINavigationController from the container view controllers as a modality of `.overCurrentContext` style.
NOTE: FloatingPanelController has the custom presentation controller. If you would like to customize the presentation/dismissal, please see [FloatingPanelTransitioning](https://github.com/SCENEE/FloatingPanel/blob/master/Framework/Sources/FloatingPanelTransitioning.swift).
✏️ FloatingPanelController has the custom presentation controller. If you would like to customize the presentation/dismissal, please see [FloatingPanelTransitioning](https://github.com/SCENEE/FloatingPanel/blob/master/Framework/Sources/FloatingPanelTransitioning.swift).
## View hierarchy
@@ -151,8 +162,9 @@ NOTE: FloatingPanelController has the custom presentation controller. If you wou
FloatingPanelController.view (FloatingPanelPassThroughView)
├─ .backdropView (FloatingPanelBackdropView)
└─ .surfaceView (FloatingPanelSurfaceView)
├─ .contentView == FloatingPanelController.contentViewController.view
└─ .grabberHandle (GrabberHandleView)
├─ .containerView (UIView)
└─ .contentView (FloatingPanelController.contentViewController.view)
└─ .grabberHandle (GrabberHandleView)
```
## Usage
@@ -251,7 +263,7 @@ class FloatingPanelLandscapeLayout: FloatingPanelLayout {
public func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint] {
return [
surfaceView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuid.leftAnchor, constant: 8.0),
surfaceView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 8.0),
surfaceView.widthAnchor.constraint(equalToConstant: 291),
]
}
@@ -429,6 +441,12 @@ A `FloatingPanelController` object proxies an action for `show(_:sender)` to the
It's a great way to decouple between a floating panel and the content VC.
### UISearchController issue
`UISearchController` isn't able to be used with `FloatingPanelController` by the system design.
Because `UISearchController` automatically presents itself modally when a user interacts with the search bar, and then it swaps the superview of the search bar to the view managed by itself while it displays. As a result, `FloatingPanelController` can't control the search bar when it's active, as you can see from [the screen shot](https://github.com/SCENEE/FloatingPanel/issues/248#issuecomment-521263831).
### FloatingPanelSurfaceView's issue on iOS 10
* On iOS 10, `FloatingPanelSurfaceView.cornerRadius` isn't not automatically masked with the top rounded corners because of `UIVisualEffectView` issue. See https://forums.developer.apple.com/thread/50854.