Compare commits

...

136 Commits

Author SHA1 Message Date
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
Shin Yamamoto 5214bd8936 Release v1.3.4 2019-02-14 09:13:09 +09:00
Shin Yamamoto a1195be08e Merge pull request #134 from g00m/feature/fix_typo_in_readme
Fix typo in `README.md`
2019-02-14 09:11:53 +09:00
Etienne Negro b69d366538 Fix typo in README-md 2019-02-13 14:28:39 +01:00
Shin Yamamoto c9de6f0dc3 Merge pull request #131 from modestman/master
Fix issue with jumping floating panel while dragging
2019-02-12 09:50:10 +09:00
Anton Glezman 21be693a9a Fix issue with jumping floating panel while dragging 2019-02-11 12:28:13 +03:00
Shin Yamamoto 129362fcd0 Merge pull request #129 from SCENEE/fix-typo
Fix typo
2019-02-06 09:40:58 +09:00
Shin Yamamoto 75e2fcc3ce Fix README 2019-02-05 22:46:56 +09:00
Shin Yamamoto 4f56b57b0e Replace FloatingPanel.panGesture with panGestureRecognizer 2019-02-05 22:10:03 +09:00
Shin Yamamoto f9bbdf3427 Update ISSUE_TEMPLATE.md 2019-02-02 18:19:06 +09:00
Shin Yamamoto fcf200e169 Merge pull request #124 from SCENEE/release-1.3.3
Release v1.3.3
2019-02-02 12:19:59 +09:00
Shin Yamamoto 7d668c8525 Release v1.3.3 2019-02-02 11:38:37 +09:00
Shin Yamamoto ebd4a32bfc Merge pull request #123 from SCENEE/fix-cut-off-by-mask
Expand the surface mask's height
2019-02-02 11:37:20 +09:00
Shin Yamamoto 6aa739231d Expand the surface mask's height
`FloatingPanelSurfaceView.updateContentViewMask()` causes a content to be cut off.
2019-02-01 10:19:51 +09:00
Shin Yamamoto 8877d32ced Merge pull request #122 from SCENEE/fix-animated-presentation-modally
Fix the presentation modally when fpc is reused
2019-01-31 19:22:46 +09:00
Shin Yamamoto 7f025ae845 Remove the unnecessary code to fix the presentation modally
It's added at `a095ace` commit, but now not needed by the previous
commit.
2019-01-31 12:48:36 +09:00
Shin Yamamoto 1b2dae2135 Fix presentation modally when fpc is reused 2019-01-30 13:09:04 +09:00
Shin Yamamoto 6e4e9df616 Merge pull request #114 from datwelk/hotfix/restore-scroll-view-delegate
Restore original scroll view delegate when updating content VC
2019-01-28 19:11:39 +09:00
Damiaan Twelker 49e868a505 restore original scroll view delegate when updating content viewcontroller 2019-01-26 12:41:17 +01:00
Shin Yamamoto f1b70e0367 Add the issue template 2019-01-25 22:08:44 +09:00
Shin Yamamoto 1d0e747578 Update README.md 2019-01-24 11:00:45 +09:00
Shin Yamamoto 2c72d07cab Merge pull request #97 from SCENEE/support-full-screen
Support full screen layout
2019-01-19 16:41:32 +09:00
Shin Yamamoto 31c057f9f8 Fix typo 2019-01-19 14:53:36 +09:00
Shin Yamamoto d3033df9da Add FloatingPanelFullScreenLayout doc comment 2019-01-19 14:51:04 +09:00
Shin Yamamoto 459d82b1c6 Update 'Show Tab Bar' for full screen layout 2019-01-19 14:06:23 +09:00
Shin Yamamoto 85d7ca640e Stop manipulating a scroll content inset for FloatingPanelFullScreenLayout
It's better that the manipulation should be operated in a user application code.
2019-01-19 14:06:23 +09:00
Shin Yamamoto c1b7f2f092 Merge pull request #106 from SCENEE/release-1.3.2
Release v1.3.2
2019-01-18 09:31:28 +09:00
Shin Yamamoto b7a7e0d4ad Release v1.3.2 2019-01-17 10:47:45 +09:00
Shin Yamamoto dc7f6d58f9 Fix the swift version in podspec 2019-01-17 10:47:45 +09:00
Shin Yamamoto a2a10bd0d3 Merge pull request #104 from SCENEE/fix-modal-presentation
Fix the modal presentation
2019-01-15 09:31:39 +09:00
Shin Yamamoto b54c8ee6ee Merge pull request #105 from CedricGatay/fix/delegateIgnored
fix(delegateIgnored): Allow to set delegate on init
2019-01-14 19:43:39 +09:00
Cedric Gatay 08d275690a fix(delegateIgnored): fix for animation use case
Do not force update layout to allow the user to animate the change later on.
2019-01-14 11:05:50 +01:00
Cedric Gatay 1c307f751e fix(delegateIgnored): listen on delegate change
Properly listens for delegate changes and trigger behavior / layout changes accordingly.

Made constructor parameter name explicit.
2019-01-14 08:49:23 +01:00
Cedric Gatay 6f06a0f7fc fix(delegateIgnored): Allow to set delegate on init
Typical use case is :

```
let floatingVC = FloatingPanelController()
floatingVC.delegate = self
```

This PR allows to set the delegate straight away by using `let floatingVC = FloatingPanelController(self)`
Its true reason is that the setup code that fetches behavior / layout through delegate is called without the delegate being set is useless on `init`ing.
Another implementation could be observing the `didSet` event on delegate to do the `setUp` for `FloatingPanel`
2019-01-12 15:15:48 +01:00
Shin Yamamoto a095ace30e Fix the modal presentation 2019-01-11 19:04:39 +09:00
Shin Yamamoto a486f61f5f Merge pull request #102 from SCENEE/fix-secound-touch-crash
Fix a crash on tap dismissal with a second touch
2019-01-10 09:38:06 +09:00
Shin Yamamoto 14e0abc240 Merge pull request #103 from SCENEE/fix-unsatisfiable-constraints-error
Fix unsatisfiable constraints error for safe area bottom on full state
2019-01-10 09:37:46 +09:00
Shin Yamamoto 32203c48bd Fix unsatisfiable constraints error for safe area bottom on full state 2019-01-10 09:05:17 +09:00
Shin Yamamoto 9d6024f603 Fix a crash on tap dismissal with a second touch
For example, a user tap the backdrop view to dismiss it while dragging
the surface view on presentation modally.
2019-01-10 08:43:46 +09:00
Shin Yamamoto a4dd4e48e7 Merge pull request #98 from SCENEE/support-swift4.1
Support Swift 4.1
2019-01-10 08:25:50 +09:00
Shin Yamamoto e6f7456a0f Merge pull request #101 from SCENEE/fix-touch-handling-on-presentation-modally
Fix the event handling on presentation modally
2019-01-10 08:25:26 +09:00
Shin Yamamoto fca79c9b0c Fix the event handling on presentation modally
If an alpha of the controller's backdrop view is zero, the presentation controller
must not block any touch event outside of surface view.
2019-01-09 09:32:17 +09:00
Shin Yamamoto 4ad7f11e93 Support Swift 4.1 2019-01-05 21:44:05 +09:00
Shin Yamamoto 0412bdc996 Support full screen layout 2019-01-05 12:26:49 +09:00
Shin Yamamoto 31faeaada3 Merge pull request #93 from SCENEE/release-1.3.1
Release v1.3.1
2018-12-30 09:52:25 +09:00
Shin Yamamoto 72539ca973 Release v1.3.1 2018-12-30 09:22:33 +09:00
Shin Yamamoto ef94630aa1 Update README 2018-12-30 09:22:33 +09:00
Shin Yamamoto cb696f9992 Merge pull request #86 from SCENEE/improve-samples-app
Fix layout bugs on v1.3.0
2018-12-29 14:33:13 +09:00
Shin Yamamoto aa23e404e1 Fix a content offset of a tracking scroll view
This bug only happens on 'Shoe Tab Bar' in Samples app on landscape
orientation.
2018-12-28 16:08:10 +09:00
Shin Yamamoto 4c0749640f Remove an unnecessary comment 2018-12-28 15:13:59 +09:00
Shin Yamamoto ee5661f304 Fix UI freezes on presentation modally from the second time 2018-12-28 15:13:59 +09:00
Shin Yamamoto ddefdc4f34 Fix crash when content view is nil 2018-12-28 15:13:59 +09:00
Shin Yamamoto f2a0af1646 Fix intrinsic height 2018-12-28 15:13:59 +09:00
Shin Yamamoto 7ded61c2bc No longer need traitCollectionDidChange(_:) 2018-12-28 15:13:59 +09:00
Shin Yamamoto 08aabcf6dd Fix bugs on iOS 10 2018-12-28 15:13:59 +09:00
Shin Yamamoto e19af7e67d Set up the height constraints with the root view's heightAnchor
* `vc.view.bounds.height` causes some layout problems. This change
  makes a content view layout more robust.
* Enable to configure the content-hugging and compression-resistance
2018-12-28 15:13:59 +09:00
Shin Yamamoto 62aa07e28e Refactor layout logic
- Update README
- Remove FloatingPanelSurfaceWrapperView
- Remove content wrapper view of FloatingPanelSurfaceView
- Remove {setUp,tearDown}Views in FloatingPanel
- Modify timing to call FloatingPanelLayoutAdapter.checkLayoutConsistance()
- Fix invalid surface height on orientation change
- Fix a layout problem on SafeArea.Top
    - Fix invalid top inset of safe area on a navigation bar with search bar
- Fix content offsets of a tracking scroll view
    - Fix the content offsets on orientation change(Regression)
- Fix FloatingPanelPresentationController
- Fix intrinsic height handling
- Reduce re-rendering the surface view unexpectedly
2018-12-28 15:13:59 +09:00
Shin Yamamoto 03b0bf747e Improve Sample apps for debugging
- Add InspectableViewController
- Add version label in Samples app
2018-12-28 15:13:59 +09:00
Shin Yamamoto 8dc75aed55 Always disable Auto Layout for Safe area bottom 2018-12-28 15:13:59 +09:00
Shin Yamamoto d5f5e99010 Fix dismiss swizzling 2018-12-28 15:13:59 +09:00
Shin Yamamoto 18c46d191e Fix panel is duplicated on orientation change 2018-12-28 15:13:59 +09:00
Shin Yamamoto e9f92430b2 Add a disable tracking sample to edit a table view 2018-12-28 15:13:59 +09:00
Shin Yamamoto 3106865449 Add SettingsViewController in Samples App 2018-12-28 15:13:44 +09:00
Shin Yamamoto 375e7a59e2 Merge pull request #89 from SCENEE/fix-failure-requirements
Fix failure requirements
2018-12-28 12:59:37 +09:00
Shin Yamamoto bc4a2def42 Add FloatingPanelControllerDelegate.floatingPanel(_:shouldRecognizeSimultaneouslyWith:) 2018-12-20 11:20:22 +09:00
Shin Yamamoto 8204a6cf27 Any gestures don't wait for the pan gesture 2018-12-18 21:43:18 +09:00
Shin Yamamoto 797292dbe5 Merge pull request #85 from SCENEE/release-1.3.0
Release v1.3.0(conflicts resolved )
2018-12-15 09:32:16 +09:00
Shin Yamamoto 6e7a33b3a1 Merge branch 'next' into release-1.3.0 2018-12-15 08:55:39 +09:00
Shin Yamamoto 2bfea00c72 Release v1.3.0 2018-12-15 08:48:00 +09:00
Shin Yamamoto 81441a724b Fix a crash in checkLayoutConsistance()
The crash happened on orientation change from landscape to portrait in
"Show Tab Bar" scene.
2018-12-12 11:47:02 +09:00
Shin Yamamoto 82c8d8dd9a Fix the boundary condition for top/bottom buffers 2018-12-12 09:59:19 +09:00
Shin Yamamoto 8751a9fe53 Fix the broken landscape layout 2018-12-12 09:59:19 +09:00
Shin Yamamoto 9cf561fcf1 Update README 2018-12-12 09:59:19 +09:00
Shin Yamamoto 332559c67d Update Samples
- Make "Show Intrinsic View" panel dismissable
- Add tap-to-hide sample
2018-12-12 09:59:19 +09:00
Shin Yamamoto 56557f0092 Merge pull request #81 from SCENEE/release-1.2.3
Release v1.2.3
2018-12-07 19:10:14 +09:00
Shin Yamamoto 1e6cb7b1ad Release v1.2.3 2018-12-07 16:50:08 +09:00
Shin Yamamoto 7f419b7e78 Merge pull request #79 from SCENEE/improve-intrinsic-height
Improve intrinsic height
2018-12-07 16:23:07 +09:00
Shin Yamamoto 973a90e071 Update README 2018-12-07 15:31:59 +09:00
Shin Yamamoto 66a8ca36e4 Fix FloatingPanelIntrinsicLayout
- `.full` position's height must be the intrinsic height.
- Work it for a safe area bottom anchor
- Remove FloatingPanelIntrinsicLayout.contentViewController because it
isn't actually needed
2018-12-07 14:37:48 +09:00
Shin Yamamoto 3715007156 Merge pull request #63 from SCENEE/feat-modality
FloatingPanelController as a Modality
2018-12-07 14:36:43 +09:00
Shin Yamamoto 5158685c02 Update README
- Show/Hide usage
- Modality usage
2018-12-07 13:25:17 +09:00
Shin Yamamoto 61d3371ea5 Fix a bug in Samples app 2018-12-06 09:11:25 +09:00
Shin Yamamoto 0491507e67 Build 'next' branch on Travis CI 2018-12-06 09:11:25 +09:00
Shin Yamamoto fcd4ad874a Use autoresizing masks instead of Auto Layout constraints 2018-12-06 09:11:25 +09:00
Shin Yamamoto e2668fcdf2 Fix the condition of removal interaction 2018-12-06 09:11:25 +09:00
Shin Yamamoto f3ac2b2cec Add the modal dismissal on backdrop tap 2018-12-06 09:11:25 +09:00
Shin Yamamoto 8b84391e36 Fix FloatingPanelModalTransitioning 2018-12-06 09:11:25 +09:00
Shin Yamamoto 1b3ca347f5 Update comment of FloatingPanelSurfaceView.grabberHandle 2018-12-06 09:11:25 +09:00
Shin Yamamoto 2dced7bfbf Improve FloatingPanelController implementation 2018-12-06 09:11:25 +09:00
Shin Yamamoto 8ba4ce36a1 Merge pull request #75 from SCENEE/fix-animation-wobbling
The default interaction animator should be uninterruptible
2018-12-05 16:01:56 +09:00
Shin Yamamoto cf60b09225 Fix invalid backdrop alpha
The bug was found when I commented out `animator.startAnimation()`.
2018-12-05 14:08:31 +09:00
Shin Yamamoto 427ec45d42 Let the default interaction animator be uninterruptible
Because an interruptible animator causes a wobbling at the animation start.
when a user flick a panel quickly to move to full position nearby the position.
2018-12-05 13:56:43 +09:00
Shin Yamamoto ac9f8fe89c Update Samples App for .hidden 2018-12-04 22:25:22 +09:00
Shin Yamamoto 6817990555 Add .hidden position 2018-12-04 22:25:22 +09:00
Shin Yamamoto d395cde316 Fix a backdrop's cut off on orientation change 2018-12-04 22:25:22 +09:00
Shin Yamamoto 20272eccb8 Add FloatingPanelTransitioning to present as Modality 2018-12-04 22:25:22 +09:00
Shin Yamamoto 091ae8abff Add 'Show Floating Panel Modal' in Samples app 2018-12-04 22:25:22 +09:00
Shin Yamamoto 6e87690649 FloatingPanelController as a Modality
* Change a floating panel view hierarchy
* Add FloatingPanelController.{show,hide}(animated:completion)
2018-12-04 22:24:26 +09:00
Shin Yamamoto d5a1bd3859 Update Samples App to use dismiss(animated:completion) 2018-12-04 22:21:50 +09:00
Shin Yamamoto a1e4643a25 Swizzling UIViewController.dismiss(animated:completion) for a content VC 2018-12-04 22:21:50 +09:00
Shin Yamamoto 71c0450614 Merge pull request #74 from SCENEE/feat-intrinsic-height
Feat intrinsic height
2018-12-04 22:20:45 +09:00
Shin Yamamoto d469caad69 Fix unexpected assertion failure 2018-12-04 08:19:41 +09:00
Derek Schade 5cc3d4fbfb Enabled inset checks again 2018-12-04 08:14:46 +09:00
Derek Schade a8691ee3a5 Update layout when layout is intrinsiclayout 2018-12-04 08:14:46 +09:00
Derek Schade 91d7941921 Add IntrinsicPanelLayout 2018-12-04 08:14:46 +09:00
Derek Schade 0bc7a0953e Intrinsic layout protocol 2018-12-04 08:14:46 +09:00
Derek Schade c60bea5952 add intrinsic viewcontroller to storyboard 2018-12-04 08:14:46 +09:00
Shin Yamamoto 4db648ad25 Merge pull request #72 from SCENEE/release-v1.2.2
Release v1.2.2
2018-12-03 12:27:47 +09:00
Shin Yamamoto 0bb8342873 Release v1.2.2 2018-12-03 09:25:09 +09:00
Shin Yamamoto d4f2a88fdf Merge pull request #71 from SCENEE/fix-bugs
Fix scroll tracking bugs
2018-12-01 15:45:41 +09:00
Shin Yamamoto af45d39841 Fix panning at grabber Area 2018-12-01 12:36:47 +09:00
Shin Yamamoto 66f5b0b210 Fix an invalid content offset on height change 2018-12-01 11:08:33 +09:00
Shin Yamamoto 4a3b79f1b4 Update README 2018-11-29 13:06:27 +09:00
Shin Yamamoto 12a100def8 Merge pull request #68 from SCENEE/release-1.2.1
Release v1.2.1
2018-11-26 13:20:24 +09:00
Shin Yamamoto 47971f607a Release v1.2.1 2018-11-26 09:40:14 +09:00
Shin Yamamoto 03a4d342a3 Merge pull request #66 from Galeas/fix-remove-backdrop-view-on-dismiss
Fix remove backdrop view on dismiss
2018-11-25 11:11:22 +09:00
Evgeniy Branitsky 4f5abfefec Removing backdrop view 2018-11-24 11:21:56 +03:00
Shin Yamamoto e1ee3c06e8 Merge pull request #62 from SCENEE/feat-make-animator-interruptible
Make the animated interaction interruptible
2018-11-22 20:21:11 +09:00
Shin Yamamoto 17ba704472 Make the animated interaction interruptible 2018-11-22 14:26:18 +09:00
Shin Yamamoto e5391fa1f4 Merge pull request #61 from SCENEE/improve-delegate
Improve delegate
2018-11-22 13:54:02 +09:00
Shin Yamamoto c0647017b5 Add FloatingPanelControllerDelegate.floatingPanelDidChangePosition(_:) 2018-11-21 09:10:50 +09:00
Shin Yamamoto 3686bb4b44 Clean up code for scrollGestureRecognizers 2018-11-20 11:08:23 +09:00
Shin Yamamoto 76c8ca4b20 Merge pull request #56 from SCENEE/fix-gesture-handling
Fix the gesture handling
2018-11-20 11:07:14 +09:00
Shin Yamamoto c53e64027b Merge pull request #45 from SCENEE/release-1.2.0
Release v1.2.0
2018-11-17 10:12:10 +09:00
Shin Yamamoto 281504c9c6 Fix the gesture handling
* Fix a detection of a long press gesture in content VC
* Fix a SwipeActionPanGesture is not working in the tracking scroll
    * Update DebugTableViewController to test it
2018-11-17 09:09:07 +09:00
19 changed files with 1766 additions and 658 deletions
+27
View File
@@ -0,0 +1,27 @@
> Please fill out this template appropriately when filing a bug report.
>
> Please remove this line and everything above it before submitting.
### Short description
### Expected behavior
### Actual behavior
### Steps to reproduce
**Code example that reproduces the issue**
### Environment
**Library version**
**Installation method**
- [ ] CocoaPods
- [ ] Carthage
- [ ] Git submodules
**iOS version(s)**
**Xcode version**
+18 -7
View File
@@ -2,12 +2,13 @@ language: swift
branches:
only:
- master
- next
cache:
directories:
- build
- vendor
- /usr/local/Homebrew
- $HOME/Library/Caches/Homebrew
before_cache:
- brew cleanup
env:
global:
- LANG=en_US.UTF-8
@@ -15,7 +16,17 @@ env:
skip_cleanup: true
jobs:
include:
- stage: carthage
- stage: Build framework(swift 4.1)
osx_image: xcode9.4
script:
- xcodebuild -scheme FloatingPanel clean build
- stage: Build framework(swift 4.2)
osx_image: xcode10
script:
- xcodebuild -scheme FloatingPanel clean build
- stage: Carthage
osx_image: xcode10
before_install:
- brew update
@@ -23,22 +34,22 @@ jobs:
script:
- carthage build --no-skip-current
- stage: podspec
- stage: Podspec
osx_image: xcode10
script:
- pod spec lint
- stage: check Maps example
- stage: Build maps example
osx_image: xcode10
script:
- xcodebuild -scheme Maps -sdk iphonesimulator clean build
- stage: check Stocks example
- stage: Build stocks example
osx_image: xcode10
script:
- xcodebuild -scheme Stocks -sdk iphonesimulator clean build
- stage: check Samples example
- stage: Build samples example
osx_image: xcode10
script:
- xcodebuild -scheme Samples -sdk iphonesimulator clean build
@@ -165,6 +165,7 @@
isa = PBXNativeTarget;
buildConfigurationList = 545DBA1221511E6400CA77B8 /* Build configuration list for PBXNativeTarget "Samples" */;
buildPhases = (
54D7209621D4DB970054A255 /* ShellScript */,
545DB9E621511E6300CA77B8 /* Sources */,
545DB9E721511E6300CA77B8 /* Frameworks */,
545DB9E821511E6300CA77B8 /* Resources */,
@@ -285,6 +286,26 @@
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
54D7209621D4DB970054A255 /* ShellScript */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/usr/libexec/PlistBuddy -c \"Set :CFBundleVersion $(git rev-parse --abbrev-ref HEAD)($(git rev-parse --short HEAD))\" $SRCROOT/$INFOPLIST_FILE\n";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
545DB9E621511E6300CA77B8 /* Sources */ = {
isa = PBXSourcesBuildPhase;
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14460.31" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="RoN-h0-uBD">
<device id="retina4_7" orientation="portrait">
<device id="retina5_9" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
@@ -14,8 +14,8 @@
<scene sceneID="Cjh-iX-VQw">
<objects>
<navigationController id="RoN-h0-uBD" sceneMemberID="viewController">
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="hNW-5m-Omi">
<rect key="frame" x="0.0" y="20" width="375" height="44"/>
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="hNW-5m-Omi">
<rect key="frame" x="0.0" y="44" width="375" height="96"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<connections>
@@ -31,22 +31,22 @@
<objects>
<viewController id="jF4-A0-Eq6" customClass="SampleListViewController" customModule="Samples" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="Smh-Bd-AAc">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" 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="667"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="778"/>
<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"/>
<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.5"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="43.666666666666664"/>
<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.5"/>
<rect key="frame" x="15" y="0.0" width="345" height="43.666666666666664"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
@@ -61,13 +61,19 @@
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="7IS-PU-x0P" firstAttribute="top" secondItem="Smh-Bd-AAc" secondAttribute="top" id="6yd-jv-ey3"/>
<constraint firstItem="7IS-PU-x0P" firstAttribute="leading" secondItem="TkN-Oh-wF8" secondAttribute="leading" id="Z6Y-Dc-cei"/>
<constraint firstItem="7IS-PU-x0P" firstAttribute="bottom" secondItem="TkN-Oh-wF8" secondAttribute="bottom" id="fNW-DP-lhV"/>
<constraint firstItem="7IS-PU-x0P" firstAttribute="trailing" secondItem="TkN-Oh-wF8" secondAttribute="trailing" id="vfY-Rc-FOI"/>
<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="trailing" secondItem="39L-Nq-qfp" secondAttribute="trailing" id="vfY-Rc-FOI"/>
</constraints>
<viewLayoutGuide key="safeArea" id="TkN-Oh-wF8"/>
<viewLayoutGuide key="safeArea" id="39L-Nq-qfp"/>
</view>
<navigationItem key="navigationItem" title="Samples" id="wCF-su-7up"/>
<navigationItem key="navigationItem" title="Samples" id="wCF-su-7up">
<barButtonItem key="rightBarButtonItem" title="Settings" id="rbH-U3-XyA">
<connections>
<action selector="showDebugMenu:" destination="jF4-A0-Eq6" id="j02-db-ZM5"/>
</connections>
</barButtonItem>
</navigationItem>
<connections>
<outlet property="tableView" destination="7IS-PU-x0P" id="YFM-9W-eP4"/>
</connections>
@@ -76,22 +82,96 @@
</objects>
<point key="canvasLocation" x="57" y="27"/>
</scene>
<!--Item 2-->
<!--Settings View Controller-->
<scene sceneID="Bd0-D2-agO">
<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"/>
<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"/>
<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"/>
<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"/>
<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"/>
<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"/>
<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"/>
<connections>
<action selector="toggleLargeTitle:" destination="C1X-9Z-TyQ" eventType="valueChanged" id="FJS-Ty-mCY"/>
</connections>
</switch>
</subviews>
</stackView>
<stackView opaque="NO" contentMode="scaleToFill" spacing="126" translatesAutoresizingMaskIntoConstraints="NO" id="ZtZ-Dz-4cC">
<rect key="frame" x="23.333333333333343" y="116.33333333333334" width="264.66666666666663" height="31"/>
<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"/>
<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"/>
<connections>
<action selector="toggleTranslucent:" destination="C1X-9Z-TyQ" eventType="valueChanged" id="nL4-3L-9hh"/>
</connections>
</switch>
</subviews>
</stackView>
</subviews>
</stackView>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="0hr-ty-yWm" firstAttribute="bottom" secondItem="n93-ZL-fmC" secondAttribute="bottom" id="2Ey-ou-E1M"/>
<constraint firstAttribute="trailing" secondItem="n93-ZL-fmC" secondAttribute="trailing" constant="32" id="DdZ-eB-F5s"/>
<constraint firstItem="n93-ZL-fmC" firstAttribute="leading" secondItem="af9-Zr-Ppc" secondAttribute="leading" constant="32" id="TyK-GP-Ari"/>
<constraint firstItem="n93-ZL-fmC" firstAttribute="top" secondItem="af9-Zr-Ppc" secondAttribute="topMargin" constant="16" id="mbC-6H-z9M"/>
</constraints>
<viewLayoutGuide key="safeArea" id="0hr-ty-yWm"/>
</view>
<size key="freeformSize" width="375" height="197.33000000000001"/>
<connections>
<outlet property="largeTitlesSwicth" destination="js8-Qv-lUC" id="FOm-6k-ffi"/>
<outlet property="translucentSwicth" destination="s6b-j9-8Kw" id="jmf-WH-bzZ"/>
<outlet property="versionLabel" destination="WmC-Tq-NDN" id="Woh-kK-U0m"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="M9h-4V-3M0" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="708" y="-200"/>
</scene>
<!--Layout 2-->
<scene sceneID="lRc-OZ-sL4">
<objects>
<viewController id="RpE-lI-27a" customClass="TabBarContentViewController" customModule="Samples" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="JER-jz-KSq">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Item 2" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="AiP-dx-mFn">
<rect key="frame" x="163.5" y="323" width="48" height="21"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="IvG-yp-yzI">
<rect key="frame" x="20" y="20" width="39" height="30"/>
<rect key="frame" x="20" y="44" width="39" height="30"/>
<state key="normal" title="Close"/>
<connections>
<action selector="closeWithSender:" destination="RpE-lI-27a" eventType="touchUpInside" id="hj3-Xv-6Gq"/>
@@ -101,35 +181,59 @@
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="IvG-yp-yzI" firstAttribute="top" secondItem="2Cd-km-qEk" secondAttribute="top" id="18k-sV-PgT"/>
<constraint firstItem="AiP-dx-mFn" firstAttribute="centerY" secondItem="JER-jz-KSq" secondAttribute="centerY" id="NUc-tM-0dN"/>
<constraint firstItem="AiP-dx-mFn" firstAttribute="centerX" secondItem="JER-jz-KSq" secondAttribute="centerX" id="hwP-mu-Vmz"/>
<constraint firstItem="IvG-yp-yzI" firstAttribute="leading" secondItem="2Cd-km-qEk" secondAttribute="leading" constant="20" id="pYt-jE-CTF"/>
<constraint firstItem="IvG-yp-yzI" firstAttribute="top" secondItem="954-Dk-zvc" secondAttribute="top" id="18k-sV-PgT"/>
<constraint firstItem="954-Dk-zvc" firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="IvG-yp-yzI" secondAttribute="trailing" id="mpr-u5-MZu"/>
<constraint firstItem="IvG-yp-yzI" firstAttribute="leading" secondItem="954-Dk-zvc" secondAttribute="leading" constant="20" id="pYt-jE-CTF"/>
</constraints>
<viewLayoutGuide key="safeArea" id="2Cd-km-qEk"/>
<viewLayoutGuide key="safeArea" id="954-Dk-zvc"/>
</view>
<tabBarItem key="tabBarItem" tag="1" title="Item 2" id="qb3-RB-B28"/>
<tabBarItem key="tabBarItem" tag="1" title="Layout 2" id="qb3-RB-B28"/>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="NhZ-u5-Beh" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="326" y="1575"/>
<point key="canvasLocation" x="-308" y="1546"/>
</scene>
<!--Item 1-->
<!--Layout 3-->
<scene sceneID="r9h-Ql-gIv">
<objects>
<viewController id="pOk-Zm-vD9" customClass="TabBarContentViewController" customModule="Samples" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="85d-ub-G8k">
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="NbG-e8-HdI">
<rect key="frame" x="20" y="44" width="39" height="30"/>
<state key="normal" title="Close"/>
<connections>
<action selector="closeWithSender:" destination="pOk-Zm-vD9" eventType="touchUpInside" id="111-PD-Pop"/>
<action selector="closeWithSender:" destination="bYI-y3-Rzb" eventType="touchUpInside" id="1Rg-YG-TtU"/>
</connections>
</button>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="0ao-SI-QZW" firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="NbG-e8-HdI" secondAttribute="trailing" id="K9F-6x-KWn"/>
<constraint firstItem="NbG-e8-HdI" firstAttribute="top" secondItem="0ao-SI-QZW" secondAttribute="top" id="nsE-so-rTl"/>
<constraint firstItem="NbG-e8-HdI" firstAttribute="leading" secondItem="0ao-SI-QZW" secondAttribute="leading" constant="20" id="sF4-Dm-aoY"/>
</constraints>
<viewLayoutGuide key="safeArea" id="0ao-SI-QZW"/>
</view>
<tabBarItem key="tabBarItem" tag="2" title="Layout 3" id="RJD-TF-Sdh"/>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Oe3-FT-q1C" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="332" y="1546"/>
</scene>
<!--Layout 1-->
<scene sceneID="m6X-j6-yBM">
<objects>
<viewController id="lto-Zc-Vtp" customClass="TabBarContentViewController" customModule="Samples" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="ji9-Ez-N7i">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Item 1" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="uoW-c8-9wx">
<rect key="frame" x="164.5" y="323" width="46" height="21"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="eFN-tN-4Ct">
<rect key="frame" x="20" y="20" width="39" height="30"/>
<rect key="frame" x="20" y="44" width="39" height="30"/>
<state key="normal" title="Close"/>
<connections>
<action selector="closeWithSender:" destination="bYI-y3-Rzb" eventType="touchUpInside" id="YL4-GP-ZEZ"/>
@@ -139,18 +243,47 @@
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="eFN-tN-4Ct" firstAttribute="leading" secondItem="f88-U8-Vja" secondAttribute="leading" constant="20" id="5BT-yZ-EKe"/>
<constraint firstItem="uoW-c8-9wx" firstAttribute="centerY" secondItem="ji9-Ez-N7i" secondAttribute="centerY" id="Nyw-Wt-78z"/>
<constraint firstItem="eFN-tN-4Ct" firstAttribute="top" secondItem="f88-U8-Vja" secondAttribute="top" id="hUV-3a-XkY"/>
<constraint firstItem="uoW-c8-9wx" firstAttribute="centerX" secondItem="ji9-Ez-N7i" secondAttribute="centerX" id="wDv-OH-7PX"/>
<constraint firstItem="eFN-tN-4Ct" firstAttribute="leading" secondItem="5Ns-4l-Ufg" secondAttribute="leading" constant="20" id="5BT-yZ-EKe"/>
<constraint firstItem="5Ns-4l-Ufg" firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="eFN-tN-4Ct" secondAttribute="trailing" id="OzZ-Dz-RNF"/>
<constraint firstItem="eFN-tN-4Ct" firstAttribute="top" secondItem="5Ns-4l-Ufg" secondAttribute="top" id="hUV-3a-XkY"/>
</constraints>
<viewLayoutGuide key="safeArea" id="f88-U8-Vja"/>
<viewLayoutGuide key="safeArea" id="5Ns-4l-Ufg"/>
</view>
<tabBarItem key="tabBarItem" title="Item 1" id="HEV-kf-jxH"/>
<tabBarItem key="tabBarItem" title="Layout 1" id="HEV-kf-jxH"/>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="bkL-bc-hZC" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-380" y="1576"/>
<point key="canvasLocation" x="-962" y="1546"/>
</scene>
<!--Intrinsic View Controller-->
<scene sceneID="wtJ-qZ-aCl">
<objects>
<viewController storyboardIdentifier="IntrinsicViewController" title="Intrinsic View Controller" id="aK0-kv-mTu" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="eLM-xc-d9e">
<rect key="frame" x="0.0" y="0.0" width="375" height="778"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="1000" verticalHuggingPriority="1000" horizontalCompressionResistancePriority="1000" verticalCompressionResistancePriority="1000" text="Change this text" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ge4-RW-Gmz">
<rect key="frame" x="24" y="24" width="327" height="20.333333333333329"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="trailing" secondItem="ge4-RW-Gmz" secondAttribute="trailing" constant="24" id="V59-MD-Lcg"/>
<constraint firstItem="ge4-RW-Gmz" firstAttribute="leading" secondItem="eLM-xc-d9e" secondAttribute="leading" constant="24" id="hAO-P0-7Kw"/>
<constraint firstItem="ge4-RW-Gmz" firstAttribute="top" secondItem="eLM-xc-d9e" secondAttribute="top" constant="24" id="j0s-fd-MYj"/>
<constraint firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="ge4-RW-Gmz" secondAttribute="bottom" constant="24" id="tEn-PO-nVD"/>
</constraints>
<viewLayoutGuide key="safeArea" id="ouu-g9-OiX"/>
</view>
<size key="freeformSize" width="375" height="778"/>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="DfE-fL-zy5" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="2753" y="734"/>
</scene>
<!--Tab Bar View Controller-->
<scene sceneID="nQ5-PV-qFw">
@@ -164,33 +297,34 @@
<connections>
<segue destination="lto-Zc-Vtp" kind="relationship" relationship="viewControllers" id="6hP-AH-YiH"/>
<segue destination="RpE-lI-27a" kind="relationship" relationship="viewControllers" id="g6X-Sq-uSW"/>
<segue destination="pOk-Zm-vD9" kind="relationship" relationship="viewControllers" id="OPp-iO-iDK"/>
</connections>
</tabBarController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Z9x-EI-p2b" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-183" y="806"/>
<point key="canvasLocation" x="-706" y="749"/>
</scene>
<!--Modal View Controller-->
<scene sceneID="C9P-Ns-Qrq">
<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="667"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="778"/>
<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="667" width="375" height="0.0"/>
<rect key="frame" x="0.0" y="744" width="375" height="34"/>
<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="20" width="39" height="30"/>
<rect key="frame" x="20" y="44" 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.5" y="108" width="96" height="252"/>
<rect key="frame" x="139.66666666666666" y="132" width="96" height="252"/>
<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"/>
@@ -225,35 +359,36 @@
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="sbF-Az-7sy" firstAttribute="top" secondItem="GBa-yx-8to" secondAttribute="top" id="3VR-hj-zeQ"/>
<constraint firstItem="9p4-06-y2T" firstAttribute="top" secondItem="GBa-yx-8to" secondAttribute="top" constant="88" id="41n-Fn-hi3"/>
<constraint firstItem="sbF-Az-7sy" firstAttribute="top" secondItem="kjr-TP-fcM" secondAttribute="top" id="3VR-hj-zeQ"/>
<constraint firstItem="9p4-06-y2T" firstAttribute="top" secondItem="kjr-TP-fcM" secondAttribute="top" constant="88" id="41n-Fn-hi3"/>
<constraint firstAttribute="bottom" secondItem="vut-mK-Y4t" secondAttribute="bottom" id="6eq-Kt-heZ"/>
<constraint firstItem="sbF-Az-7sy" firstAttribute="leading" secondItem="GBa-yx-8to" secondAttribute="leading" constant="20" id="T2G-1L-PRs"/>
<constraint firstItem="vut-mK-Y4t" firstAttribute="leading" secondItem="qwo-GK-p1U" secondAttribute="leading" id="gVC-jv-VJX"/>
<constraint firstItem="vut-mK-Y4t" firstAttribute="trailing" secondItem="GBa-yx-8to" secondAttribute="trailing" id="jkq-p2-lUm"/>
<constraint firstItem="9p4-06-y2T" firstAttribute="centerX" secondItem="qwo-GK-p1U" secondAttribute="centerX" id="l8t-p3-ETf"/>
<constraint firstItem="vut-mK-Y4t" firstAttribute="top" secondItem="GBa-yx-8to" secondAttribute="bottom" id="rMy-JT-t4B"/>
<constraint firstItem="sbF-Az-7sy" firstAttribute="leading" secondItem="kjr-TP-fcM" secondAttribute="leading" constant="20" id="T2G-1L-PRs"/>
<constraint firstItem="vut-mK-Y4t" firstAttribute="leading" secondItem="kjr-TP-fcM" secondAttribute="leading" id="gVC-jv-VJX"/>
<constraint firstItem="vut-mK-Y4t" firstAttribute="trailing" secondItem="kjr-TP-fcM" secondAttribute="trailing" id="jkq-p2-lUm"/>
<constraint firstItem="9p4-06-y2T" firstAttribute="centerX" secondItem="kjr-TP-fcM" secondAttribute="centerX" id="l8t-p3-ETf"/>
<constraint firstItem="vut-mK-Y4t" firstAttribute="top" secondItem="kjr-TP-fcM" secondAttribute="bottom" id="rMy-JT-t4B"/>
</constraints>
<viewLayoutGuide key="safeArea" id="GBa-yx-8to"/>
<viewLayoutGuide key="safeArea" id="kjr-TP-fcM"/>
</view>
<size key="freeformSize" width="375" height="778"/>
<connections>
<outlet property="safeAreaView" destination="vut-mK-Y4t" id="r9P-XF-wLd"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="fbi-LZ-M4Y" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="561" y="806"/>
<point key="canvasLocation" x="1375" y="734"/>
</scene>
<!--Nested Scroll View Controller-->
<scene sceneID="TfC-A3-4R0">
<objects>
<viewController storyboardIdentifier="NestedScrollViewController" id="LAe-jm-k6f" customClass="NestedScrollViewController" customModule="Samples" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="414-Wy-0t1">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="778"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="sBe-tN-uMi">
<rect key="frame" x="0.0" y="32" width="375" height="635"/>
<rect key="frame" x="0.0" y="32" width="375" height="746"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="lFR-Sp-Sj1">
<rect key="frame" x="0.0" y="0.0" width="375" height="968"/>
@@ -329,20 +464,21 @@
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<gestureRecognizers/>
<constraints>
<constraint firstItem="sBe-tN-uMi" firstAttribute="leading" secondItem="414-Wy-0t1" secondAttribute="leading" id="8Qd-my-knA"/>
<constraint firstItem="sBe-tN-uMi" firstAttribute="leading" secondItem="ufS-Rf-F2F" secondAttribute="leading" id="8Qd-my-knA"/>
<constraint firstItem="sBe-tN-uMi" firstAttribute="top" secondItem="414-Wy-0t1" secondAttribute="top" constant="32" id="9Js-LU-lNr"/>
<constraint firstAttribute="bottom" secondItem="sBe-tN-uMi" secondAttribute="bottom" id="jzB-47-P7e"/>
<constraint firstAttribute="trailing" secondItem="sBe-tN-uMi" secondAttribute="trailing" id="nHG-wg-pLP"/>
<constraint firstItem="ufS-Rf-F2F" firstAttribute="trailing" secondItem="sBe-tN-uMi" secondAttribute="trailing" id="nHG-wg-pLP"/>
</constraints>
<viewLayoutGuide key="safeArea" id="sL5-d5-za2"/>
<viewLayoutGuide key="safeArea" id="ufS-Rf-F2F"/>
<connections>
<outletCollection property="gestureRecognizers" destination="tOa-bf-zGz" appends="YES" id="zle-Sz-M3U"/>
<outletCollection property="gestureRecognizers" destination="SCk-hG-weZ" appends="YES" id="OcK-FK-Lac"/>
<outletCollection property="gestureRecognizers" destination="Fvp-Z6-eVc" appends="YES" id="Fds-J5-YCg"/>
</connections>
</view>
<size key="freeformSize" width="375" height="667"/>
<size key="freeformSize" width="375" height="778"/>
<connections>
<outlet property="nestedScrollView" destination="xba-kG-VQ2" id="ddV-kf-37A"/>
<outlet property="scrollView" destination="sBe-tN-uMi" id="h4S-Zl-cLO"/>
</connections>
</viewController>
@@ -363,7 +499,7 @@
</connections>
</swipeGestureRecognizer>
</objects>
<point key="canvasLocation" x="1311" y="806"/>
<point key="canvasLocation" x="2097" y="734"/>
</scene>
<!--Detail View Controller-->
<scene sceneID="b6k-zi-3wn">
@@ -373,8 +509,19 @@
<rect key="frame" x="0.0" y="0.0" width="375" height="778"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="8yw-OC-Ubk">
<rect key="frame" x="0.0" y="690" width="375" height="88"/>
<color key="backgroundColor" white="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="height" constant="88" id="jwV-YU-tXG"/>
</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"/>
<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">
<rect key="frame" x="319" y="12" width="44" height="44"/>
<rect key="frame" x="319" y="44" width="44" height="44"/>
<constraints>
<constraint firstAttribute="height" constant="44" id="0jg-5D-A1F"/>
<constraint firstAttribute="width" constant="44" id="1Cq-PA-wgW"/>
@@ -383,22 +530,8 @@
<action selector="closeWithSender:" destination="YC8-ae-15L" eventType="touchUpInside" id="Z2v-19-S5k"/>
</connections>
</button>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="8yw-OC-Ubk">
<rect key="frame" x="0.0" y="690" width="375" height="88"/>
<color key="backgroundColor" white="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="height" constant="88" id="jwV-YU-tXG"/>
</constraints>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Kva-Z7-0qY" customClass="OnSafeAreaView" customModule="Samples" customModuleProvider="target">
<rect key="frame" x="0.0" y="734" width="375" height="44"/>
<color key="backgroundColor" red="0.0078431372550000003" green="0.72156862749999995" blue="0.45882352939999999" alpha="1" colorSpace="calibratedRGB"/>
<constraints>
<constraint firstAttribute="height" constant="44" id="DQJ-cY-cKx"/>
</constraints>
</view>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="22" translatesAutoresizingMaskIntoConstraints="NO" id="tP3-oJ-4EB">
<rect key="frame" x="130.5" y="108" width="114" height="82"/>
<rect key="frame" x="130.66666666666666" y="132" width="114" height="134"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="c5r-jU-haj">
<rect key="frame" x="0.0" y="0.0" width="114" height="30"/>
@@ -415,24 +548,33 @@
<segue destination="bYI-y3-Rzb" kind="presentation" identifier="PresentModallySegue" id="3yq-HE-Tgn"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="01L-lp-oy6">
<rect key="frame" x="0.0" y="104" width="114" height="30"/>
<state key="normal" title="Update Layout"/>
<connections>
<action selector="buttonPressed:" destination="YC8-ae-15L" eventType="touchUpInside" id="zTb-sq-B6f"/>
</connections>
</button>
</subviews>
</stackView>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<color key="backgroundColor" red="1" green="0.83234566450000003" blue="0.47320586440000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<gestureRecognizers/>
<constraints>
<constraint firstItem="noi-1a-5bZ" firstAttribute="top" secondItem="g7l-kO-y7q" secondAttribute="top" constant="12" id="EQy-cr-F2Y"/>
<constraint firstItem="tP3-oJ-4EB" firstAttribute="centerX" secondItem="g7l-kO-y7q" secondAttribute="centerX" id="EsD-Vf-dNZ"/>
<constraint firstItem="noi-1a-5bZ" firstAttribute="top" secondItem="aOK-7l-cA6" secondAttribute="top" id="EQy-cr-F2Y"/>
<constraint firstItem="tP3-oJ-4EB" firstAttribute="centerX" secondItem="aOK-7l-cA6" secondAttribute="centerX" id="EsD-Vf-dNZ"/>
<constraint firstItem="Kva-Z7-0qY" firstAttribute="top" secondItem="aOK-7l-cA6" secondAttribute="top" id="Fff-HL-4mo"/>
<constraint firstItem="8yw-OC-Ubk" firstAttribute="bottom" secondItem="g7l-kO-y7q" secondAttribute="bottom" id="JOL-wC-w74"/>
<constraint firstItem="8yw-OC-Ubk" firstAttribute="leading" secondItem="tAi-nk-rDB" secondAttribute="leading" id="RiJ-Hb-OOZ"/>
<constraint firstItem="8yw-OC-Ubk" firstAttribute="trailing" secondItem="tAi-nk-rDB" secondAttribute="trailing" id="Sof-yL-mwK"/>
<constraint firstItem="tP3-oJ-4EB" firstAttribute="top" secondItem="tAi-nk-rDB" secondAttribute="top" constant="88" id="Zhb-Ss-epe"/>
<constraint firstItem="Kva-Z7-0qY" firstAttribute="trailing" secondItem="tAi-nk-rDB" secondAttribute="trailing" id="kkp-Yo-FQW"/>
<constraint firstItem="tAi-nk-rDB" firstAttribute="trailing" secondItem="noi-1a-5bZ" secondAttribute="trailing" constant="12" id="lv9-Nf-HNB"/>
<constraint firstItem="Kva-Z7-0qY" firstAttribute="leading" secondItem="tAi-nk-rDB" secondAttribute="leading" id="oVC-i1-TwS"/>
<constraint firstItem="tAi-nk-rDB" firstAttribute="bottom" secondItem="Kva-Z7-0qY" secondAttribute="bottom" id="rW2-mF-5DR"/>
<constraint firstItem="8yw-OC-Ubk" firstAttribute="leading" secondItem="aOK-7l-cA6" secondAttribute="leading" id="RiJ-Hb-OOZ"/>
<constraint firstItem="8yw-OC-Ubk" firstAttribute="trailing" secondItem="aOK-7l-cA6" secondAttribute="trailing" id="Sof-yL-mwK"/>
<constraint firstItem="tP3-oJ-4EB" firstAttribute="top" secondItem="aOK-7l-cA6" secondAttribute="top" constant="88" id="Zhb-Ss-epe"/>
<constraint firstItem="Kva-Z7-0qY" firstAttribute="trailing" secondItem="aOK-7l-cA6" secondAttribute="trailing" id="kkp-Yo-FQW"/>
<constraint firstItem="aOK-7l-cA6" firstAttribute="trailing" secondItem="noi-1a-5bZ" secondAttribute="trailing" constant="12" id="lv9-Nf-HNB"/>
<constraint firstItem="Kva-Z7-0qY" firstAttribute="leading" secondItem="aOK-7l-cA6" secondAttribute="leading" id="oVC-i1-TwS"/>
<constraint firstItem="aOK-7l-cA6" firstAttribute="bottom" secondItem="Kva-Z7-0qY" secondAttribute="bottom" id="rW2-mF-5DR"/>
<constraint firstItem="8yw-OC-Ubk" firstAttribute="top" relation="greaterThanOrEqual" secondItem="tP3-oJ-4EB" secondAttribute="bottom" constant="88" id="vKQ-h9-uKt"/>
</constraints>
<viewLayoutGuide key="safeArea" id="tAi-nk-rDB"/>
<viewLayoutGuide key="safeArea" id="aOK-7l-cA6"/>
<connections>
<outletCollection property="gestureRecognizers" destination="6Ca-p8-7uF" appends="YES" id="xOy-f1-NZE"/>
<outletCollection property="gestureRecognizers" destination="SPY-Vr-XDT" appends="YES" id="vgS-Am-jhQ"/>
@@ -462,7 +604,7 @@
</connections>
</pongPressGestureRecognizer>
</objects>
<point key="canvasLocation" x="1440.8" y="-23.388305847076463"/>
<point key="canvasLocation" x="655" y="734"/>
</scene>
<!--Debug Text View Controller-->
<scene sceneID="Bkq-O7-q4A">
@@ -520,24 +662,26 @@ Section 1.10.33 of "de Finibus Bonorum et Malorum", written by Cicero in 45 BC
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="rN1-HL-YHv" firstAttribute="leading" secondItem="ix0-2W-gQN" secondAttribute="leading" id="7V3-KL-vXd"/>
<constraint firstItem="rN1-HL-YHv" firstAttribute="leading" secondItem="5ET-zC-lCb" secondAttribute="leading" id="7V3-KL-vXd"/>
<constraint firstAttribute="bottom" secondItem="rN1-HL-YHv" secondAttribute="bottom" id="efD-U5-Tet"/>
<constraint firstItem="rN1-HL-YHv" firstAttribute="top" secondItem="9YG-0j-Zzg" secondAttribute="top" constant="17" id="fiO-LL-nSC"/>
<constraint firstItem="rN1-HL-YHv" firstAttribute="trailing" secondItem="ix0-2W-gQN" secondAttribute="trailing" id="lfg-EE-euw"/>
<constraint firstItem="rN1-HL-YHv" firstAttribute="trailing" secondItem="5ET-zC-lCb" secondAttribute="trailing" id="lfg-EE-euw"/>
</constraints>
<viewLayoutGuide key="safeArea" id="ix0-2W-gQN"/>
<viewLayoutGuide key="safeArea" id="5ET-zC-lCb"/>
</view>
<size key="freeformSize" width="375" height="778"/>
<connections>
<outlet property="textView" destination="rN1-HL-YHv" id="gmr-Uf-jd8"/>
<outlet property="textViewTopConstraint" destination="fiO-LL-nSC" id="Rum-TN-c2e"/>
<outlet property="view" destination="9YG-0j-Zzg" id="jhb-eT-nEn"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="x1h-y1-h8q" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="729" y="-23"/>
<point key="canvasLocation" x="-1" y="734"/>
</scene>
</scenes>
<inferredMetricsTieBreakers>
<segue reference="3yq-HE-Tgn"/>
<segue reference="r1P-2i-NDe"/>
</inferredMetricsTieBreakers>
</document>
+32 -14
View File
@@ -5,21 +5,39 @@
import UIKit
extension UIView {
var layoutInsets: UIEdgeInsets {
if #available(iOS 11.0, *) {
return safeAreaInsets
} else {
return layoutMargins
}
}
protocol LayoutGuideProvider {
var topAnchor: NSLayoutYAxisAnchor { get }
var bottomAnchor: NSLayoutYAxisAnchor { get }
}
extension UILayoutGuide: LayoutGuideProvider {}
var layoutGuide: UILayoutGuide {
if #available(iOS 11.0, *) {
return safeAreaLayoutGuide
} else {
return layoutMarginsGuide
}
class CustomLayoutGuide: LayoutGuideProvider {
let topAnchor: NSLayoutYAxisAnchor
let bottomAnchor: NSLayoutYAxisAnchor
init(topAnchor: NSLayoutYAxisAnchor, bottomAnchor: NSLayoutYAxisAnchor) {
self.topAnchor = topAnchor
self.bottomAnchor = bottomAnchor
}
}
extension UIViewController {
var layoutInsets: UIEdgeInsets {
if #available(iOS 11.0, *) {
return view.safeAreaInsets
} else {
return UIEdgeInsets(top: topLayoutGuide.length,
left: 0.0,
bottom: bottomLayoutGuide.length,
right: 0.0)
}
}
var layoutGuide: LayoutGuideProvider {
if #available(iOS 11.0, *) {
return view!.safeAreaLayoutGuide
} else {
return CustomLayoutGuide(topAnchor: topLayoutGuide.bottomAnchor,
bottomAnchor: bottomLayoutGuide.topAnchor)
}
}
}
+400 -94
View File
@@ -17,9 +17,11 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable
case trackingTextView
case showDetail
case showModal
case showFloatingPanelModal
case showTabBar
case showNestedScrollView
case showRemovablePanel
case showIntrinsicView
var name: String {
switch self {
@@ -27,9 +29,11 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable
case .trackingTextView: return "Scroll tracking(TextView)"
case .showDetail: return "Show Detail Panel"
case .showModal: return "Show Modal"
case .showFloatingPanelModal: return "Show Floating Panel Modal"
case .showTabBar: return "Show Tab Bar"
case .showNestedScrollView: return "Show Nested ScrollView"
case .showRemovablePanel: return "Show Removable Panel"
case .showIntrinsicView: return "Show Intrinsic View"
}
}
@@ -39,16 +43,23 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable
case .trackingTextView: return "ConsoleViewController"
case .showDetail: return "DetailViewController"
case .showModal: return "ModalViewController"
case .showFloatingPanelModal: return nil
case .showTabBar: return "TabBarViewController"
case .showNestedScrollView: return "NestedScrollViewController"
case .showRemovablePanel: return "DetailViewController"
case .showIntrinsicView: return "IntrinsicViewController"
}
}
}
var currentMenu: Menu = .trackingTableView
var mainPanelVC: FloatingPanelController!
var detailPanelVC: FloatingPanelController!
var currentMenu: Menu = .trackingTableView
var settingsPanelVC: FloatingPanelController!
var mainPanelObserves: [NSKeyValueObservation] = []
var settingsObserves: [NSKeyValueObservation] = []
override func viewDidLoad() {
super.viewDidLoad()
@@ -56,19 +67,42 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable
tableView.delegate = self
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
let searchController = UISearchController(searchResultsController: nil)
if #available(iOS 11.0, *) {
navigationItem.searchController = searchController
navigationItem.hidesSearchBarWhenScrolling = false
navigationItem.largeTitleDisplayMode = .automatic
} else {
// Fallback on earlier versions
}
let contentVC = DebugTableViewController()
addMainPanel(with: contentVC)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if #available(iOS 11.0, *) {
if let observation = navigationController?.navigationBar.observe(\.prefersLargeTitles, changeHandler: { (bar, _) in
self.tableView.reloadData()
}) {
settingsObserves.append(observation)
}
}
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
settingsObserves.removeAll()
}
func addMainPanel(with contentVC: UIViewController) {
mainPanelObserves.removeAll()
// Initialize FloatingPanelController
mainPanelVC = FloatingPanelController()
mainPanelVC.delegate = self
mainPanelVC.isRemovalInteractionEnabled = (currentMenu == .showRemovablePanel)
// Initialize FloatingPanelController and add the view
mainPanelVC.surfaceView.cornerRadius = 6.0
@@ -77,18 +111,34 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable
// Set a content view controller
mainPanelVC.set(contentViewController: contentVC)
// Enable tap-to-hide and removal interaction
switch currentMenu {
case .showRemovablePanel, .showIntrinsicView:
mainPanelVC.isRemovalInteractionEnabled = true
let backdropTapGesture = UITapGestureRecognizer(target: self, action: #selector(handleBackdrop(tapGesture:)))
mainPanelVC.backdropView.addGestureRecognizer(backdropTapGesture)
default:
break
}
// Track a scroll view
switch contentVC {
case let consoleVC as DebugTextViewController:
mainPanelVC.track(scrollView: consoleVC.textView)
case let contentVC as DebugTableViewController:
let ob = contentVC.tableView.observe(\.isEditing) { (tableView, _) in
self.mainPanelVC.panGestureRecognizer.isEnabled = !tableView.isEditing
}
mainPanelObserves.append(ob)
mainPanelVC.track(scrollView: contentVC.tableView)
case let contentVC as NestedScrollViewController:
mainPanelVC.track(scrollView: contentVC.scrollView)
default:
break
}
// Add FloatingPanel to self.view
mainPanelVC.addPanel(toParent: self, belowView: nil, animated: true)
}
@@ -97,22 +147,72 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable
detailPanelVC.removePanelFromParent(animated: true, completion: nil)
}
@objc func handleBackdrop(tapGesture: UITapGestureRecognizer) {
switch tapGesture.view {
case mainPanelVC.backdropView:
mainPanelVC.hide(animated: true, completion: nil)
case settingsPanelVC.backdropView:
settingsPanelVC.removePanelFromParent(animated: true)
settingsPanelVC = nil
default:
break
}
}
// MARK:- TableViewDatasource
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return Menu.allCases.count
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)
let menu = Menu.allCases[indexPath.row]
cell.textLabel?.text = menu.name
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 }
// Initialize FloatingPanelController
settingsPanelVC = FloatingPanelController()
// Initialize FloatingPanelController and add the view
settingsPanelVC.surfaceView.cornerRadius = 6.0
settingsPanelVC.surfaceView.shadowHidden = false
settingsPanelVC.isRemovalInteractionEnabled = true
let backdropTapGesture = UITapGestureRecognizer(target: self, action: #selector(handleBackdrop(tapGesture:)))
settingsPanelVC.backdropView.addGestureRecognizer(backdropTapGesture)
settingsPanelVC.delegate = self
let contentVC = storyboard?.instantiateViewController(withIdentifier: "SettingsViewController")
// Set a content view controller
settingsPanelVC.set(contentViewController: contentVC)
// Add FloatingPanel to self.view
settingsPanelVC.addPanel(toParent: self, belowView: nil, animated: true)
}
// MARK:- TableViewDelegate
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard Menu.allCases.count > indexPath.row else { return }
let menu = Menu.allCases[indexPath.row]
let contentVC: UIViewController = {
guard let storyboardID = menu.storyboardID else { return DebugTableViewController() }
@@ -124,7 +224,7 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable
switch menu {
case .showDetail:
detailPanelVC?.removeFromParent()
detailPanelVC?.removePanelFromParent(animated: false)
// Initialize FloatingPanelController
detailPanelVC = FloatingPanelController()
@@ -141,6 +241,18 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable
case .showModal, .showTabBar:
let modalVC = contentVC
present(modalVC, animated: true, completion: nil)
case .showFloatingPanelModal:
let fpc = FloatingPanelController()
let contentVC = self.storyboard!.instantiateViewController(withIdentifier: "DetailViewController")
fpc.set(contentViewController: contentVC)
fpc.delegate = self
fpc.surfaceView.cornerRadius = 38.5
fpc.surfaceView.shadowHidden = false
fpc.isRemovalInteractionEnabled = true
self.present(fpc, animated: true, completion: nil)
default:
detailPanelVC?.removePanelFromParent(animated: true, completion: nil)
mainPanelVC?.removePanelFromParent(animated: true) {
@@ -150,10 +262,40 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable
}
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? {
if currentMenu == .showRemovablePanel {
if vc == settingsPanelVC {
return IntrinsicPanelLayout()
}
switch currentMenu {
case .showRemovablePanel:
return newCollection.verticalSizeClass == .compact ? RemovablePanelLandscapeLayout() : RemovablePanelLayout()
} else {
return self
case .showIntrinsicView:
return IntrinsicPanelLayout()
case .showFloatingPanelModal:
if vc != mainPanelVC && vc != detailPanelVC {
return ModalPanelLayout()
}
fallthrough
default:
return (newCollection.verticalSizeClass == .compact) ? nil : self
}
}
func floatingPanel(_ vc: FloatingPanelController, shouldRecognizeSimultaneouslyWith gestureRecognizer: UIGestureRecognizer) -> Bool {
switch currentMenu {
case .showNestedScrollView:
return (vc.contentViewController as? NestedScrollViewController)?.nestedScrollView.gestureRecognizers?.contains(gestureRecognizer) ?? false
default:
return false
}
}
func floatingPanelDidEndRemove(_ vc: FloatingPanelController) {
switch vc {
case settingsPanelVC:
settingsPanelVC = nil
default:
break
}
}
@@ -166,16 +308,22 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable
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
}
}
}
class RemovablePanelLayout: FloatingPanelLayout {
class IntrinsicPanelLayout: FloatingPanelIntrinsicLayout { }
class RemovablePanelLayout: FloatingPanelIntrinsicLayout {
var supportedPositions: Set<FloatingPanelPosition> {
return [.full, .half]
}
var initialPosition: FloatingPanelPosition {
return .half
}
var supportedPositions: Set<FloatingPanelPosition> {
return [.full, .half]
var topInteractionBuffer: CGFloat {
return 200.0
}
var bottomInteractionBuffer: CGFloat {
return 261.0 - 22.0
@@ -183,7 +331,24 @@ class RemovablePanelLayout: FloatingPanelLayout {
func insetFor(position: FloatingPanelPosition) -> CGFloat? {
switch position {
case .full: return 16.0
case .half: return 130.0
default: return nil
}
}
func backdropAlphaFor(position: FloatingPanelPosition) -> CGFloat {
return 0.3
}
}
class RemovablePanelLandscapeLayout: FloatingPanelIntrinsicLayout {
var supportedPositions: Set<FloatingPanelPosition> {
return [.full, .half]
}
var bottomInteractionBuffer: CGFloat {
return 261.0 - 22.0
}
func insetFor(position: FloatingPanelPosition) -> CGFloat? {
switch position {
case .half: return 261.0
default: return nil
}
@@ -193,22 +358,9 @@ class RemovablePanelLayout: FloatingPanelLayout {
}
}
class RemovablePanelLandscapeLayout: FloatingPanelLayout {
var initialPosition: FloatingPanelPosition {
return .half
}
var supportedPositions: Set<FloatingPanelPosition> {
return [.half]
}
var bottomInteractionBuffer: CGFloat {
return 261.0 - 22.0
}
func insetFor(position: FloatingPanelPosition) -> CGFloat? {
switch position {
case .half: return 261.0
default: return nil
}
class ModalPanelLayout: FloatingPanelIntrinsicLayout {
var topInteractionBuffer: CGFloat {
return 100.0
}
func backdropAlphaFor(position: FloatingPanelPosition) -> CGFloat {
return 0.3
@@ -217,6 +369,7 @@ class RemovablePanelLandscapeLayout: FloatingPanelLayout {
class NestedScrollViewController: UIViewController {
@IBOutlet weak var scrollView: UIScrollView!
@IBOutlet weak var nestedScrollView: UIScrollView!
@IBAction func longPressed(_ sender: Any) {
print("LongPressed!")
@@ -231,16 +384,32 @@ class NestedScrollViewController: UIViewController {
class DebugTextViewController: UIViewController, UITextViewDelegate {
@IBOutlet weak var textView: UITextView!
@IBOutlet weak var textViewTopConstraint: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
textView.delegate = self
print("viewDidLoad: TextView --- ", textView.contentOffset, textView.contentInset)
if #available(iOS 11.0, *) {
textView.contentInsetAdjustmentBehavior = .never
}
}
override func viewWillLayoutSubviews() {
print("viewWillLayoutSubviews: TextView --- ", textView.contentOffset, textView.contentInset, textView.frame)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
print("viewDidLayoutSubviews: TextView --- ", textView.contentOffset, textView.contentInset, textView.frame)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
print("TextView --- ", textView.contentOffset, textView.contentInset, textView.frame)
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
print("TextView --- ", scrollView.contentOffset, scrollView.contentInset)
if #available(iOS 11.0, *) {
@@ -249,16 +418,68 @@ class DebugTextViewController: UIViewController, UITextViewDelegate {
}
@IBAction func close(sender: UIButton) {
// Now impossible
// dismiss(animated: true, completion: nil)
(self.parent as? FloatingPanelController)?.removePanelFromParent(animated: true, completion: nil)
// (self.parent as? FloatingPanelController)?.removePanelFromParent(animated: true, completion: nil)
dismiss(animated: true, completion: nil)
}
}
class DebugTableViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
class InspectableViewController: UIViewController {
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
print(">>> Content View: viewWillLayoutSubviews", layoutInsets)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
print(">>> Content View: viewDidLayoutSubviews", layoutInsets)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
print(">>> Content View: viewWillAppear", layoutInsets)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
print(">>> Content View: viewDidAppear", view.bounds, layoutInsets)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
print(">>> Content View: viewWillDisappear")
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
print(">>> Content View: viewDidDisappear")
}
override func willMove(toParent parent: UIViewController?) {
super.willMove(toParent: parent)
print(">>> Content View: willMove(toParent: \(String(describing: parent))")
}
override func didMove(toParent parent: UIViewController?) {
super.didMove(toParent: parent)
print(">>> Content View: didMove(toParent: \(String(describing: parent))")
}
public override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) {
print(">>> Content View: willTransition(to: \(newCollection), with: \(coordinator))", layoutInsets)
}
}
class DebugTableViewController: InspectableViewController, UITableViewDataSource, UITableViewDelegate {
weak var tableView: UITableView!
var items: [String] = []
var itemHeight: CGFloat = 66.0
enum Menu: String, CaseIterable {
case animateScroll = "Animate Scroll"
case changeContentSize = "Change content size"
case reorder = "Reorder"
}
var reorderButton: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
@@ -288,17 +509,21 @@ class DebugTableViewController: UIViewController, UITableViewDataSource, UITable
stackView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -22.0),
])
let button = UIButton()
button.setTitle("Animate Scroll", for: .normal)
button.setTitleColor(view.tintColor, for: .normal)
button.addTarget(self, action: #selector(animateScroll), for: .touchUpInside)
stackView.addArrangedSubview(button)
let button2 = UIButton()
button2.setTitle("Change content size", for: .normal)
button2.setTitleColor(view.tintColor, for: .normal)
button2.addTarget(self, action: #selector(changeContentSize), for: .touchUpInside)
stackView.addArrangedSubview(button2)
for menu in Menu.allCases {
let button = UIButton()
button.setTitle(menu.rawValue, for: .normal)
button.setTitleColor(view.tintColor, for: .normal)
switch menu {
case .animateScroll:
button.addTarget(self, action: #selector(animateScroll), for: .touchUpInside)
case .changeContentSize:
button.addTarget(self, action: #selector(changeContentSize), for: .touchUpInside)
case .reorder:
button.addTarget(self, action: #selector(reorderItems), for: .touchUpInside)
reorderButton = button
}
stackView.addArrangedSubview(button)
}
for i in 0...100 {
items.append("Items \(i)")
@@ -339,6 +564,16 @@ class DebugTableViewController: UIViewController, UITableViewDataSource, UITable
self.present(actionSheet, animated: true, completion: nil)
}
@objc func reorderItems() {
if reorderButton.titleLabel?.text == Menu.reorder.rawValue {
tableView.isEditing = true
reorderButton.setTitle("Cancel", for: .normal)
} else {
tableView.isEditing = false
reorderButton.setTitle(Menu.reorder.rawValue, for: .normal)
}
}
func changeItems(_ count: Int) {
items.removeAll()
for i in 0..<count {
@@ -352,50 +587,6 @@ class DebugTableViewController: UIViewController, UITableViewDataSource, UITable
(self.parent as! FloatingPanelController).removePanelFromParent(animated: true, completion: nil)
}
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
//print("Content View: viewWillLayoutSubviews")
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
//print("Content View: viewDidLayoutSubviews")
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
print("Content View: viewWillAppear")
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
print("Content View: viewDidAppear", view.bounds)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
print("Content View: viewWillDisappear")
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
print("Content View: viewDidDisappear")
}
override func willMove(toParent parent: UIViewController?) {
super.willMove(toParent: parent)
print("Content View: willMove(toParent: \(String(describing: parent))")
}
override func didMove(toParent parent: UIViewController?) {
super.didMove(toParent: parent)
print("Content View: didMove(toParent: \(String(describing: parent))")
}
public override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) {
print("Content View: willTransition(to: \(newCollection), with: \(coordinator))")
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
@@ -409,14 +600,30 @@ class DebugTableViewController: UIViewController, UITableViewDataSource, UITable
cell.textLabel?.text = items[indexPath.row]
return cell
}
func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
return [
UITableViewRowAction(style: .destructive, title: "Delete", handler: { (action, path) in
self.items.remove(at: path.row)
tableView.deleteRows(at: [path], with: .automatic)
}),
]
}
func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
return true
}
func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
items.insert(items.remove(at: sourceIndexPath.row), at: destinationIndexPath.row)
}
}
class DetailViewController: UIViewController {
class DetailViewController: InspectableViewController {
@IBOutlet weak var closeButton: UIButton!
@IBAction func close(sender: UIButton) {
// Now impossible
// dismiss(animated: true, completion: nil)
(self.parent as? FloatingPanelController)?.removePanelFromParent(animated: true, completion: nil)
// (self.parent as? FloatingPanelController)?.removePanelFromParent(animated: true, completion: nil)
dismiss(animated: true, completion: nil)
}
@IBAction func buttonPressed(_ sender: UIButton) {
@@ -512,6 +719,7 @@ class ModalSecondLayout: FloatingPanelLayout {
case .full: return 18.0
case .half: return 262.0
case .tip: return 44.0
case .hidden: return nil
}
}
}
@@ -553,12 +761,60 @@ class TabBarContentViewController: UIViewController, FloatingPanelControllerDele
case 0:
return OneTabBarPanelLayout()
case 1:
return TwoTabBarPanel2Layout()
return TwoTabBarPanelLayout()
case 2:
return ThreeTabBarPanelLayout()
default:
return nil
}
}
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
*/
// 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
}
consoleVC.view.layoutIfNeeded()
}
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)
}
*/
// 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)
}
@@ -597,7 +853,7 @@ class OneTabBarPanelLayout: FloatingPanelLayout {
}
}
class TwoTabBarPanel2Layout: FloatingPanelLayout {
class TwoTabBarPanelLayout: FloatingPanelLayout {
var initialPosition: FloatingPanelPosition {
return .half
}
@@ -616,3 +872,53 @@ class TwoTabBarPanel2Layout: FloatingPanelLayout {
}
}
}
class ThreeTabBarPanelLayout: FloatingPanelFullScreenLayout {
var initialPosition: FloatingPanelPosition {
return .half
}
var supportedPositions: Set<FloatingPanelPosition> {
return [.full, .half]
}
func insetFor(position: FloatingPanelPosition) -> CGFloat? {
switch position {
case .full: return 0.0
case .half: return 261.0
default: return nil
}
}
func backdropAlphaFor(position: FloatingPanelPosition) -> CGFloat {
return 0.3
}
}
class SettingsViewController: InspectableViewController {
@IBOutlet weak var largeTitlesSwicth: UISwitch!
@IBOutlet weak var translucentSwicth: UISwitch!
@IBOutlet weak var versionLabel: UILabel!
override func viewDidLoad() {
versionLabel.text = "Version: \(Bundle.main.infoDictionary?["CFBundleVersion"] ?? "--")"
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if #available(iOS 11.0, *) {
let prefersLargeTitles = navigationController!.navigationBar.prefersLargeTitles
largeTitlesSwicth.setOn(prefersLargeTitles, animated: false)
} else {
largeTitlesSwicth.isEnabled = false
}
let isTranslucent = navigationController!.navigationBar.isTranslucent
translucentSwicth.setOn(isTranslucent, animated: false)
}
@IBAction func toggleLargeTitle(_ sender: UISwitch) {
if #available(iOS 11.0, *) {
navigationController?.navigationBar.prefersLargeTitles = sender.isOn
}
}
@IBAction func toggleTranslucent(_ sender: UISwitch) {
navigationController?.navigationBar.isTranslucent = sender.isOn
}
}
@@ -114,6 +114,7 @@ class FloatingPanelStocksLayout: FloatingPanelLayout {
case .full: return 56.0
case .half: return 262.0
case .tip: return 85.0 + 44.0 // Visible + ToolView
default: return nil
}
}
+2 -2
View File
@@ -1,7 +1,7 @@
Pod::Spec.new do |s|
s.name = "FloatingPanel"
s.version = "1.2.0"
s.version = "1.3.5"
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.
@@ -13,7 +13,7 @@ The new interface displays the related contents and utilities in parallel as a u
s.platform = :ios, "10.0"
s.source = { :git => "https://github.com/SCENEE/FloatingPanel.git", :tag => "v#{s.version}" }
s.source_files = "Framework/Sources/*.swift"
s.swift_version = "4.2"
s.swift_version = "4.0"
s.pod_target_xcconfig = { 'SWIFT_WHOLE_MODULE_OPTIMIZATION' => 'YES', 'APPLICATION_EXTENSION_API_ONLY' => 'YES' }
s.framework = "UIKit"
@@ -7,6 +7,8 @@
objects = {
/* Begin PBXBuildFile section */
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 */; };
@@ -32,6 +34,8 @@
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
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>"; };
@@ -92,9 +96,11 @@
545DB9C52151169500CA77B8 /* Info.plist */,
545DB9C42151169500CA77B8 /* FloatingPanelController.h */,
545DB9DF21511AC100CA77B8 /* FloatingPanelController.swift */,
54352E9521A51A2500CBCA08 /* FloatingPanelTransitioning.swift */,
54CFBFC4215CD09C006B5735 /* FloatingPanel.swift */,
54CFBFC2215CD045006B5735 /* FloatingPanelLayout.swift */,
5450EEE321646DF500135936 /* FloatingPanelBehavior.swift */,
54352E9721A521CA00CBCA08 /* FloatingPanelView.swift */,
54CDC5D2215B6D5A007D205C /* FloatingPanelSurfaceView.swift */,
54CDC5D4215B6D8D007D205C /* FloatingPanelBackdropView.swift */,
545DBA2A2152383100CA77B8 /* GrabberHandleView.swift */,
@@ -225,11 +231,13 @@
54CDC5D3215B6D5A007D205C /* FloatingPanelSurfaceView.swift in Sources */,
54CFBFC3215CD045006B5735 /* FloatingPanelLayout.swift in Sources */,
54CDC5D5215B6D8D007D205C /* FloatingPanelBackdropView.swift in Sources */,
54352E9821A521CA00CBCA08 /* FloatingPanelView.swift in Sources */,
54CFBFC5215CD09C006B5735 /* FloatingPanel.swift in Sources */,
54ABD7AF216CCFF7002E6C13 /* Logger.swift in Sources */,
545DB9E021511AC100CA77B8 /* FloatingPanelController.swift in Sources */,
5450EEE421646DF500135936 /* FloatingPanelBehavior.swift in Sources */,
545DBA2B2152383100CA77B8 /* GrabberHandleView.swift in Sources */,
54352E9621A51A2500CBCA08 /* FloatingPanelTransitioning.swift in Sources */,
545DB9DE215118C800CA77B8 /* UIExtensions.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -398,7 +406,7 @@
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 4.2;
SWIFT_VERSION = 4.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
@@ -424,7 +432,7 @@
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingPanel;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SKIP_INSTALL = YES;
SWIFT_VERSION = 4.2;
SWIFT_VERSION = 4.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
+268 -188
View File
@@ -3,14 +3,15 @@
//
import UIKit
import UIKit.UIGestureRecognizerSubclass // For Xcode 9.4.1
///
/// FloatingPanel presentation model
///
class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate {
/* Cause 'terminating with uncaught exception of type NSException' error on Swift Playground
unowned let view: UIView
*/
// MUST be a weak reference to prevent UI freeze on the presentaion modally
weak var viewcontroller: FloatingPanelController!
let surfaceView: FloatingPanelSurfaceView
let backdropView: FloatingPanelBackdropView
var layoutAdapter: FloatingPanelLayoutAdapter
@@ -26,27 +27,26 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
}
weak var userScrollViewDelegate: UIScrollViewDelegate?
var safeAreaInsets: UIEdgeInsets! {
get { return layoutAdapter.safeAreaInsets }
set { layoutAdapter.safeAreaInsets = newValue }
private(set) var state: FloatingPanelPosition = .hidden {
didSet { viewcontroller.delegate?.floatingPanelDidChangePosition(viewcontroller) }
}
unowned let viewcontroller: FloatingPanelController
private(set) var state: FloatingPanelPosition = .tip
private var isBottomState: Bool {
let remains = layoutAdapter.layout.supportedPositions.filter { $0.rawValue > state.rawValue }
let remains = layoutAdapter.supportedPositions.filter { $0.rawValue > state.rawValue }
return remains.count == 0
}
let panGesture: FloatingPanelPanGestureRecognizer
let panGestureRecognizer: FloatingPanelPanGestureRecognizer
var isRemovalInteractionEnabled: Bool = false
private var animator: UIViewPropertyAnimator?
fileprivate var animator: UIViewPropertyAnimator?
private var initialFrame: CGRect = .zero
private var initialScrollOffset: CGPoint = .zero
private var initialScrollInset: UIEdgeInsets = .zero
private var transOffsetY: CGFloat = 0
private var interactionInProgress: Bool = false
var interactionInProgress: Bool = false
var isDecelerating: Bool = false
// Scroll handling
private var stopScrollDeceleration: Bool = false
@@ -57,7 +57,10 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
init(_ vc: FloatingPanelController, layout: FloatingPanelLayout, behavior: FloatingPanelBehavior) {
viewcontroller = vc
surfaceView = vc.view as! FloatingPanelSurfaceView
surfaceView = FloatingPanelSurfaceView()
surfaceView.backgroundColor = .white
backdropView = FloatingPanelBackdropView()
backdropView.backgroundColor = .black
backdropView.alpha = 0.0
@@ -67,44 +70,26 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
layout: layout)
self.behavior = behavior
state = layoutAdapter.layout.initialPosition
panGesture = FloatingPanelPanGestureRecognizer()
panGestureRecognizer = FloatingPanelPanGestureRecognizer()
if #available(iOS 11.0, *) {
panGesture.name = "FloatingPanelSurface"
panGestureRecognizer.name = "FloatingPanelSurface"
}
super.init()
surfaceView.addGestureRecognizer(panGesture)
panGesture.addTarget(self, action: #selector(handle(panGesture:)))
panGesture.delegate = self
}
panGestureRecognizer.floatingPanel = self
func setUpViews(in vc: UIViewController) {
unowned let view = vc.view!
view.insertSubview(backdropView, belowSubview: surfaceView)
backdropView.frame = view.bounds
layoutAdapter.prepareLayout(toParent: vc)
surfaceView.addGestureRecognizer(panGestureRecognizer)
panGestureRecognizer.addTarget(self, action: #selector(handle(panGesture:)))
panGestureRecognizer.delegate = self
}
func move(to: FloatingPanelPosition, animated: Bool, completion: (() -> Void)? = nil) {
move(from: state, to: to, animated: animated, completion: completion)
}
func present(animated: Bool, completion: (() -> Void)? = nil) {
self.layoutAdapter.activateLayout(of: nil)
move(from: nil, to: layoutAdapter.layout.initialPosition, animated: animated, completion: completion)
}
func dismiss(animated: Bool, completion: (() -> Void)? = nil) {
move(from: state, to: nil, animated: animated, completion: completion)
}
private func move(from: FloatingPanelPosition?, to: FloatingPanelPosition?, animated: Bool, completion: (() -> Void)? = nil) {
private func move(from: FloatingPanelPosition, to: FloatingPanelPosition, animated: Bool, completion: (() -> Void)? = nil) {
if to != .full {
lockScrollView()
}
@@ -112,48 +97,45 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
if animated {
let animator: UIViewPropertyAnimator
switch (from, to) {
case (nil, let to?):
case (.hidden, let to):
animator = behavior.addAnimator(self.viewcontroller, to: to)
case (let from?, let to?):
animator = behavior.moveAnimator(self.viewcontroller, from: from, to: to)
case (let from?, nil):
case (let from, .hidden):
animator = behavior.removeAnimator(self.viewcontroller, from: from)
case (nil, nil):
fatalError()
case (let from, let to):
animator = behavior.moveAnimator(self.viewcontroller, from: from, to: to)
}
animator.addAnimations { [weak self] in
guard let self = self else { return }
guard let `self` = self else { return }
self.updateLayout(to: to)
if let to = to {
self.state = to
}
self.state = to
}
animator.addCompletion { _ in
animator.addCompletion { [weak self] _ in
guard let `self` = self else { return }
self.animator = nil
completion?()
}
self.animator = animator
animator.startAnimation()
} else {
self.updateLayout(to: to)
if let to = to {
self.state = to
}
self.state = to
completion?()
}
}
// MARK: - Layout update
private func updateLayout(to target: FloatingPanelPosition?) {
private func updateLayout(to target: FloatingPanelPosition) {
self.layoutAdapter.activateLayout(of: target)
}
private func getBackdropAlpha(with translation: CGPoint) -> CGFloat {
let currentY = getCurrentY(from: initialFrame, with: translation)
let next = directionalPosition(with: translation)
let pre = redirectionalPosition(with: translation)
let next = directionalPosition(at: currentY, with: translation)
let pre = redirectionalPosition(at: currentY, with: translation)
let nextY = layoutAdapter.positionY(for: next)
let preY = layoutAdapter.positionY(for: pre)
@@ -171,43 +153,13 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
guard gestureRecognizer == panGesture else { return false }
guard gestureRecognizer == panGestureRecognizer else { return false }
/* log.debug("shouldRecognizeSimultaneouslyWith", otherGestureRecognizer) */
return otherGestureRecognizer == scrollView?.panGestureRecognizer
}
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
guard gestureRecognizer == panGesture else { return false }
/* log.debug("shouldBeRequiredToFailBy", otherGestureRecognizer) */
// Do not begin any gestures excluding the tracking scrollView's pan gesture
// until the pan gesture fails
if otherGestureRecognizer == scrollView?.panGestureRecognizer {
return false
} else {
if viewcontroller.delegate?.floatingPanel(viewcontroller, shouldRecognizeSimultaneouslyWith: otherGestureRecognizer) ?? false {
return true
}
}
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool {
guard gestureRecognizer == panGesture else { return false }
/* log.debug("shouldRequireFailureOf", otherGestureRecognizer) */
// Do not begin the pan gesture until any other gestures fail except for
// the tracking scrollView's pan gesture and other gestures.
if let scrollView = scrollView {
if scrollView.panGestureRecognizer == otherGestureRecognizer {
return false
}
// For short scroll contents
if scrollView.gestureRecognizers?.contains(otherGestureRecognizer) ?? false {
return false
}
}
switch otherGestureRecognizer {
case is UIPanGestureRecognizer,
@@ -215,10 +167,67 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
is UIRotationGestureRecognizer,
is UIScreenEdgePanGestureRecognizer,
is UIPinchGestureRecognizer:
return true
// all gestures of the tracking scroll view should be recognized in parallel
// and handle them in self.handle(panGesture:)
return scrollView?.gestureRecognizers?.contains(otherGestureRecognizer) ?? false
default:
// Should always recognize tap/long press gestures in parallel
return true
}
}
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
guard gestureRecognizer == panGestureRecognizer else { return false }
/* log.debug("shouldBeRequiredToFailBy", otherGestureRecognizer) */
return false
}
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool {
guard gestureRecognizer == panGestureRecognizer else { return false }
/* log.debug("shouldRequireFailureOf", otherGestureRecognizer) */
// Should begin the pan gesture without waiting for the tracking scroll view's gestures.
// `scrollView.gestureRecognizers` can contains the following gestures
// * UIScrollViewDelayedTouchesBeganGestureRecognizer
// * UIScrollViewPanGestureRecognizer (scrollView.panGestureRecognizer)
// * _UIDragAutoScrollGestureRecognizer
// * _UISwipeActionPanGestureRecognizer
// * UISwipeDismissalGestureRecognizer
if let scrollView = scrollView {
// On short contents scroll, `_UISwipeActionPanGestureRecognizer` blocks
// the panel's pan gesture if not returns false
if let scrollGestureRecognizers = scrollView.gestureRecognizers,
scrollGestureRecognizers.contains(otherGestureRecognizer) {
return false
}
}
if viewcontroller.delegate?.floatingPanel(viewcontroller, shouldRecognizeSimultaneouslyWith: otherGestureRecognizer) ?? false {
return false
}
switch otherGestureRecognizer {
case is UIPanGestureRecognizer,
is UISwipeGestureRecognizer,
is UIRotationGestureRecognizer,
is UIScreenEdgePanGestureRecognizer,
is UIPinchGestureRecognizer:
// Do not begin the pan gesture until these gestures fail
return true
default:
// Should begin the pan gesture witout waiting tap/long press gestures fail
return false
}
}
var grabberAreaFrame: CGRect {
let grabberAreaFrame = CGRect(x: surfaceView.bounds.origin.x,
y: surfaceView.bounds.origin.y,
width: surfaceView.bounds.width,
height: FloatingPanelSurfaceView.topGrabberBarHeight * 2)
return grabberAreaFrame
}
// MARK: - Gesture handling
@@ -242,8 +251,14 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
if surfaceView.frame.minY > layoutAdapter.topY {
switch state {
case .full:
// Prevent over scrolling from scroll top in moving the panel from full.
scrollView.contentOffset.y = scrollView.contentOffsetZero.y
let point = panGesture.location(in: surfaceView)
if grabberAreaFrame.contains(point) {
// Preserve the current content offset in moving from full.
scrollView.contentOffset.y = initialScrollOffset.y
} else {
// Prevent over scrolling in moving from full.
scrollView.contentOffset.y = scrollView.contentOffsetZero.y
}
case .half, .tip:
guard scrollView.isDecelerating == false else {
// Don't fix the scroll offset in animating the panel to half and tip.
@@ -252,7 +267,9 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
return
}
// Fix the scroll offset in moving the panel from half and tip.
scrollView.contentOffset.y = initialScrollOffset.y
scrollView.contentOffset.y = initialScrollOffset.y + (initialScrollInset.top - scrollView.contentInset.top)
case .hidden:
fatalError("Now .hidden must not be used for a user interaction")
}
// Always hide a scroll indicator at the non-top.
@@ -265,7 +282,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
unlockScrollView()
}
}
case panGesture:
case panGestureRecognizer:
let translation = panGesture.translation(in: panGesture.view!.superview)
let location = panGesture.location(in: panGesture.view)
@@ -275,6 +292,13 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
return
}
if let animator = self.animator {
if animator.isInterruptible {
animator.stopAnimation(true)
}
self.animator = nil
}
switch panGesture.state {
case .began:
panningBegan()
@@ -294,17 +318,26 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
}
private func shouldScrollViewHandleTouch(_ scrollView: UIScrollView?, point: CGPoint, velocity: CGPoint) -> Bool {
let grabberBarFrame = CGRect(x: surfaceView.bounds.origin.x,
y: surfaceView.bounds.origin.y,
width: surfaceView.bounds.width,
height: FloatingPanelSurfaceView.topGrabberBarHeight * 2)
// When no scrollView, nothing to handle.
guard let scrollView = scrollView else { return false }
// For _UISwipeActionPanGestureRecognizer
if let scrollGestureRecognizers = scrollView.gestureRecognizers {
for gesture in scrollGestureRecognizers {
guard gesture.state == .began || gesture.state == .changed
else { continue }
if gesture != scrollView.panGestureRecognizer {
return true
}
}
}
guard
let scrollView = scrollView, // When no scrollView, nothing to handle.
state == .full, // When not .full, don't scroll.
interactionInProgress == false, // When interaction already in progress, don't scroll.
scrollView.frame.contains(point), // When point not in scrollView, don't scroll.
!grabberBarFrame.contains(point) // When point within grabber area, don't scroll.
!grabberAreaFrame.contains(point) // When point within grabber area, don't scroll.
else {
return false
}
@@ -347,6 +380,12 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
private func panningEnd(with translation: CGPoint, velocity: CGPoint) {
log.debug("panningEnd")
guard state != .hidden else {
log.debug("Already hidden")
return
}
if interactionInProgress == false {
initialFrame = surfaceView.frame
}
@@ -359,7 +398,21 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
endInteraction(for: targetPosition)
if isRemovalInteractionEnabled, isBottomState {
if startRemovalAnimation(with: translation, velocity: velocity, distance: distance) {
let velocityVector = (distance != 0) ? CGVector(dx: 0,
dy: min(fabs(velocity.y)/distance, behavior.removalVelocity)) : .zero
if shouldStartRemovalAnimation(with: translation, velocityVector: velocityVector) {
viewcontroller.delegate?.floatingPanelDidEndDraggingToRemove(viewcontroller, withVelocity: velocity)
self.startRemovalAnimation(with: velocityVector) { [weak self] in
guard let `self` = self else { return }
self.viewcontroller.dismiss(animated: false, completion: { [weak self] in
guard let `self` = self else { return }
self.viewcontroller.delegate?.floatingPanelDidEndRemove(self.viewcontroller)
})
}
return
}
}
@@ -370,43 +423,63 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
startAnimation(to: targetPosition, at: distance, with: velocity)
}
private func startRemovalAnimation(with translation: CGPoint, velocity: CGPoint, distance: CGFloat) -> Bool {
private func shouldStartRemovalAnimation(with translation: CGPoint, velocityVector: CGVector) -> Bool {
let posY = layoutAdapter.positionY(for: state)
let currentY = getCurrentY(from: initialFrame, with: translation)
let safeAreaBottomY = layoutAdapter.safeAreaBottomY
let vth = behavior.removalVelocity
let pth = max(min(behavior.removalProgress, 1.0), 0.0)
let velocityVector = (distance != 0) ? CGVector(dx: 0, dy: max(min(velocity.y/distance, vth), 0.0)) : .zero
guard (safeAreaBottomY - posY) != 0 else { return false }
guard (currentY - posY) / (safeAreaBottomY - posY) >= pth || velocityVector.dy == vth else { return false }
let num = (currentY - posY)
let den = (safeAreaBottomY - posY)
guard num >= 0, den != 0, (num / den >= pth || velocityVector.dy == vth)
else { return false }
viewcontroller.delegate?.floatingPanelDidEndDraggingToRemove(viewcontroller, withVelocity: velocity)
let animator = self.behavior.removalInteractionAnimator(self.viewcontroller, with: velocityVector)
animator.addAnimations { [weak self] in
guard let self = self else { return }
self.updateLayout(to: nil)
}
animator.addCompletion({ [weak self] (_) in
guard let self = self else { return }
self.viewcontroller.removePanelFromParent(animated: false)
self.viewcontroller.delegate?.floatingPanelDidEndRemove(self.viewcontroller)
})
animator.startAnimation()
return true
}
private func startRemovalAnimation(with velocityVector: CGVector, completion: (() -> Void)?) {
let animator = self.behavior.removalInteractionAnimator(self.viewcontroller, with: velocityVector)
animator.addAnimations { [weak self] in
self?.updateLayout(to: .hidden)
}
animator.addCompletion({ _ in
self.animator = nil
completion?()
})
self.animator = animator
animator.startAnimation()
}
private func startInteraction(with translation: CGPoint) {
/* Don't lock a scroll view to show a scroll indicator after hitting the top */
log.debug("startInteraction")
initialFrame = surfaceView.frame
if let scrollView = scrollView {
initialScrollOffset = scrollView.contentOffset
initialScrollInset = scrollView.contentInset
}
transOffsetY = translation.y
viewcontroller.delegate?.floatingPanelWillBeginDragging(viewcontroller)
if state == .full {
viewcontroller.contentViewController?.view?.constraints.forEach({ (const) in
switch viewcontroller.contentViewController?.layoutGuide.bottomAnchor {
case const.firstAnchor:
(const.secondItem as? UIView)?.disableAutoLayout()
const.isActive = false
case const.secondAnchor:
(const.firstItem as? UIView)?.disableAutoLayout()
const.isActive = false
default:
break
}
})
}
interactionInProgress = true
}
@@ -418,6 +491,21 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
if targetPosition != .full {
lockScrollView()
}
if state == .full {
viewcontroller.contentViewController?.view?.constraints.forEach({ (const) in
switch viewcontroller.contentViewController?.layoutGuide.bottomAnchor {
case const.firstAnchor:
(const.secondItem as? UIView)?.enableAutoLayout()
const.isActive = true
case const.secondAnchor:
(const.firstItem as? UIView)?.enableAutoLayout()
const.isActive = true
default:
break
}
})
}
}
private func getCurrentY(from rect: CGRect, with translation: CGPoint) -> CGFloat {
@@ -428,23 +516,28 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
let topBuffer = layoutAdapter.layout.topInteractionBuffer
let bottomY = layoutAdapter.bottomY
let bottomBuffer = layoutAdapter.layout.bottomInteractionBuffer
let topMax = layoutAdapter.topMaxY
let bottomMax = layoutAdapter.bottomMaxY
if let scrollView = scrollView, scrollView.panGestureRecognizer.state == .changed {
let preY = surfaceView.frame.origin.y
if preY > 0 && preY > y {
return max(topY, min(bottomY, y))
return max(max(topY, topMax), min(min(bottomY + bottomBuffer, bottomMax), y))
}
}
return max(topY - topBuffer, min(bottomY + bottomBuffer, y))
return max(max(topY - topBuffer, topMax), min(min(bottomY + bottomBuffer, bottomMax), y))
}
private func startAnimation(to targetPosition: FloatingPanelPosition, at distance: CGFloat, with velocity: CGPoint) {
log.debug("startAnimation", targetPosition, distance, velocity)
let targetY = layoutAdapter.positionY(for: targetPosition)
let velocityVector = (distance != 0) ? CGVector(dx: 0, dy: max(min(velocity.y/distance, 30.0), -30.0)) : .zero
isDecelerating = true
let velocityVector = (distance != 0) ? CGVector(dx: 0, dy: min(fabs(velocity.y)/distance, 30.0)) : .zero
let animator = behavior.interactionAnimator(self.viewcontroller, to: targetPosition, with: velocityVector)
animator.isInterruptible = false // To prevent a backdrop color's punk
animator.addAnimations { [weak self] in
guard let self = self else { return }
guard let `self` = self else { return }
if self.state == targetPosition {
self.surfaceView.frame.origin.y = targetY
self.layoutAdapter.setBackdropAlpha(of: targetPosition)
@@ -454,21 +547,22 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
self.state = targetPosition
}
animator.addCompletion { [weak self] pos in
guard let self = self else { return }
guard let `self` = self else { return }
self.isDecelerating = false
guard
self.interactionInProgress == false,
animator == self.animator,
pos == .end
else { return }
self.animator = nil
self.finishAnimation(at: targetPosition)
}
animator.startAnimation()
self.animator = animator
animator.startAnimation()
}
private func finishAnimation(at targetPosition: FloatingPanelPosition) {
log.debug("finishAnimation \(targetPosition)")
self.animator = nil
self.viewcontroller.delegate?.floatingPanelDidEndDecelerating(self.viewcontroller)
stopScrollDeceleration = false
@@ -483,72 +577,46 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
let middleY = layoutAdapter.middleY
let bottomY = layoutAdapter.bottomY
let currentY = getCurrentY(from: initialFrame, with: translation)
switch targetPosition {
case .full:
return CGFloat(fabs(Double(currentY - topY)))
return CGFloat(fabs(currentY - topY))
case .half:
return CGFloat(fabs(Double(currentY - middleY)))
return CGFloat(fabs(currentY - middleY))
case .tip:
return CGFloat(fabs(Double(currentY - bottomY)))
return CGFloat(fabs(currentY - bottomY))
case .hidden:
fatalError("Now .hidden must not be used for a user interaction")
}
}
private func directionalPosition(with translation: CGPoint) -> FloatingPanelPosition {
let currentY = getCurrentY(from: initialFrame, with: translation)
let supportedPositions: Set = layoutAdapter.layout.supportedPositions
if supportedPositions.count == 1 {
return state
}
switch supportedPositions {
case [.full, .half]: return translation.y >= 0 ? .half : .full
case [.half, .tip]: return translation.y >= 0 ? .tip : .half
case [.full, .tip]: return translation.y >= 0 ? .tip : .full
default:
let middleY = layoutAdapter.middleY
switch state {
case .full:
if translation.y <= 0 {
return .full
}
return currentY > middleY ? .tip : .half
case .half:
return translation.y >= 0 ? .tip : .full
case .tip:
if translation.y >= 0 {
return .tip
}
return currentY > middleY ? .half : .full
}
}
private func directionalPosition(at currentY: CGFloat, with translation: CGPoint) -> FloatingPanelPosition {
return getPosition(at: currentY, with: translation, directional: true)
}
private func redirectionalPosition(with translation: CGPoint) -> FloatingPanelPosition {
let currentY = getCurrentY(from: initialFrame, with: translation)
let supportedPositions: Set = layoutAdapter.layout.supportedPositions
private func redirectionalPosition(at currentY: CGFloat, with translation: CGPoint) -> FloatingPanelPosition {
return getPosition(at: currentY, with: translation, directional: false)
}
private func getPosition(at currentY: CGFloat, with translation: CGPoint, directional: Bool) -> FloatingPanelPosition {
let supportedPositions: Set = layoutAdapter.supportedPositions
if supportedPositions.count == 1 {
return state
}
let isForwardYAxis = (translation.y >= 0)
switch supportedPositions {
case [.full, .half]: return translation.y >= 0 ? .full : .half
case [.half, .tip]: return translation.y >= 0 ? .half : .tip
case [.full, .tip]: return translation.y >= 0 ? .full : .tip
case [.full, .half]:
return (isForwardYAxis == directional) ? .half : .full
case [.half, .tip]:
return (isForwardYAxis == directional) ? .tip : .half
case [.full, .tip]:
return (isForwardYAxis == directional) ? .tip : .full
default:
let middleY = layoutAdapter.middleY
switch state {
case .full:
return currentY > middleY ? .half : .full
case .half:
return .half
case .tip:
return currentY > middleY ? .tip : .half
if currentY > middleY {
return (isForwardYAxis == directional) ? .tip : .half
} else {
return (isForwardYAxis == directional) ? .half : .full
}
}
}
@@ -556,13 +624,13 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
// Distance travelled after decelerating to zero velocity at a constant rate.
// Refer to the slides p176 of [Designing Fluid Interfaces](https://developer.apple.com/videos/play/wwdc2018/803/)
private func project(initialVelocity: CGFloat) -> CGFloat {
let decelerationRate = UIScrollView.DecelerationRate.normal.rawValue
let decelerationRate = UIScrollViewDecelerationRateNormal
return (initialVelocity / 1000.0) * decelerationRate / (1.0 - decelerationRate)
}
private func targetPosition(with translation: CGPoint, velocity: CGPoint) -> (FloatingPanelPosition) {
let currentY = getCurrentY(from: initialFrame, with: translation)
let supportedPositions: Set = layoutAdapter.layout.supportedPositions
let supportedPositions = layoutAdapter.supportedPositions
if supportedPositions.count == 1 {
return state
@@ -583,27 +651,27 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
let middleY = layoutAdapter.middleY
let bottomY = layoutAdapter.bottomY
let target: FloatingPanelPosition
let nextState: FloatingPanelPosition
let forwardYDirection: Bool
/*
full <-> half <-> tip
*/
switch state {
case .full:
target = .half
nextState = .half
forwardYDirection = true
case .half:
if (currentY < middleY) {
target = .full
forwardYDirection = false
} else {
target = .tip
forwardYDirection = true
}
nextState = (currentY > middleY) ? .tip : .full
forwardYDirection = (currentY > middleY)
case .tip:
target = .half
nextState = .half
forwardYDirection = false
case .hidden:
fatalError("Now .hidden must not be used for a user interaction")
}
let redirectionalProgress = max(min(behavior.redirectionalProgress(viewcontroller, from: state, to: target), 1.0), 0.0)
let redirectionalProgress = max(min(behavior.redirectionalProgress(viewcontroller, from: state, to: nextState), 1.0), 0.0)
let th1: CGFloat
let th2: CGFloat
@@ -719,12 +787,24 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
targetContentOffset.pointee = scrollView.contentOffset
stopScrollDeceleration = false
} else {
let targetOffset = targetContentOffset.pointee
userScrollViewDelegate?.scrollViewWillEndDragging?(scrollView, withVelocity: velocity, targetContentOffset: targetContentOffset)
// Stop scrolling on tip and half
if state != .full, targetOffset == targetContentOffset.pointee {
targetContentOffset.pointee.y = scrollView.contentOffset.y
}
}
}
}
class FloatingPanelPanGestureRecognizer: UIPanGestureRecognizer {
fileprivate var floatingPanel: FloatingPanel?
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event)
if floatingPanel?.animator != nil {
self.state = .began
}
}
override weak var delegate: UIGestureRecognizerDelegate? {
get {
return super.delegate
@@ -85,7 +85,9 @@ public extension FloatingPanelBehavior {
class FloatingPanelDefaultBehavior: FloatingPanelBehavior {
func interactionAnimator(_ fpc: FloatingPanelController, to targetPosition: FloatingPanelPosition, with velocity: CGVector) -> UIViewPropertyAnimator {
let timing = timeingCurve(with: velocity)
return UIViewPropertyAnimator(duration: 0, timingParameters: timing)
let animator = UIViewPropertyAnimator(duration: 0, timingParameters: timing)
animator.isInterruptible = false
return animator
}
private func timeingCurve(with velocity: CGVector) -> UITimingCurveProvider {
+209 -82
View File
@@ -12,6 +12,8 @@ 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
func floatingPanelDidMove(_ vc: FloatingPanelController) // any offset changes
// called on start of dragging (may require some time and or distance to move)
@@ -25,6 +27,8 @@ public protocol FloatingPanelControllerDelegate: class {
func floatingPanelDidEndDraggingToRemove(_ vc: FloatingPanelController, withVelocity velocity: CGPoint)
// called when its views are removed from a parent view controller
func floatingPanelDidEndRemove(_ vc: FloatingPanelController)
func floatingPanel(_ vc: FloatingPanelController, shouldRecognizeSimultaneouslyWith gestureRecognizer: UIGestureRecognizer) -> Bool
}
public extension FloatingPanelControllerDelegate {
@@ -34,6 +38,7 @@ public extension FloatingPanelControllerDelegate {
func floatingPanel(_ vc: FloatingPanelController, behaviorFor newCollection: UITraitCollection) -> FloatingPanelBehavior? {
return nil
}
func floatingPanelDidChangePosition(_ vc: FloatingPanelController) {}
func floatingPanelDidMove(_ vc: FloatingPanelController) {}
func floatingPanelWillBeginDragging(_ vc: FloatingPanelController) {}
func floatingPanelDidEndDragging(_ vc: FloatingPanelController, withVelocity velocity: CGPoint, targetPosition: FloatingPanelPosition) {}
@@ -42,19 +47,22 @@ public extension FloatingPanelControllerDelegate {
func floatingPanelDidEndDraggingToRemove(_ vc: FloatingPanelController, withVelocity velocity: CGPoint) {}
func floatingPanelDidEndRemove(_ vc: FloatingPanelController) {}
func floatingPanel(_ vc: FloatingPanelController, shouldRecognizeSimultaneouslyWith gestureRecognizer: UIGestureRecognizer) -> Bool { return false }
}
public enum FloatingPanelPosition: Int, CaseIterable {
public enum FloatingPanelPosition: Int {
case full
case half
case tip
case hidden
}
///
/// A container view controller to display a floating panel to present contents in parallel as a user wants.
///
public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UIGestureRecognizerDelegate {
/// Constants indicating how safe area insets are added to the adjusted content inset.
public enum ContentInsetAdjustmentBehavior: Int {
case always
@@ -62,11 +70,15 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
}
/// The delegate of the floating panel controller object.
public weak var delegate: FloatingPanelControllerDelegate?
public weak var delegate: FloatingPanelControllerDelegate?{
didSet{
didUpdateDelegate()
}
}
/// Returns the surface view managed by the controller object. It's the same as `self.view`.
public var surfaceView: FloatingPanelSurfaceView! {
return view as? FloatingPanelSurfaceView
return floatingPanel.surfaceView
}
/// Returns the backdrop view managed by the controller object.
@@ -81,7 +93,7 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
// The underlying gesture recognizer for pan gestures
public var panGestureRecognizer: UIPanGestureRecognizer {
return floatingPanel.panGesture
return floatingPanel.panGestureRecognizer
}
/// The current position of the floating panel controller's contents.
@@ -99,7 +111,7 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
return floatingPanel.behavior
}
/// The content insets of the tracking scroll view derived from the safe area of the parent view
/// The content insets of the tracking scroll view derived from this safe area
public var adjustedContentInsets: UIEdgeInsets {
return floatingPanel.layoutAdapter.adjustedContentInsets
}
@@ -123,63 +135,89 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
private var _contentViewController: UIViewController?
private var floatingPanel: FloatingPanel!
private var layoutInsetsObservations: [NSKeyValueObservation] = []
private var safeAreaInsetsObservation: NSKeyValueObservation?
private let modalTransition = FloatingPanelModalTransition()
required init?(coder aDecoder: NSCoder) {
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
floatingPanel = FloatingPanel(self,
layout: fetchLayout(for: self.traitCollection),
behavior: fetchBehavior(for: self.traitCollection))
setUp()
}
/// Initialize a newly created floating panel controller.
public init() {
public init(delegate: FloatingPanelControllerDelegate? = nil) {
super.init(nibName: nil, bundle: nil)
self.delegate = delegate
setUp()
}
private func setUp() {
_ = FloatingPanelController.dismissSwizzling
modalPresentationStyle = .custom
transitioningDelegate = modalTransition
floatingPanel = FloatingPanel(self,
layout: fetchLayout(for: self.traitCollection),
behavior: fetchBehavior(for: self.traitCollection))
}
private func didUpdateDelegate(){
floatingPanel.layoutAdapter.layout = fetchLayout(for: traitCollection)
floatingPanel.behavior = fetchBehavior(for: self.traitCollection)
}
// MARK:- Overrides
/// Creates the view that the controller manages.
override public func loadView() {
assert(self.storyboard == nil, "Storyboard isn't supported")
let view = FloatingPanelSurfaceView()
view.backgroundColor = .white
let view = FloatingPanelPassThroughView()
view.backgroundColor = .clear
backdropView.frame = view.bounds
view.addSubview(backdropView)
surfaceView.frame = view.bounds
view.addSubview(surfaceView)
self.view = view as UIView
}
public override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if #available(iOS 11.0, *) {}
else {
// Because {top,bottom}LayoutGuide is managed as a view
self.update(safeAreaInsets: layoutInsets)
}
}
public override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
if view.translatesAutoresizingMaskIntoConstraints {
view.frame.size = size
view.layoutIfNeeded()
}
}
public override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) {
super.willTransition(to: newCollection, with: coordinator)
// Change layout for a new trait collection
updateLayout(for: newCollection)
reloadLayout(for: newCollection)
setUpLayout()
floatingPanel.behavior = fetchBehavior(for: newCollection)
}
public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
guard previousTraitCollection != traitCollection else { return }
if let parent = parent {
self.update(safeAreaInsets: parent.layoutInsets)
}
public override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
safeAreaInsetsObservation = nil
}
public override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// Need to update safeAreaInsets here to ensure that the `adjustedContentInsets` has a correct value.
// Because the parent VC does not call viewSafeAreaInsetsDidChange() expectedly and
// `view.safeAreaInsets` has a correct value of the bottom inset here.
if let parent = parent {
self.update(safeAreaInsets: parent.layoutInsets)
}
}
// MARK:- Privates
private func fetchLayout(for traitCollection: UITraitCollection) -> FloatingPanelLayout {
switch traitCollection.verticalSizeClass {
@@ -195,7 +233,19 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
}
private func update(safeAreaInsets: UIEdgeInsets) {
floatingPanel.safeAreaInsets = safeAreaInsets
// Don't re-layout the surface on SafeArea.Bottom enabled/disabled in interaction progress
guard
floatingPanel.layoutAdapter.safeAreaInsets != safeAreaInsets,
self.floatingPanel.interactionInProgress == false,
self.floatingPanel.isDecelerating == false
else { return }
log.debug("Update safeAreaInsets", safeAreaInsets)
floatingPanel.layoutAdapter.safeAreaInsets = safeAreaInsets
setUpLayout()
switch contentInsetAdjustmentBehavior {
case .always:
scrollView?.contentInset = adjustedContentInsets
@@ -205,17 +255,58 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
}
}
private func updateLayout(for traitCollection: UITraitCollection) {
private func reloadLayout(for traitCollection: UITraitCollection) {
floatingPanel.layoutAdapter.layout = fetchLayout(for: traitCollection)
floatingPanel.layoutAdapter.prepareLayout(in: self)
}
guard let parent = parent else { return }
private func setUpLayout() {
// preserve the current content offset
let contentOffset = scrollView?.contentOffset
floatingPanel.layoutAdapter.prepareLayout(toParent: parent)
floatingPanel.layoutAdapter.updateHeight()
floatingPanel.layoutAdapter.activateLayout(of: floatingPanel.state)
scrollView?.contentOffset = contentOffset ?? .zero
}
// MARK: - Container view controller interface
/// Shows the surface view at the initial position defined by the current layout
public func show(animated: Bool = false, completion: (() -> Void)? = nil) {
// Must apply the current layout here
reloadLayout(for: traitCollection)
setUpLayout()
if #available(iOS 11.0, *) {
// Must track the safeAreaInsets of `self.view` to update the layout.
// There are 2 reasons.
// 1. This or the parent VC doesn't call viewSafeAreaInsetsDidChange() on the bottom
// inset's update expectedly.
// 2. The safe area top inset can be variable on the large title navigation bar(iOS11+).
// That's why it needs the observation to keep `adjustedContentInsets` correct.
safeAreaInsetsObservation = self.observe(\.view.safeAreaInsets) { [weak self] (vc, chaneg) in
guard let `self` = self else { return }
self.update(safeAreaInsets: vc.layoutInsets)
}
} else {
// KVOs for topLayoutGuide & bottomLayoutGuide are not effective.
// Instead, update(safeAreaInsets:) is called at `viewDidLayoutSubviews()`
}
move(to: floatingPanel.layoutAdapter.layout.initialPosition,
animated: animated,
completion: completion)
}
/// Hides the surface view to the hidden position
public func hide(animated: Bool = false, completion: (() -> Void)? = nil) {
safeAreaInsetsObservation = nil
move(to: .hidden,
animated: animated,
completion: completion)
}
/// Adds the view managed by the controller as a child of the specified view controller.
/// - Parameters:
/// - parent: A parent view controller object that displays FloatingPanelController's view. A container view controller object isn't applicable.
@@ -232,43 +323,26 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
precondition((parent is UITableViewController) == false, "UITableViewController should not be the parent because the view is a table view so that a floating panel doens't work well")
precondition((parent is UICollectionViewController) == false, "UICollectionViewController should not be the parent because the view is a collection view so that a floating panel doens't work well")
view.frame = parent.view.bounds
if let belowView = belowView {
parent.view.insertSubview(self.view, belowSubview: belowView)
} else {
parent.view.addSubview(self.view)
}
layoutInsetsObservations.removeAll()
parent.addChildViewController(self)
// Must track safeAreaInsets/{top,bottom}LayoutGuide of the `parent.view`
// to update floatingPanel.safeAreaInsets`. There are 2 reasons.
// 1. The parent VC doesn't call viewSafeAreaInsetsDidChange() on the bottom
// inset's update expectedly.
// 2. The safe area top inset can be variable on the large title navigation bar.
// That's why it needs the observation to keep `adjustedContentInsets` correct.
if #available(iOS 11.0, *) {
let observaion = parent.observe(\.view.safeAreaInsets) { [weak self] (vc, chaneg) in
guard let self = self else { return }
self.update(safeAreaInsets: vc.layoutInsets)
}
layoutInsetsObservations.append(observaion)
} else {
// KVOs for topLayoutGuide & bottomLayoutGuide are not effective.
// Instead, safeAreaInsets will be updated in viewDidAppear()
}
view.frame = parent.view.bounds // Needed for a correct safe area configuration
view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
self.view.topAnchor.constraint(equalTo: parent.view.topAnchor, constant: 0.0),
self.view.leftAnchor.constraint(equalTo: parent.view.leftAnchor, constant: 0.0),
self.view.rightAnchor.constraint(equalTo: parent.view.rightAnchor, constant: 0.0),
self.view.bottomAnchor.constraint(equalTo: parent.view.bottomAnchor, constant: 0.0),
])
parent.addChild(self)
// Must set a layout again here because `self.traitCollection` is applied correctly once it's added to a parent VC
floatingPanel.layoutAdapter.layout = fetchLayout(for: traitCollection)
floatingPanel.behavior = fetchBehavior(for: traitCollection)
floatingPanel.setUpViews(in: parent)
floatingPanel.present(animated: animated) { [weak self] in
guard let self = self else { return }
self.didMove(toParent: parent)
show(animated: animated) { [weak self] in
guard let `self` = self else { return }
self.didMove(toParentViewController: self)
}
}
@@ -282,14 +356,11 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
return
}
layoutInsetsObservations.removeAll()
floatingPanel.dismiss(animated: animated) { [weak self] in
guard let self = self else { return }
self.willMove(toParent: nil)
hide(animated: animated) { [weak self] in
guard let `self` = self else { return }
self.willMove(toParentViewController: nil)
self.view.removeFromSuperview()
self.removeFromParent()
self.removeFromParentViewController()
completion?()
}
}
@@ -300,22 +371,29 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
/// - animated: Pass true to animate the presentation; otherwise, pass false.
/// - completion: The block to execute after the view controller has finished moving. This block has no return value and takes no parameters. You may specify nil for this parameter.
public func move(to: FloatingPanelPosition, animated: Bool, completion: (() -> Void)? = nil) {
precondition(floatingPanel.layoutAdapter.vc != nil, "Use show(animated:completion)")
floatingPanel.move(to: to, animated: animated, completion: completion)
}
/// Sets the view controller responsible for the content portion of the floating panel..
public func set(contentViewController: UIViewController?) {
if let vc = _contentViewController {
vc.willMove(toParent: nil)
vc.willMove(toParentViewController: nil)
vc.view.removeFromSuperview()
vc.removeFromParent()
vc.removeFromParentViewController()
if let scrollView = floatingPanel.scrollView,
let delegate = floatingPanel.userScrollViewDelegate,
vc.view.subviews.contains(scrollView) {
scrollView.delegate = delegate
}
}
if let vc = contentViewController {
let surfaceView = self.view as! FloatingPanelSurfaceView
surfaceView.add(childView: vc.view)
addChild(vc)
vc.didMove(toParent: self)
addChildViewController(vc)
let surfaceView = floatingPanel.surfaceView
surfaceView.add(contentView: vc.view)
vc.didMove(toParentViewController: self)
}
_contentViewController = contentViewController
@@ -353,7 +431,7 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
if #available(iOS 11.0, *) {
scrollView.contentInsetAdjustmentBehavior = .never
} else {
children.forEach { (vc) in
childViewControllers.forEach { (vc) in
vc.automaticallyAdjustsScrollViewInsets = false
}
}
@@ -368,11 +446,12 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
/// by the controller immediately.
///
/// This method updates the `FloatingPanelLayout` object from the delegate and
/// then it calls `layoutIfNeeded()` of the parent's root view to force the view
/// then it calls `layoutIfNeeded()` of the root view to force the view
/// to update the floating panel's layout immediately. It can be called in an
/// animation block.
public func updateLayout() {
updateLayout(for: view.traitCollection)
reloadLayout(for: traitCollection)
setUpLayout()
}
/// Returns the y-coordinate of the point at the origin of the surface view
@@ -384,6 +463,54 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
return floatingPanel.layoutAdapter.middleY
case .tip:
return floatingPanel.layoutAdapter.bottomY
case .hidden:
return floatingPanel.layoutAdapter.hiddenY
}
}
}
extension FloatingPanelController {
private static let dismissSwizzling: Any? = {
let aClass: AnyClass! = UIViewController.self //object_getClass(vc)
if let imp = class_getMethodImplementation(aClass, #selector(dismiss(animated:completion:))),
let originalAltMethod = class_getInstanceMethod(aClass, #selector(fp_original_dismiss(animated:completion:))) {
method_setImplementation(originalAltMethod, imp)
}
let originalMethod = class_getInstanceMethod(aClass, #selector(dismiss(animated:completion:)))
let swizzledMethod = class_getInstanceMethod(aClass, #selector(fp_dismiss(animated:completion:)))
if let originalMethod = originalMethod, let swizzledMethod = swizzledMethod {
// switch implementation..
method_exchangeImplementations(originalMethod, swizzledMethod)
}
return nil
}()
}
public extension UIViewController {
@objc public 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) {
// Call dismiss(animated:completion:) to a content view controller
if let fpc = parent as? FloatingPanelController {
if fpc.presentingViewController != nil {
self.fp_original_dismiss(animated: flag, completion: completion)
} else {
fpc.removePanelFromParent(animated: flag, completion: completion)
}
return
}
// Call dismiss(animated:completion:) to FloatingPanelController directly
if let fpc = self as? FloatingPanelController {
if fpc.presentingViewController != nil {
self.fp_original_dismiss(animated: flag, completion: completion)
} else {
fpc.removePanelFromParent(animated: flag, completion: completion)
}
return
}
// For other view controllers
self.fp_original_dismiss(animated: flag, completion: completion)
}
}
+187 -81
View File
@@ -5,11 +5,46 @@
import UIKit
/// FloatingPanelFullScreenLayout
///
/// Use the layout protocol if you want to configure a full inset from Superview.Top, not SafeArea.Top.
/// It can't be used with FloatingPanelIntrinsicLayout.
public protocol FloatingPanelFullScreenLayout: FloatingPanelLayout { }
/// FloatingPanelIntrinsicLayout
///
/// Use the layout protocol if you want to layout a panel using the intrinsic height.
/// It can't be used with FloatingPanelFullScreenLayout.
///
/// - Attention:
/// `insetFor(position:)` must return `nil` for the full position. Because
/// the inset is determined automatically by the intrinsic height.
/// You can customize insets only for the half, tip and hidden positions.
public protocol FloatingPanelIntrinsicLayout: FloatingPanelLayout { }
public extension FloatingPanelIntrinsicLayout {
var initialPosition: FloatingPanelPosition {
return .full
}
var supportedPositions: Set<FloatingPanelPosition> {
return [.full]
}
func insetFor(position: FloatingPanelPosition) -> CGFloat? {
return nil
}
}
public protocol FloatingPanelLayout: class {
/// Returns the initial position of a floating panel.
var initialPosition: FloatingPanelPosition { get }
/// Returns a set of FloatingPanelPosition objects to tell the applicable positions of the floating panel controller. Default is all of them.
/// 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.
var supportedPositions: Set<FloatingPanelPosition> { get }
/// Return the interaction buffer to the top from the top position. Default is 6.0.
@@ -18,10 +53,13 @@ public protocol FloatingPanelLayout: class {
/// Return the interaction buffer to the bottom from the bottom position. Default is 6.0.
var bottomInteractionBuffer: CGFloat { get }
/// Returns a CGFloat value to determine a floating panel height for each position(full, half and tip).
/// A value for full position indicates a top inset from a safe area.
/// On the other hand, values for half and tip positions indicate bottom insets from a safe area.
/// If a position doesn't contain the supported positions, return nil.
/// Returns a CGFloat value to determine a Y coordinate of a floating panel for each position(full, half, tip and hidden).
///
/// Its returning value indicates a different inset for each position.
/// For full position, a top inset from a safe area in `FloatingPanelController.view`.
/// For half or tip position, a bottom inset from the safe area.
/// For hidden position, a bottom inset from `FloatingPanelController.view`.
/// If a position isn't supported or the default value is used, return nil.
func insetFor(position: FloatingPanelPosition) -> CGFloat?
/// Returns X-axis and width layout constraints of the surface view of a floating panel.
@@ -41,9 +79,9 @@ public extension FloatingPanelLayout {
var bottomInteractionBuffer: CGFloat { return 6.0 }
var supportedPositions: Set<FloatingPanelPosition> {
return Set(FloatingPanelPosition.allCases)
return Set([.full, .half, .tip])
}
func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint] {
return [
surfaceView.leftAnchor.constraint(equalTo: view.sideLayoutGuide.leftAnchor, constant: 0.0),
@@ -66,6 +104,7 @@ public class FloatingPanelDefaultLayout: FloatingPanelLayout {
case .full: return 18.0
case .half: return 262.0
case .tip: return 69.0
case .hidden: return nil
}
}
}
@@ -89,7 +128,7 @@ public class FloatingPanelDefaultLandscapeLayout: FloatingPanelLayout {
class FloatingPanelLayoutAdapter {
private weak var parent: UIViewController!
weak var vc: UIViewController!
private weak var surfaceView: FloatingPanelSurfaceView!
private weak var backdropView: FloatingPanelBackdropView!
@@ -99,15 +138,8 @@ class FloatingPanelLayoutAdapter {
}
}
var safeAreaInsets: UIEdgeInsets = .zero {
didSet {
updateHeight()
checkLayoutConsistance()
}
}
var safeAreaInsets: UIEdgeInsets = .zero
private var parentHeight: CGFloat = 0.0
private var heightBuffer: CGFloat = 88.0 // For bounce
private var fixedConstraints: [NSLayoutConstraint] = []
private var fullConstraints: [NSLayoutConstraint] = []
private var halfConstraints: [NSLayoutConstraint] = []
@@ -116,7 +148,11 @@ class FloatingPanelLayoutAdapter {
private var heightConstraints: [NSLayoutConstraint] = []
private var fullInset: CGFloat {
return layout.insetFor(position: .full) ?? 0.0
if layout is FloatingPanelIntrinsicLayout {
return intrinsicHeight
} else {
return layout.insetFor(position: .full) ?? 0.0
}
}
private var halfInset: CGFloat {
return layout.insetFor(position: .half) ?? 0.0
@@ -124,10 +160,26 @@ class FloatingPanelLayoutAdapter {
private var tipInset: CGFloat {
return layout.insetFor(position: .tip) ?? 0.0
}
private var hiddenInset: CGFloat {
return layout.insetFor(position: .hidden) ?? 0.0
}
var supportedPositions: Set<FloatingPanelPosition> {
var supportedPositions = layout.supportedPositions
supportedPositions.remove(.hidden)
return supportedPositions
}
var topY: CGFloat {
if layout.supportedPositions.contains(.full) {
return (safeAreaInsets.top + fullInset)
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
}
@@ -138,17 +190,26 @@ class FloatingPanelLayoutAdapter {
}
var bottomY: CGFloat {
if layout.supportedPositions.contains(.tip) {
if supportedPositions.contains(.tip) {
return surfaceView.superview!.bounds.height - (safeAreaInsets.bottom + tipInset)
} else {
return middleY
}
}
var safeAreaBottomY: CGFloat {
return surfaceView.superview!.bounds.height - (safeAreaInsets.bottom)
var hiddenY: CGFloat {
return surfaceView.superview!.bounds.height
}
var safeAreaBottomY: CGFloat {
return surfaceView.superview!.bounds.height - (safeAreaInsets.bottom + hiddenInset)
}
var topMaxY: CGFloat {
return layout is FloatingPanelFullScreenLayout ? 0.0 : safeAreaInsets.top
}
var bottomMaxY: CGFloat { return safeAreaBottomY }
var adjustedContentInsets: UIEdgeInsets {
return UIEdgeInsets(top: 0.0,
left: 0.0,
@@ -164,136 +225,181 @@ class FloatingPanelLayoutAdapter {
return middleY
case .tip:
return bottomY
case .hidden:
return hiddenY
}
}
var intrinsicHeight: CGFloat = 0.0
init(surfaceView: FloatingPanelSurfaceView, backdropView: FloatingPanelBackdropView, layout: FloatingPanelLayout) {
self.layout = layout
self.surfaceView = surfaceView
self.backdropView = backdropView
}
func prepareLayout(toParent parent: UIViewController) {
self.parent = parent
func updateIntrinsicHeight() {
let fittingSize = UILayoutFittingCompressedSize
var intrinsicHeight = surfaceView.contentView?.systemLayoutSizeFitting(fittingSize).height ?? 0.0
var safeAreaBottom: CGFloat = 0.0
if #available(iOS 11.0, *) {
safeAreaBottom = surfaceView.contentView?.safeAreaInsets.bottom ?? 0.0
if safeAreaBottom > 0 {
intrinsicHeight -= safeAreaInsets.bottom
}
}
self.intrinsicHeight = max(intrinsicHeight, 0.0)
log.debug("Update intrinsic height =", intrinsicHeight,
", surface(height) =", surfaceView.frame.height,
", content(height) =", surfaceView.contentView?.frame.height ?? 0.0,
", content safe area(bottom) =", safeAreaBottom)
}
func prepareLayout(in vc: UIViewController) {
self.vc = vc
NSLayoutConstraint.deactivate(fixedConstraints + fullConstraints + halfConstraints + tipConstraints + offConstraints)
surfaceView.translatesAutoresizingMaskIntoConstraints = false
backdropView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.deactivate(fixedConstraints + fullConstraints + halfConstraints + tipConstraints + offConstraints)
// Fixed constraints of surface and backdrop views
let surfaceConstraints = layout.prepareLayout(surfaceView: surfaceView, in: parent.view!)
let surfaceConstraints = layout.prepareLayout(surfaceView: surfaceView, in: vc.view!)
let backdropConstraints = [
backdropView.topAnchor.constraint(equalTo: parent.view.topAnchor,
constant: 0.0),
backdropView.leftAnchor.constraint(equalTo: parent.view.leftAnchor,
constant: 0.0),
backdropView.rightAnchor.constraint(equalTo: parent.view.rightAnchor,
constant: 0.0),
backdropView.bottomAnchor.constraint(equalTo: parent.view.bottomAnchor,
constant: 0.0),
backdropView.topAnchor.constraint(equalTo: vc.view.topAnchor, constant: 0.0),
backdropView.leftAnchor.constraint(equalTo: vc.view.leftAnchor,constant: 0.0),
backdropView.rightAnchor.constraint(equalTo: vc.view.rightAnchor, constant: 0.0),
backdropView.bottomAnchor.constraint(equalTo: vc.view.bottomAnchor, constant: 0.0),
]
fixedConstraints = surfaceConstraints + backdropConstraints
// Flexible surface constarints for full, half, tip and off
fullConstraints = [
surfaceView.topAnchor.constraint(equalTo: parent.layoutGuide.topAnchor,
constant: fullInset),
]
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,
constant: fullInset),
]
}
halfConstraints = [
surfaceView.topAnchor.constraint(equalTo: parent.layoutGuide.bottomAnchor,
surfaceView.topAnchor.constraint(equalTo: vc.layoutGuide.bottomAnchor,
constant: -halfInset),
]
tipConstraints = [
surfaceView.topAnchor.constraint(equalTo: parent.layoutGuide.bottomAnchor,
surfaceView.topAnchor.constraint(equalTo: vc.layoutGuide.bottomAnchor,
constant: -tipInset),
]
offConstraints = [
surfaceView.topAnchor.constraint(equalTo: parent.view.bottomAnchor, constant: 0.0),
surfaceView.topAnchor.constraint(equalTo: vc.view.bottomAnchor,
constant: -hiddenInset),
]
}
// The method is separated from prepareLayout(to:) for the rotation support
// It must be called in FloatingPanelController.traitCollectionDidChange(_:)
func updateHeight() {
defer {
UIView.performWithoutAnimation {
surfaceView.superview!.layoutIfNeeded()
}
}
guard let vc = vc else { return }
NSLayoutConstraint.deactivate(heightConstraints)
// Must use the parent height, not the screen height because safe area insets
// of the parent are relative values. For example, a view controller in
// Navigation controller's safe area insets and frame can be changed whether
// the navigation bar is translucent or not.
let height = self.parent.view.bounds.height - (safeAreaInsets.top + fullInset)
heightConstraints = [
surfaceView.heightAnchor.constraint(equalToConstant: height)
]
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),
]
default:
heightConstraints = [
surfaceView.heightAnchor.constraint(equalTo: vc.view.heightAnchor,
constant: -(safeAreaInsets.top + fullInset)),
]
}
NSLayoutConstraint.activate(heightConstraints)
surfaceView.set(bottomOverflow: heightBuffer)
surfaceView.bottomOverflow = vc.view.bounds.height + layout.topInteractionBuffer
if layout is FloatingPanelIntrinsicLayout {
NSLayoutConstraint.deactivate(fullConstraints)
fullConstraints = [
surfaceView.topAnchor.constraint(equalTo: vc.layoutGuide.bottomAnchor,
constant: -fullInset),
]
}
}
func activateLayout(of state: FloatingPanelPosition?) {
func activateLayout(of state: FloatingPanelPosition) {
defer {
surfaceView.superview!.layoutIfNeeded()
}
var state = state
setBackdropAlpha(of: state)
NSLayoutConstraint.activate(fixedConstraints)
guard var state = state else {
NSLayoutConstraint.deactivate(fullConstraints + halfConstraints + tipConstraints)
NSLayoutConstraint.activate(offConstraints)
return
}
if layout.supportedPositions.contains(state) == false {
if supportedPositions.union([.hidden]).contains(state) == false {
state = layout.initialPosition
}
NSLayoutConstraint.deactivate(fullConstraints + halfConstraints + tipConstraints + offConstraints)
switch state {
case .full:
NSLayoutConstraint.deactivate(halfConstraints + tipConstraints + offConstraints)
NSLayoutConstraint.activate(fullConstraints)
case .half:
NSLayoutConstraint.deactivate(fullConstraints + tipConstraints + offConstraints)
NSLayoutConstraint.activate(halfConstraints)
case .tip:
NSLayoutConstraint.deactivate(fullConstraints + halfConstraints + offConstraints)
NSLayoutConstraint.activate(tipConstraints)
case .hidden:
NSLayoutConstraint.activate(offConstraints)
}
}
func setBackdropAlpha(of target: FloatingPanelPosition?) {
if let target = target {
self.backdropView.alpha = layout.backdropAlphaFor(position: target)
} else {
func setBackdropAlpha(of target: FloatingPanelPosition) {
if target == .hidden {
self.backdropView.alpha = 0.0
} else {
self.backdropView.alpha = layout.backdropAlphaFor(position: target)
}
}
func checkLayoutConsistance() {
private func checkLayoutConsistance() {
// Verify layout configurations
let supportedPositions = layout.supportedPositions
assert(supportedPositions.count > 0)
assert(supportedPositions.contains(layout.initialPosition),
"Does not include an initial potision(\(layout.initialPosition)) in supportedPositions(\(supportedPositions))")
supportedPositions.forEach { pos in
assert(layout.insetFor(position: pos) != nil,
"Undefined an inset for a pos(\(pos))")
if layout is FloatingPanelIntrinsicLayout {
assert(layout.insetFor(position: .full) == nil, "Return `nil` for full position on FloatingPanelIntrinsicLayout")
}
if halfInset > 0 {
assert(halfInset > tipInset, "Invalid half and tip insets")
}
if fullInset > 0 {
assert(middleY > topY, "Invalid insets")
assert(bottomY > topY, "Invalid insets")
}
// The verification isn't working on orientation change(portrait -> landscape)
// of a floating panel in tab bar. Because the `safeAreaInsets.bottom` is
// updated in delay so that it can be 83.0(not 53.0) even after the surface
// and the super view's frame is fit to landscape already.
/*if fullInset > 0 {
assert(middleY > topY, "Invalid insets { topY: \(topY), middleY: \(middleY) }")
assert(bottomY > topY, "Invalid insets { topY: \(topY), bottomY: \(bottomY) }")
}*/
}
}
@@ -10,7 +10,10 @@ 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
/// 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!
/// The height of the grabber bar area
@@ -18,11 +21,11 @@ public class FloatingPanelSurfaceView: UIView {
return Default.grabberTopPadding * 2 + GrabberHandleView.Default.height // 17.0
}
/// A UIView object that can have the surface view added to it.
public var contentView: UIView!
/// A root view of a content view controller
public weak var contentView: UIView!
private var color: UIColor? = .white { didSet { setNeedsLayout() } }
private var bottomOverflow: CGFloat = 0.0 // Must not call setNeedsLayout()
var bottomOverflow: CGFloat = 0.0 // Must not call setNeedsLayout()
public override var backgroundColor: UIColor? {
get { return color }
@@ -67,7 +70,7 @@ public class FloatingPanelSurfaceView: UIView {
render()
}
required init?(coder aDecoder: NSCoder) {
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
render()
}
@@ -80,18 +83,6 @@ public class FloatingPanelSurfaceView: UIView {
layer.insertSublayer(backgroundLayer, at: 0)
self.backgroundLayer = backgroundLayer
let contentView = FloatingPanelSurfaceContentView()
addSubview(contentView)
self.contentView = contentView as UIView
contentView.backgroundColor = color
contentView.translatesAutoresizingMaskIntoConstraints = false
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),
])
let grabberHandle = GrabberHandleView()
addSubview(grabberHandle)
self.grabberHandle = grabberHandle
@@ -107,13 +98,14 @@ public class FloatingPanelSurfaceView: UIView {
public override func layoutSubviews() {
super.layoutSubviews()
log.debug("SurfaceView frame", frame)
updateLayers()
updateContentViewMask()
contentView.layer.borderColor = borderColor?.cgColor
contentView.layer.borderWidth = borderWidth
contentView.backgroundColor = color
contentView?.layer.borderColor = borderColor?.cgColor
contentView?.layer.borderWidth = borderWidth
contentView?.frame = bounds
}
private func updateLayers() {
@@ -147,29 +139,23 @@ public class FloatingPanelSurfaceView: UIView {
byRoundingCorners: [.topLeft, .topRight],
cornerRadii: CGSize(width: cornerRadius, height: cornerRadius))
maskLayer.path = path.cgPath
contentView.layer.mask = maskLayer
contentView?.layer.mask = maskLayer
} 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.
}
}
func set(bottomOverflow: CGFloat) {
self.bottomOverflow = bottomOverflow
updateLayers()
updateContentViewMask()
}
func add(childView: UIView) {
contentView.addSubview(childView)
childView.frame = contentView.bounds
childView.translatesAutoresizingMaskIntoConstraints = false
func add(contentView: UIView) {
insertSubview(contentView, belowSubview: grabberHandle)
self.contentView = contentView
/* contentView.frame = bounds */ // MUST NOT: Because the top safe area inset of a content VC will be incorrect.
contentView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
childView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 0.0),
childView.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 0.0),
childView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: 0.0),
childView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: 0.0),
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),
])
}
}
@@ -0,0 +1,124 @@
//
// Created by Shin Yamamoto on 2018/11/21.
// Copyright © 2018 Shin Yamamoto. All rights reserved.
//
import UIKit
class FloatingPanelModalTransition: NSObject, UIViewControllerTransitioningDelegate {
func animationController(forPresented presented: UIViewController,
presenting: UIViewController,
source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return FloatingPanelModalPresentTransition()
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return FloatingPanelModalDismissTransition()
}
func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
return FloatingPanelPresentationController(presentedViewController: presented, presenting: presenting)
}
}
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
addFloatingPanel()
}
override func presentationTransitionDidEnd(_ completed: Bool) {
// For non-animated presentation
if let fpc = presentedViewController as? FloatingPanelController, fpc.position == .hidden {
fpc.show(animated: false, completion: nil)
}
}
override func dismissalTransitionDidEnd(_ completed: Bool) {
if let fpc = presentedViewController as? FloatingPanelController {
// For non-animated dismissal
if fpc.position != .hidden {
fpc.hide(animated: false, completion: nil)
}
fpc.view.removeFromSuperview()
}
}
override func containerViewWillLayoutSubviews() {
guard
let fpc = presentedViewController as? FloatingPanelController
else { fatalError() }
/*
* Layout the views managed by `FloatingPanelController` here for the
* sake of the presentation and disimissal 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
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleBackdrop(tapGesture:)))
fpc.backdropView.addGestureRecognizer(tapGesture)
}
@objc func handleBackdrop(tapGesture: UITapGestureRecognizer) {
presentedViewController.dismiss(animated: true, completion: nil)
}
private func addFloatingPanel() {
guard
let containerView = self.containerView,
let fpc = presentedViewController as? FloatingPanelController
else { fatalError() }
containerView.addSubview(fpc.view)
fpc.view.frame = containerView.bounds
fpc.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
}
}
class FloatingPanelModalPresentTransition: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
guard
let fpc = transitionContext?.viewController(forKey: .to) as? FloatingPanelController
else { fatalError()}
let animator = fpc.behavior.addAnimator(fpc, to: fpc.layout.initialPosition)
return TimeInterval(animator.duration)
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard
let fpc = transitionContext.viewController(forKey: .to) as? FloatingPanelController
else { fatalError() }
fpc.show(animated: true) {
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
}
}
class FloatingPanelModalDismissTransition: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
guard
let fpc = transitionContext?.viewController(forKey: .from) as? FloatingPanelController
else { fatalError()}
let animator = fpc.behavior.removeAnimator(fpc, from: fpc.position)
return TimeInterval(animator.duration)
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard
let fpc = transitionContext.viewController(forKey: .from) as? FloatingPanelController
else { fatalError() }
fpc.hide(animated: true) {
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
}
}
+19
View File
@@ -0,0 +1,19 @@
//
// Created by Shin Yamamoto on 2018/11/21.
// Copyright © 2018 Shin Yamamoto. All rights reserved.
//
import UIKit
class FloatingPanelPassThroughView: UIView {
public weak var eventForwardingView: UIView?
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let hitView = super.hitTest(point, with: event)
switch hitView {
case self:
return eventForwardingView?.hitTest(self.convert(point, to: eventForwardingView), with: event)
default:
return hitView
}
}
}
+1 -1
View File
@@ -12,7 +12,7 @@ public class GrabberHandleView: UIView {
public static let barColor = UIColor(displayP3Red: 0.76, green: 0.77, blue: 0.76, alpha: 1.0)
}
required init?(coder aDecoder: NSCoder) {
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
render()
}
+13 -1
View File
@@ -62,7 +62,19 @@ extension UIView {
}
}
extension UIGestureRecognizer.State: CustomDebugStringConvertible {
extension UIView {
func disableAutoLayout() {
let frame = self.frame
translatesAutoresizingMaskIntoConstraints = true
self.frame = frame
}
func enableAutoLayout() {
translatesAutoresizingMaskIntoConstraints = false
}
}
extension UIGestureRecognizerState: CustomDebugStringConvertible {
public var debugDescription: String {
switch self {
case .began: return "Began"
+167 -51
View File
@@ -2,6 +2,7 @@
[![Version](https://img.shields.io/cocoapods/v/FloatingPanel.svg)](https://cocoapods.org/pods/FloatingPanel)
[![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage)
[![Platform](https://img.shields.io/cocoapods/p/FloatingPanel.svg)](https://cocoapods.org/pods/FloatingPanel)
[![Swift 4.1](https://img.shields.io/badge/Swift-4.1-orange.svg?style=flat)](https://swift.org/)
[![Swift 4.2](https://img.shields.io/badge/Swift-4.2-orange.svg?style=flat)](https://swift.org/)
# FloatingPanel
@@ -23,15 +24,22 @@ The new interface displays the related contents and utilities in parallel as a u
- [CocoaPods](#cocoapods)
- [Carthage](#carthage)
- [Getting Started](#getting-started)
- [Add a floating panel as a child view controller](#add-a-floating-panel-as-a-child-view-controller)
- [Present a floating panel as a modality](#present-a-floating-panel-as-a-modality)
- [View hierarchy](#view-hierarchy)
- [Usage](#usage)
- [Customize the layout of a floating panel with `FloatingPanelLayout` protocol](#customize-the-layout-of-a-floating-panel-with--floatingpanellayout-protocol)
- [Show/Hide a floating panel in a view with your view hierarchy](#showhide-a-floating-panel-in-a-view-with-your-view-hierarchy)
- [Customize the layout with `FloatingPanelLayout` protocol](#customize-the-layout-with-floatingpanellayout-protocol)
- [Change the initial position and height](#change-the-initial-position-and-height)
- [Support your landscape layout](#support-your-landscape-layout)
- [Use Intrinsic height layout](#use-intrinsic-height-layout)
- [Customize the behavior with `FloatingPanelBehavior` protocol](#customize-the-behavior-with-floatingpanelbehavior-protocol)
- [Modify your floating panel's interaction](#modify-your-floating-panels-interaction)
- [Use a custom grabber handle](#use-a-custom-grabber-handle)
- [Add tap gestures to the surface or backdrop views](#add-tap-gestures-to-the-surface-or-backdrop-views)
- [Create an additional floating panel for a detail](#create-an-additional-floating-panel-for-a-detail)
- [Move a position with an animation](#move-a-position-with-an-animation)
- [Make your contents correspond with a floating panel behavior](#make-your-contents-correspond-with-a-floating-panel-behavior)
- [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)
- [FloatingPanelSurfaceView's issue on iOS 10](#floatingpanelsurfaceviews-issue-on-ios-10)
@@ -50,6 +58,7 @@ The new interface displays the related contents and utilities in parallel as a u
- [x] Layout customization for all trait environments(i.e. Landscape orientation support)
- [x] Behavior customization
- [x] Free from common issues of Auto Layout and gesture handling
- [x] Modal presentation
Examples are here.
@@ -58,7 +67,7 @@ Examples are here.
## Requirements
FloatingPanel is written in Swift 4.2. Compatible with iOS 10.0+
FloatingPanel is written in Swift. It can be built by Xcode 9.4.1 or later. Compatible with iOS 10.0+.
## Installation
@@ -79,9 +88,10 @@ For [Carthage](https://github.com/Carthage/Carthage), add the following to your
github "scenee/FloatingPanel"
```
## Getting Started
### Add a floating panel as a child view controller
```swift
import UIKit
import FloatingPanel
@@ -110,16 +120,82 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate {
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
// Remove the views managed by the `FloatingPanelController` object from self.view.
fpc.removePanelFromParent()
}
...
}
```
### Present a floating panel as a modality
```swift
let fpc = FloatingPanelController()
let contentVC = ...
fpc.set(contentViewController: contentVC)
fpc.isRemovalInteractionEnabled = true // Optional: Let it removable by a swipe-down
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).
## View hierarchy
`FloatingPanelController` manages the views as the following view hierarchy.
```
FloatingPanelController.view (FloatingPanelPassThroughView)
├─ .backdropView (FloatingPanelBackdropView)
└─ .surfaceView (FloatingPanelSurfaceView)
├─ .contentView == FloatingPanelController.contentViewController.view
└─ .grabberHandle (GrabberHandleView)
```
## Usage
### Customize the layout of a floating panel with `FloatingPanelLayout` protocol
### Show/Hide a floating panel in a view with your view hierarchy
```swift
// Add the controller and the managed views to a view controller.
// From the second time, just call `show(animated:completion)`.
view.addSubview(fpc.view)
fpc.view.frame = view.bounds // MUST
// In addition, Auto Layout constraints are highly recommended.
// Because it makes the layout more robust on trait collection change.
//
// fpc.view.translatesAutoresizingMaskIntoConstraints = false
// NSLayoutConstraint.activate([...])
//
parent.addChild(fpc)
// Show a floating panel to the initial position defined in your `FloatingPanelLayout` object.
fpc.show(animated: true) {
// Only for the first time
self.didMove(toParent: self)
}
...
// Hide it
fpc.hide(animated: true) {
// Remove it if needed
self.willMove(toParent: nil)
self.view.removeFromSuperview()
self.removeFromParent()
}
```
NOTE: `FloatingPanelController` wraps `show`/`hide` with `addPanel`/`removePanelFromParent` for easy-to-use. But `show`/`hide` are more convenience for your app.
### Customize the layout with `FloatingPanelLayout` protocol
#### Change the initial position and height
@@ -129,7 +205,6 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate {
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? {
return MyFloatingPanelLayout()
}
...
}
class MyFloatingPanelLayout: FloatingPanelLayout {
@@ -142,6 +217,7 @@ class MyFloatingPanelLayout: FloatingPanelLayout {
case .full: return 16.0 // A top inset from safe area
case .half: return 216.0 // A bottom inset from the safe area
case .tip: return 44.0 // A bottom inset from the safe area
default: return nil // Or `case .hidden: return nil`
}
}
}
@@ -155,7 +231,6 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate {
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? {
return (newCollection.verticalSizeClass == .compact) ? FloatingPanelLandscapeLayout() : nil // Returning nil indicates to use the default layout
}
...
}
class FloatingPanelLandscapeLayout: FloatingPanelLayout {
@@ -183,6 +258,33 @@ class FloatingPanelLandscapeLayout: FloatingPanelLayout {
}
```
#### Use Intrinsic height layout
1. Lay out your content View with the intrinsic height size. For example, see "Detail View Controller scene"/"Intrinsic View Controller scene" of [Main.storyboard](https://github.com/SCENEE/FloatingPanel/blob/master/Examples/Samples/Sources/Base.lproj/Main.storyboard). The 'Stack View.bottom' constraint determines the intrinsic height.
2. Create a layout that adopts and conforms to `FloatingPanelIntrinsicLayout` and use it.
```swift
class ViewController: UIViewController, FloatingPanelControllerDelegate {
...
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? {
return RemovablePanelLayout()
}
}
class RemovablePanelLayout: FloatingPanelIntrinsicLayout {
var supportedPositions: Set<FloatingPanelPosition> {
return [.full, .half]
}
func insetFor(position: FloatingPanelPosition) -> CGFloat? {
switch position {
case .half: return 130.0
default: return nil // Must return nil for .full
}
}
...
}
```
### Customize the behavior with `FloatingPanelBehavior` protocol
#### Modify your floating panel's interaction
@@ -193,51 +295,67 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate {
func floatingPanel(_ vc: FloatingPanelController, behaviorFor newCollection: UITraitCollection) -> FloatingPanelBehavior? {
return FloatingPanelStocksBehavior()
}
...
}
...
class FloatingPanelStocksBehavior: FloatingPanelBehavior {
var velocityThreshold: CGFloat {
return 15.0
}
...
func interactionAnimator(_ fpc: FloatingPanelController, to targetPosition: FloatingPanelPosition, with velocity: CGVector) -> UIViewPropertyAnimator {
let damping = self.damping(with: velocity)
let springTiming = UISpringTimingParameters(dampingRatio: damping, initialVelocity: velocity)
return UIViewPropertyAnimator(duration: 0.5, timingParameters: springTiming)
}
}
```
### Use a custom grabber handle
```swift
let myGrabberHandleView = MyGrabberHandleView()
fpc.surfaceView.grabberHandle.isHidden = true
fpc.surfaceView.addSubview(myGrabberHandleView)
```
### Add tap gestures to the surface or backdrop views
```swift
override func viewDidLoad() {
...
surfaceTapGesture = UITapGestureRecognizer(target: self, action: #selector(handleSurface(tapGesture:)))
fpc.surfaceView.addGestureRecognizer(surfaceTapGesture)
backdropTapGesture = UITapGestureRecognizer(target: self, action: #selector(handleBackdrop(tapGesture:)))
fpc.backdropView.addGestureRecognizer(backdropTapGesture)
surfaceTapGesture.isEnabled = (fpc.position == .tip)
}
// Enable `surfaceTapGesture` only at `tip` position
func floatingPanelDidChangePosition(_ vc: FloatingPanelController) {
surfaceTapGesture.isEnabled = (vc.position == .tip)
}
```
### Create an additional floating panel for a detail
```swift
class ViewController: UIViewController, FloatingPanelControllerDelegate {
var searchPanelVC: FloatingPanelController!
var detailPanelVC: FloatingPanelController!
override func viewDidLoad() {
// Setup Search panel
self.searchPanelVC = FloatingPanelController()
override func viewDidLoad() {
// Setup Search panel
self.searchPanelVC = FloatingPanelController()
let searchVC = SearchViewController()
self.searchPanelVC.set(contentViewController: searchVC)
self.searchPanelVC.track(scrollView: contentVC.tableView)
let searchVC = SearchViewController()
self.searchPanelVC.set(contentViewController: searchVC)
self.searchPanelVC.track(scrollView: contentVC.tableView)
self.searchPanelVC.addPanel(toParent: self)
self.searchPanelVC.addPanel(toParent: self)
// Setup Detail panel
self.detailPanelVC = FloatingPanelController()
// Setup Detail panel
self.detailPanelVC = FloatingPanelController()
let contentVC = ContentViewController()
self.detailPanelVC.set(contentViewController: contentVC)
self.detailPanelVC.track(scrollView: contentVC.scrollView)
let contentVC = ContentViewController()
self.detailPanelVC.set(contentViewController: contentVC)
self.detailPanelVC.track(scrollView: contentVC.scrollView)
self.detailPanelVC.addPanel(toParent: self)
}
...
self.detailPanelVC.addPanel(toParent: self)
}
```
@@ -246,18 +364,18 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate {
In the following example, I move a floating panel to full or half position while opening or closing a search bar like Apple Maps.
```swift
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
...
fpc.move(to: .half, animated: true)
}
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
...
fpc.move(to: .half, animated: true)
}
func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
...
fpc.move(to: .full, animated: true)
}
func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
...
fpc.move(to: .full, animated: true)
}
```
### Make your contents correspond with a floating panel behavior
### Work your contents together with a floating panel behavior
```swift
class ViewController: UIViewController, FloatingPanelControllerDelegate {
@@ -274,7 +392,6 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate {
searchVC.hideHeader()
}
}
...
}
```
@@ -282,7 +399,7 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate {
### 'Show' or 'Show Detail' Segues from `FloatingPanelController`'s content view controller
'Show' or 'Show Detail' segues from a content view controller will be managed by a view controller(hereinafter called 'master VC') adding a floating panel. Because a floating panel is just a subview of the master VC.
'Show' or 'Show Detail' segues from a content view controller will be managed by a view controller(hereinafter called 'master VC') adding a floating panel. Because a floating panel is just a subview of the master VC(except for modality).
`FloatingPanelController` has no way to manage a stack of view controllers like `UINavigationController`. If so, it would be so complicated and the interface will become `UINavigationController`. This component should not have the responsibility to manage the stack.
@@ -305,17 +422,16 @@ class ViewController: UIViewController {
secondFpc.addPanel(toParent: self)
}
...
}
```
A `FloatingPanelController` object proxies an action for `show(_:sender)` to the master VC. That's why the master VC can handle a destination view controller of a 'Show' or 'Show Detail' segue and you can hook `show(_:sender)` to show a secondally floating panel set the destination view controller to the content.
A `FloatingPanelController` object proxies an action for `show(_:sender)` to the master VC. That's why the master VC can handle a destination view controller of a 'Show' or 'Show Detail' segue and you can hook `show(_:sender)` to show a secondary floating panel set the destination view controller to the content.
It's a greate way to decouple between a floating panel and the content VC.
It's a great way to decouple between a floating panel and the content VC.
### FloatingPanelSurfaceView's issue on iOS 10
### 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.
* 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.
So you need to draw top rounding corners of your content. Here is an example in Examples/Maps.
```swift
override func viewDidLayoutSubviews() {
@@ -326,11 +442,11 @@ override func viewDidLayoutSubviews() {
}
}
```
* If you sets clear color to `FloatingPanelSurfaceView.backgroundColor`, please note the bottom overflow of your content on bouncing at full position. To prevent it, you need to expand your content. For example, See Example/Maps's Auto Layout settings of UIVisualEffectView in Main.storyborad.
* If you sets clear color to `FloatingPanelSurfaceView.backgroundColor`, please note the bottom overflow of your content on bouncing at full position. To prevent it, you need to expand your content. For example, See Example/Maps App's Auto Layout settings of `UIVisualEffectView` in Main.storyboard.
## Author
Shin Yamamoto <shin@scenee.com>
Shin Yamamoto <shin@scenee.com> | [@scenee](https://twitter.com/scenee)
## License