Compare commits

...

143 Commits

Author SHA1 Message Date
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 c15d236320 Release v1.2.0 2018-11-17 09:40:34 +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
Shin Yamamoto eaf0ebe62b Merge pull request #52 from futuretap/master
Fixed infinite recursion in FloatingPanel.responds(to:)
2018-11-16 10:25:34 +09:00
Ortwin Gentz a4a08662be Fixed infinite recursion in FloatingPanel.responds(to:)
if track(scrollView:) is called twice
2018-11-15 22:11:40 +01:00
Shin Yamamoto e8a0ffeca5 Merge pull request #48 from SCENEE/fix-scrolling-short-contents
Fix scrolling short contents
2018-11-16 00:19:41 +09:00
Shin Yamamoto 1399cc6fbd Fix a layout bug on orientation changed 2018-11-15 10:33:46 +09:00
Shin Yamamoto 8b44ad4b08 Fix unexpected bounciness on short contents
This issue happens in dragging from half to full position.
2018-11-15 10:33:46 +09:00
Shin Yamamoto e4a1a6e293 Improve Scroll tracking(TableView) sample 2018-11-15 10:33:46 +09:00
Shin Yamamoto ba00786b91 Merge pull request #51 from futuretap/improved-shadow
Improved shadow, not darkening content background
2018-11-15 09:33:55 +09:00
Ortwin Gentz 5e7529d1e6 Improved shadow, not darkening content background
By removing shadowPath and putting the shadow directly onto the surface view layer instead of a sublayer, the shadow doesn't affect the content view background
2018-11-14 15:33:38 +01:00
Shin Yamamoto e75108113a Fix comment 2018-11-14 20:16:49 +09:00
Shin Yamamoto 6d51b0d420 Fix failure requirements for scroll view's gesture recognizers 2018-11-14 11:50:41 +09:00
Shin Yamamoto c3b199755e Add short contents scroll sample 2018-11-14 11:50:41 +09:00
Shin Yamamoto d57d8e9da5 Fix scrolling contents shorter than scroll view size 2018-11-14 11:38:23 +09:00
Shin Yamamoto d24e1c5355 Prevent unexpected assertion failure
The assertion always fails on orientation change from landspace to
portrait with the default layouts.
2018-11-14 11:18:54 +09:00
Shin Yamamoto 95c94560be Merge pull request #44 from SCENEE/fix-scroll-indicator-hidden
Fix scroll indicator hidden
2018-11-14 07:48:50 +09:00
Shin Yamamoto 7b4ed52eb1 Add comments and fix typos 2018-11-12 13:35:36 +09:00
Shin Yamamoto cab8c15474 Fix scroll view indicators hidden at the top once 2018-11-12 13:35:36 +09:00
Shin Yamamoto 72f5d59a75 Merge pull request #43 from SCENEE/add-update-layout-api
Add FloatingPanelController.updateLayout()
2018-11-12 13:33:25 +09:00
Shin Yamamoto 273adc8d1b Fix FloatingPanel's initial state value 2018-11-12 13:03:54 +09:00
Shin Yamamoto 5f0f28cb0e Add update layout sample 2018-11-12 13:03:54 +09:00
Shin Yamamoto e1c9fe120b Add FloatingPanelController.updateLayout() 2018-11-12 13:03:54 +09:00
Shin Yamamoto 630580beb6 Merge pull request #38 from SCENEE/fix-scroll-view-bounce
Fix scroll top bounce bugs
2018-11-09 10:20:35 +09:00
Shin Yamamoto 68a2c43580 Merge pull request #37 from SCENEE/fix-orientation-change-on-removal
Fix orientation change on removal
2018-11-09 10:20:20 +09:00
Shin Yamamoto 817fce6d10 Fix scroll top bounce bugs
Disable scroll top bouncing if a user scroll down contents(no
deceleration) and the scroll offset Y is less than 10.0, instead
of the velocity condition(greater than 2500.0).

This change prevents potential bugs on scroll bouncing so that the
scroll view tracking is more robust.
2018-11-08 11:32:54 +09:00
Shin Yamamoto 6e85afaee6 Update checkLayoutConsistance 2018-11-08 09:52:04 +09:00
Shin Yamamoto 98c5096f67 Merge pull request #36 from SCENEE/fix-scroll-jump-after-scroll-animation
Fix scrollview jumps after it moved programmatically
2018-11-08 09:28:17 +09:00
Shin Yamamoto 57c7ced59d Add RemovablePanelLandscapeLayout 2018-11-07 13:38:35 +09:00
Shin Yamamoto 73e6d38344 Introduce FloatingPanelBehavior.redirectionalProgress(_:from:to:) 2018-11-07 13:38:35 +09:00
Shin Yamamoto bd128bf8b0 Fix removal interaction 2018-11-07 13:11:38 +09:00
Shin Yamamoto 16e8808ce5 Modify the backdrop default behavior 2018-11-07 11:18:05 +09:00
Shin Yamamoto f4088fcb6b Fix backdrop handling 2018-11-07 11:06:18 +09:00
Shin Yamamoto 63b8aa24e8 Improve removal interaction impl 2018-11-07 10:54:28 +09:00
Shin Yamamoto 5744491606 Fix the surface height on orientation change 2018-11-07 10:45:32 +09:00
Shin Yamamoto f5ecbef724 Add 'Animate Scroll' button in Samples app 2018-11-07 10:44:35 +09:00
Shin Yamamoto f176a2c70e Fix scrollview jumps after it moved programmatically 2018-11-07 09:49:00 +09:00
Shin Yamamoto 51c124d3e4 Merge pull request #33 from SCENEE/fix-unexpected-stop-deleration
Reset 'stopScrollDeceleration' on animation finish
2018-11-07 09:19:16 +09:00
Shin Yamamoto 5f6c97336e Merge pull request #34 from ffittschen/fix-moving-without-scroll-top
Fix moving floating panel without scrolling to top of scroll view
2018-11-07 09:18:59 +09:00
Florian Fittschen 894eb77d5d Revert scrollview offset changes 2018-11-06 10:01:57 +01:00
Shin Yamamoto d1b5a1f517 Merge pull request #29 from SCENEE/fix-navigation-bar-problem
Fix navigation bar problem
2018-11-06 10:10:45 +09:00
Shin Yamamoto 300d5f8d91 Reset 'stopScrollDeceleration' on animation finish 2018-11-06 10:04:41 +09:00
Shin Yamamoto bbc885f783 Merge pull request #32 from SCENEE/fix-moving-without-scroll-top
Fix a bug in moving interaction from full position
2018-11-06 09:19:18 +09:00
Florian Fittschen 6badeeebe5 Fix moving without scrolling to top 2018-11-05 11:15:54 +01:00
Shin Yamamoto e282806422 Merge pull request #31 from SCENEE/fix-removal-interaction
Fix the removal interaction
2018-11-05 18:22:08 +09:00
Shin Yamamoto 629807584b Fix the removal interaction 2018-11-05 08:50:39 +09:00
Shin Yamamoto 922c0e53d2 Fix a bug in moving interaction from full position
The floating panel must work well if the tracking scroll view's
content offset isn't the top.
2018-11-04 11:55:30 +09:00
Shin Yamamoto ec9fcd473a Fix the surface view height on non traslucent nav bar 2018-11-02 19:51:06 +09:00
Shin Yamamoto de9f415ded Observe parent's view.safeAreaInsets 2018-11-02 17:00:34 +09:00
Shin Yamamoto fd5ca2c2fc Clean up FloatingPanelSurfaceView and FloatingPanelLayout 2018-11-02 16:19:53 +09:00
Shin Yamamoto c0b9ddc4a3 Revert a sample code in README to prevent a confusion for v1.1.0 2018-11-02 12:19:49 +09:00
Shin Yamamoto 43f33083f1 Fix README 2018-11-02 12:17:44 +09:00
Shin Yamamoto 2b483e6adb Merge pull request #21 from SCENEE/fix-show-segue-problem
Fix show segue problem
2018-11-02 12:12:50 +09:00
Shin Yamamoto 0cf0f42ca4 Replace FloatingPanelController.show(_:sender:) with the set(contentViewController:)
And add a sample code to test show(_:sender:) in ContentVC.

`FloatingPanelController.show(_:sender:)` can block 'Show' segue in a
content view controller. 'Show' segue should not be handled by
'FloatingPanelController`. So I replace this method.
2018-11-02 10:33:12 +09:00
Shin Yamamoto c9ccea3f84 Merge pull request #27 from SCENEE/fix-backdrop-bugs
Fix backdrop bugs
2018-11-02 10:29:27 +09:00
Shin Yamamoto c2dee28132 Improve FloatingPanelSurfaceView.bottomOverflow handling 2018-11-02 10:07:49 +09:00
Shin Yamamoto 4fd4709182 Fix FloatingPanelSurfaceView.backgroundColor 2018-11-02 10:07:49 +09:00
Shin Yamamoto 00ccc0eb6a Fix backdrop alpha issue 2018-11-02 10:07:49 +09:00
Shin Yamamoto ed91f51482 Merge pull request #25 from SCENEE/add-removable-interaction
Add a removable interaction
2018-11-02 09:42:57 +09:00
Shin Yamamoto c2cea95aa5 Clean up FloatingPanelLayout consistance check 2018-11-02 09:17:58 +09:00
Shin Yamamoto 274027cb64 Allow one supported position 2018-11-02 09:17:58 +09:00
Shin Yamamoto b4a26344d9 Add a removable interaction 2018-11-02 09:17:58 +09:00
Shin Yamamoto 580c708788 Merge pull request #24 from SCENEE/add-preconditions-of-parent
Add and Update preconditions of the parent VC
2018-11-01 12:20:50 +09:00
Shin Yamamoto f4d6380094 Add and Update preconditions of the parent VC 2018-11-01 11:19:32 +09:00
Shin Yamamoto e44dc06a61 Merge pull request #18 from kingcos/master
Add missing constraints of the title
2018-10-29 22:09:59 +09:00
kingcos b6184f5b41 Add missing constraint of the title 2018-10-29 11:16:59 +08:00
Shin Yamamoto e6fc2f397e Merge pull request #17 from SCENEE/release-1.1.0
Release v1.1.0
2018-10-29 08:38:12 +09:00
Shin Yamamoto 60d0b62675 Release v1.1.0 2018-10-28 09:00:50 +09:00
Shin Yamamoto 7a8eb1833f Merge pull request #14 from SCENEE/improve-public-interface
Improve public interface
2018-10-28 08:59:55 +09:00
Shin Yamamoto 6367b76b9d Fix failure requirements of the pan gesture 2018-10-28 08:36:31 +09:00
Shin Yamamoto 977b685071 Modify a custom landscape layout for Maps 2018-10-28 08:36:31 +09:00
Shin Yamamoto b97d418158 Change the default landscape layout 2018-10-28 08:36:31 +09:00
Shin Yamamoto 0e4cb372d5 Add a sample code to test FloatingPanelController.move(to:animated:) 2018-10-28 08:36:31 +09:00
Shin Yamamoto 2ec7576ae9 Improve FloatingPanelBehavior protocol
present/dismiss words should be used for modality.
add/remove words are appropriate for them.
2018-10-28 08:36:31 +09:00
Shin Yamamoto c4bf4c3067 Open the pan gesture recognizer of FloatingPanelController 2018-10-28 06:41:37 +09:00
Shin Yamamoto ea9bbcad27 Check consistance of FloatingPanelLayout 2018-10-28 06:41:37 +09:00
Shin Yamamoto 71be1f2ed5 Change the type of 'supportedPositions' from Array to Set 2018-10-28 06:41:37 +09:00
Shin Yamamoto 349bb91c6c Improve the default impls of FloatingPanelLayout methods 2018-10-28 06:41:37 +09:00
Shin Yamamoto 52da673358 Fix README 2018-10-28 06:41:37 +09:00
Shin Yamamoto ce0b9d1413 Merge pull request #16 from 0xflotus/patch-1
fixed some errors
2018-10-28 06:34:55 +09:00
0xflotus 8ef332f3e5 fixed some errors 2018-10-27 21:23:06 +02:00
Shin Yamamoto a4002f83c1 Merge pull request #13 from SCENEE/fix-gesture-handling
Fix untracked scroll view's freezing in a floating panel
2018-10-27 20:49:56 +09:00
Shin Yamamoto 64d756d8a9 Add a nested scroll view's sample 2018-10-27 16:39:22 +09:00
Shin Yamamoto 187fe47268 Fix untracked scroll view's freezing in a floating panel 2018-10-27 15:16:08 +09:00
Shin Yamamoto 060f3a0b1b Merge pull request #11 from futuretap/master
Fixed some typos and language in comments
2018-10-27 09:55:53 +09:00
Ortwin Gentz 4dcc5bc564 Fixed some typos and language in comments 2018-10-26 15:30:11 +02:00
Shin Yamamoto 52efac6643 Fix Usage contents in README 2018-10-26 18:39:41 +09:00
Shin Yamamoto 97c91fb7aa Merge pull request #10 from SCENEE/support-travis-ci
Support Travis CI
2018-10-26 15:22:59 +09:00
Shin Yamamoto efcc598550 Add 'Build Status' shield in README 2018-10-26 15:00:02 +09:00
Shin Yamamoto fd5fc1f485 Add travis yml 2018-10-26 15:00:02 +09:00
Shin Yamamoto f713d4057f Merge pull request #9 from SCENEE/fix-surface-view-height
Fix surface view height
2018-10-26 14:29:50 +09:00
Shin Yamamoto 4ebbea8e86 Fix FloatingPanelLayout.{topInteractionBuffer,bottomInteractionBuffer} 2018-10-26 14:27:40 +09:00
Shin Yamamoto 5515e6f788 Update README
- Add shields
- Add TOC
- Update Usage section
- Revise the contents
2018-10-26 14:27:40 +09:00
Shin Yamamoto 65a6315f1b Fix the table view height in Examples/Maps
The bottom of a scroll view tracked by a floating panel controller must align
the bottom of a screen when `FloatingPanelController.contentInsetAdjustmentBehavior`
is set to `always`.
2018-10-26 14:27:40 +09:00
Shin Yamamoto aafe32bb3d Fix a critical bug on 2(full and half) anchor positions 2018-10-26 14:27:40 +09:00
Shin Yamamoto 1c6c783dbe Add sample codes in Samples app to test a floating panel in TabBar 2018-10-26 14:27:40 +09:00
Shin Yamamoto 1e322f47d4 Update doc comment 2018-10-26 14:27:40 +09:00
Shin Yamamoto 37196abe77 Improve updating the shadow layer of the surface 2018-10-24 12:53:27 +09:00
Shin Yamamoto e476cf5ce4 Fix the initial height of DebugTableViewController 2018-10-24 12:53:27 +09:00
Shin Yamamoto dc4b1e7a90 Escape UIVisualEffectView problem on iOS10
The floating panel controller can't resolve this issue, but the
workaround is much easy in the content view controller.
So I stop auto-rounding corners in content view on iOS 10.
2018-10-24 12:53:27 +09:00
Shin Yamamoto 95d188d5f1 Match the bottoms of the surface view and a device bottom 2018-10-24 08:16:42 +09:00
Shin Yamamoto 4dd60ca855 Update Samples App 2018-10-23 14:20:39 +09:00
Shin Yamamoto 5067917295 Fix README 2018-10-23 10:22:48 +09:00
Shin Yamamoto e620ef27ee Merge pull request #2 from SCENEE/enhance-scroll-view-tracking
Enhance scroll view handling
2018-10-23 09:58:30 +09:00
Shin Yamamoto 868dc17425 Release v1.0.0 2018-10-23 00:20:10 +09:00
Shin Yamamoto 03966f356f Clean up code for safeAreaInsets update 2018-10-23 00:16:42 +09:00
Shin Yamamoto c28ab32874 Update FloatingPanelStocksBehavior 2018-10-23 00:16:42 +09:00
Shin Yamamoto 32b7ab64d5 Improve FloatingPanelBehavior and the default 2018-10-23 00:16:42 +09:00
Shin Yamamoto 79d8e1851a Prevent moving the panel in scrolling at high speed 2018-10-23 00:16:42 +09:00
Shin Yamamoto e3c1743b57 Update README 2018-10-23 00:08:13 +09:00
Shin Yamamoto ed257bf5b7 Replace my ID with my name 2018-10-23 00:08:13 +09:00
Shin Yamamoto f918b8709e Update doc comments 2018-10-23 00:08:13 +09:00
Shin Yamamoto ad46f5bd55 Modify API names to add/remove a floating panel 2018-10-21 10:19:51 +09:00
Shin Yamamoto 4e2f9bc349 Fix a backdrop bug 2018-10-21 10:19:51 +09:00
Shin Yamamoto a3e8d1587b Fix FloatingPanel.safeAreaInsets on iOS10 2018-10-21 10:19:38 +09:00
Shin Yamamoto ce556e213c Fix FloatingPanelController.removeFromParent() on iOS10 2018-10-21 10:19:38 +09:00
Shin Yamamoto 7c3581d8be Improve scroll view tracking
- Improve locking/unlocking scroll view to prevent scroll bouncing
and showing a scroll indicator unexpectedly.
- Handle scrollview.panGestureRecognizer change
- Handle scrollView.delegate intermediately.
2018-10-21 10:19:38 +09:00
Shin Yamamoto 8c53fd4869 Add Samples app 2018-10-21 00:50:04 +09:00
Shin Yamamoto 540862e95a Fix backdrop punk 2018-10-21 00:50:04 +09:00
Shin Yamamoto e08ce7fe18 Modify FloatingPanelController.removeFromParent() 2018-10-20 12:23:27 +09:00
Shin Yamamoto be2a455088 Layout the backdrop view in Auto Layout 2018-10-20 12:23:27 +09:00
Shin Yamamoto 6333dfacb1 Make FloatingPanelSurfaceView.topGrabberBarHeight public 2018-10-20 12:23:27 +09:00
Shin Yamamoto 56aa1c405c Add FloatingPanelController.adjustedContentInset 2018-10-20 12:23:27 +09:00
Shin Yamamoto 55b76a5fca Fix UIScrollView.contentOffsetZero 2018-10-18 15:08:07 +09:00
Shin Yamamoto c9453655d5 Revise README 2018-10-18 09:35:09 +09:00
Shin Yamamoto f241227e7a Fix workspace 2018-10-17 15:01:34 +09:00
Shin Yamamoto f304bf0362 Add maps-landscape.gif 2018-10-17 14:38:41 +09:00
Shin Yamamoto 7d1c12d3a6 Clean up workspace 2018-10-17 11:27:15 +09:00
40 changed files with 3720 additions and 453 deletions
+44
View File
@@ -0,0 +1,44 @@
language: swift
branches:
only:
- master
cache:
directories:
- build
- vendor
- /usr/local/Homebrew
- $HOME/Library/Caches/Homebrew
env:
global:
- LANG=en_US.UTF-8
- LC_ALL=en_US.UTF-8
skip_cleanup: true
jobs:
include:
- stage: carthage
osx_image: xcode10
before_install:
- brew update
- brew outdated carthage || brew upgrade carthage
script:
- carthage build --no-skip-current
- stage: podspec
osx_image: xcode10
script:
- pod spec lint
- stage: check Maps example
osx_image: xcode10
script:
- xcodebuild -scheme Maps -sdk iphonesimulator clean build
- stage: check Stocks example
osx_image: xcode10
script:
- xcodebuild -scheme Stocks -sdk iphonesimulator clean build
- stage: check Samples example
osx_image: xcode10
script:
- xcodebuild -scheme Samples -sdk iphonesimulator clean build
+2 -2
View File
@@ -303,7 +303,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = J3D7L9FHSS;
DEVELOPMENT_TEAM = "";
INFOPLIST_FILE = Maps/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
LD_RUNPATH_SEARCH_PATHS = (
@@ -322,7 +322,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = J3D7L9FHSS;
DEVELOPMENT_TEAM = "";
INFOPLIST_FILE = Maps/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
LD_RUNPATH_SEARCH_PATHS = (
+1 -1
View File
@@ -3,7 +3,7 @@
// Maps
//
// Created by Shin Yamamoto on 2018/10/09.
// Copyright © 2018 scenee. All rights reserved.
// Copyright © 2018 Shin Yamamoto. All rights reserved.
//
import UIKit
@@ -60,9 +60,9 @@
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<visualEffectView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Ye3-uU-bq3">
<rect key="frame" x="0.0" y="0.0" width="375" height="778"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="900"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="ED1-gT-FBj">
<rect key="frame" x="0.0" y="0.0" width="375" height="778"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="900"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<searchBar contentMode="redraw" searchBarStyle="minimal" translatesAutoresizingMaskIntoConstraints="NO" id="Zcj-SE-gb8">
@@ -70,7 +70,7 @@
<textInputTraits key="textInputTraits"/>
</searchBar>
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" showsHorizontalScrollIndicator="NO" showsVerticalScrollIndicator="NO" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="D7r-re-InH">
<rect key="frame" x="0.0" y="66" width="375" height="712"/>
<rect key="frame" x="0.0" y="66" width="375" height="748"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<view key="tableHeaderView" contentMode="scaleToFill" id="u28-LY-hIh" customClass="SearchHeaderView" customModule="Maps" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="375" height="116"/>
@@ -227,7 +227,7 @@
<constraint firstAttribute="trailing" secondItem="D7r-re-InH" secondAttribute="trailing" id="BES-GA-Btp"/>
<constraint firstItem="D7r-re-InH" firstAttribute="leading" secondItem="ED1-gT-FBj" secondAttribute="leading" id="UTe-YL-17h"/>
<constraint firstItem="Zcj-SE-gb8" firstAttribute="top" secondItem="ED1-gT-FBj" secondAttribute="top" constant="6" id="apU-Pd-PEO"/>
<constraint firstAttribute="bottom" secondItem="D7r-re-InH" secondAttribute="bottom" id="vfS-Lx-TXz"/>
<constraint firstAttribute="bottom" secondItem="D7r-re-InH" secondAttribute="bottom" constant="86" id="vfS-Lx-TXz"/>
<constraint firstItem="D7r-re-InH" firstAttribute="top" secondItem="Zcj-SE-gb8" secondAttribute="bottom" constant="4" id="vro-cd-B9c"/>
<constraint firstItem="Zcj-SE-gb8" firstAttribute="leading" secondItem="ED1-gT-FBj" secondAttribute="leading" id="wMb-L2-Z0W"/>
</constraints>
@@ -238,7 +238,7 @@
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="Ye3-uU-bq3" firstAttribute="leading" secondItem="G74-X7-Za8" secondAttribute="leading" id="Kr2-sU-ZWZ"/>
<constraint firstItem="Ye3-uU-bq3" firstAttribute="bottom" secondItem="G74-X7-Za8" secondAttribute="bottom" id="aWM-s3-3o4"/>
<constraint firstItem="Ye3-uU-bq3" firstAttribute="bottom" secondItem="Ncl-E9-yRn" secondAttribute="bottom" constant="88" id="aWM-s3-3o4"/>
<constraint firstItem="Ye3-uU-bq3" firstAttribute="trailing" secondItem="G74-X7-Za8" secondAttribute="trailing" id="fEL-8y-Acc"/>
<constraint firstItem="Ye3-uU-bq3" firstAttribute="top" secondItem="Ncl-E9-yRn" secondAttribute="top" id="w77-ba-FrJ"/>
</constraints>
@@ -247,6 +247,7 @@
<connections>
<outlet property="searchBar" destination="Zcj-SE-gb8" id="BH7-Gy-RG5"/>
<outlet property="tableView" destination="D7r-re-InH" id="nRN-fY-b8j"/>
<outlet property="visualEffectView" destination="Ye3-uU-bq3" id="rS6-Mq-OKs"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="EqR-Hp-zhc" userLabel="First Responder" sceneMemberID="firstResponder"/>
+54 -7
View File
@@ -1,5 +1,5 @@
//
// Copyright © 2018 scenee. All rights reserved.
// Copyright © 2018 Shin Yamamoto. All rights reserved.
//
import UIKit
@@ -26,8 +26,8 @@ class ViewController: UIViewController, MKMapViewDelegate, UISearchBarDelegate,
searchVC = storyboard?.instantiateViewController(withIdentifier: "SearchPanel") as? SearchPanelViewController
// Add a content view controller
fpc.show(searchVC, sender: self)
// Set a content view controller
fpc.set(contentViewController: searchVC)
fpc.track(scrollView: searchVC.tableView)
setupMapView()
@@ -36,7 +36,7 @@ class ViewController: UIViewController, MKMapViewDelegate, UISearchBarDelegate,
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// Add FloatingPanel to a view with animation.
fpc.add(toParent: self, animated: true)
fpc.addPanel(toParent: self, animated: true)
// Must be here
searchVC.searchBar.delegate = self
@@ -84,15 +84,16 @@ class ViewController: UIViewController, MKMapViewDelegate, UISearchBarDelegate,
// MARK: FloatingPanelControllerDelegate
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? {
switch traitCollection.verticalSizeClass {
switch newCollection.verticalSizeClass {
case .compact:
fpc.surfaceView.borderWidth = 1.0 / traitCollection.displayScale
fpc.surfaceView.borderColor = UIColor.black.withAlphaComponent(0.2)
return SearchPanelLandscapeLayout()
default:
fpc.surfaceView.borderWidth = 0.0
fpc.surfaceView.borderColor = nil
return nil
}
return nil
}
func floatingPanelDidMove(_ vc: FloatingPanelController) {
@@ -133,7 +134,8 @@ class SearchPanelViewController: UIViewController, UITableViewDataSource, UITabl
@IBOutlet weak var tableView: UITableView!
@IBOutlet weak var searchBar: UISearchBar!
@IBOutlet weak var visualEffectView: UIVisualEffectView!
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
@@ -143,6 +145,15 @@ class SearchPanelViewController: UIViewController, UITableViewDataSource, UITabl
textField.font = UIFont(name: textField.font!.fontName, size: 15.0)
hideHeader()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if #available(iOS 10, *) {
visualEffectView.layer.cornerRadius = 9.0
visualEffectView.clipsToBounds = true
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
@@ -193,6 +204,42 @@ class SearchPanelViewController: UIViewController, UITableViewDataSource, UITabl
}
}
public class SearchPanelLandscapeLayout: FloatingPanelLayout {
public var initialPosition: FloatingPanelPosition {
return .tip
}
public var supportedPositions: Set<FloatingPanelPosition> {
return [.full, .tip]
}
public func insetFor(position: FloatingPanelPosition) -> CGFloat? {
switch position {
case .full: return 16.0
case .tip: return 69.0
default: return nil
}
}
public func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint] {
if #available(iOS 11.0, *) {
return [
surfaceView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 8.0),
surfaceView.widthAnchor.constraint(equalToConstant: 291),
]
} else {
return [
surfaceView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 8.0),
surfaceView.widthAnchor.constraint(equalToConstant: 291),
]
}
}
public func backdropAlphaFor(position: FloatingPanelPosition) -> CGFloat {
return 0.0
}
}
class SearchCell: UITableViewCell {
@IBOutlet weak var iconImageView: UIImageView!
@IBOutlet weak var titleLabel: UILabel!
@@ -0,0 +1,631 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 50;
objects = {
/* Begin PBXBuildFile section */
545DB9EE21511E6300CA77B8 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DB9ED21511E6300CA77B8 /* AppDelegate.swift */; };
545DB9F021511E6300CA77B8 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DB9EF21511E6300CA77B8 /* ViewController.swift */; };
545DB9F321511E6300CA77B8 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 545DB9F121511E6300CA77B8 /* Main.storyboard */; };
545DB9F521511E6400CA77B8 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 545DB9F421511E6400CA77B8 /* Assets.xcassets */; };
545DB9F821511E6400CA77B8 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 545DB9F621511E6400CA77B8 /* LaunchScreen.storyboard */; };
545DBA0321511E6400CA77B8 /* SampleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DBA0221511E6400CA77B8 /* SampleTests.swift */; };
545DBA0E21511E6400CA77B8 /* SampleUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DBA0D21511E6400CA77B8 /* SampleUITests.swift */; };
54B51116216AFE5F0033A6F3 /* UIExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B51115216AFE5F0033A6F3 /* UIExtensions.swift */; };
54B5113C216C40670033A6F3 /* FloatingPanel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 54B5113B216C40670033A6F3 /* FloatingPanel.framework */; };
54B5113D216C40670033A6F3 /* FloatingPanel.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 54B5113B216C40670033A6F3 /* FloatingPanel.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
54CDC5D8215BBE23007D205C /* UIComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CDC5D7215BBE23007D205C /* UIComponents.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
545DB9FF21511E6400CA77B8 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 545DB9E221511E6300CA77B8 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 545DB9E921511E6300CA77B8;
remoteInfo = FloatingModalSample;
};
545DBA0A21511E6400CA77B8 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 545DB9E221511E6300CA77B8 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 545DB9E921511E6300CA77B8;
remoteInfo = FloatingModalSample;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
54B5111C216C3B300033A6F3 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
54B5113D216C40670033A6F3 /* FloatingPanel.framework in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
545DB9EA21511E6300CA77B8 /* Samples.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Samples.app; sourceTree = BUILT_PRODUCTS_DIR; };
545DB9ED21511E6300CA77B8 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
545DB9EF21511E6300CA77B8 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
545DB9F221511E6300CA77B8 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
545DB9F421511E6400CA77B8 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
545DB9F721511E6400CA77B8 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
545DB9F921511E6400CA77B8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
545DB9FE21511E6400CA77B8 /* SamplesTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SamplesTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
545DBA0221511E6400CA77B8 /* SampleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SampleTests.swift; sourceTree = "<group>"; };
545DBA0421511E6400CA77B8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
545DBA0921511E6400CA77B8 /* SamplesUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SamplesUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
545DBA0D21511E6400CA77B8 /* SampleUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SampleUITests.swift; sourceTree = "<group>"; };
545DBA0F21511E6400CA77B8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
54B51115216AFE5F0033A6F3 /* UIExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIExtensions.swift; sourceTree = "<group>"; };
54B5113B216C40670033A6F3 /* FloatingPanel.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = FloatingPanel.framework; sourceTree = BUILT_PRODUCTS_DIR; };
54CDC5D7215BBE23007D205C /* UIComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIComponents.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
545DB9E721511E6300CA77B8 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
54B5113C216C40670033A6F3 /* FloatingPanel.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
545DB9FB21511E6400CA77B8 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
545DBA0621511E6400CA77B8 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
545DB9E121511E6300CA77B8 = {
isa = PBXGroup;
children = (
54B5113B216C40670033A6F3 /* FloatingPanel.framework */,
545DB9EC21511E6300CA77B8 /* Sources */,
545DBA0121511E6400CA77B8 /* Tests */,
545DBA0C21511E6400CA77B8 /* UITests */,
545DB9EB21511E6300CA77B8 /* Products */,
545DBA1B2151CC1000CA77B8 /* Frameworks */,
);
sourceTree = "<group>";
};
545DB9EB21511E6300CA77B8 /* Products */ = {
isa = PBXGroup;
children = (
545DB9EA21511E6300CA77B8 /* Samples.app */,
545DB9FE21511E6400CA77B8 /* SamplesTests.xctest */,
545DBA0921511E6400CA77B8 /* SamplesUITests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
545DB9EC21511E6300CA77B8 /* Sources */ = {
isa = PBXGroup;
children = (
545DB9F421511E6400CA77B8 /* Assets.xcassets */,
545DB9F621511E6400CA77B8 /* LaunchScreen.storyboard */,
545DB9F121511E6300CA77B8 /* Main.storyboard */,
545DB9ED21511E6300CA77B8 /* AppDelegate.swift */,
545DB9EF21511E6300CA77B8 /* ViewController.swift */,
54B51115216AFE5F0033A6F3 /* UIExtensions.swift */,
54CDC5D7215BBE23007D205C /* UIComponents.swift */,
545DB9F921511E6400CA77B8 /* Info.plist */,
);
path = Sources;
sourceTree = "<group>";
};
545DBA0121511E6400CA77B8 /* Tests */ = {
isa = PBXGroup;
children = (
545DBA0221511E6400CA77B8 /* SampleTests.swift */,
545DBA0421511E6400CA77B8 /* Info.plist */,
);
path = Tests;
sourceTree = "<group>";
};
545DBA0C21511E6400CA77B8 /* UITests */ = {
isa = PBXGroup;
children = (
545DBA0D21511E6400CA77B8 /* SampleUITests.swift */,
545DBA0F21511E6400CA77B8 /* Info.plist */,
);
path = UITests;
sourceTree = "<group>";
};
545DBA1B2151CC1000CA77B8 /* Frameworks */ = {
isa = PBXGroup;
children = (
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
545DB9E921511E6300CA77B8 /* Samples */ = {
isa = PBXNativeTarget;
buildConfigurationList = 545DBA1221511E6400CA77B8 /* Build configuration list for PBXNativeTarget "Samples" */;
buildPhases = (
545DB9E621511E6300CA77B8 /* Sources */,
545DB9E721511E6300CA77B8 /* Frameworks */,
545DB9E821511E6300CA77B8 /* Resources */,
54B5111C216C3B300033A6F3 /* Embed Frameworks */,
);
buildRules = (
);
dependencies = (
);
name = Samples;
productName = FloatingModalSample;
productReference = 545DB9EA21511E6300CA77B8 /* Samples.app */;
productType = "com.apple.product-type.application";
};
545DB9FD21511E6400CA77B8 /* SamplesTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 545DBA1521511E6400CA77B8 /* Build configuration list for PBXNativeTarget "SamplesTests" */;
buildPhases = (
545DB9FA21511E6400CA77B8 /* Sources */,
545DB9FB21511E6400CA77B8 /* Frameworks */,
545DB9FC21511E6400CA77B8 /* Resources */,
);
buildRules = (
);
dependencies = (
545DBA0021511E6400CA77B8 /* PBXTargetDependency */,
);
name = SamplesTests;
productName = FloatingModalSampleTests;
productReference = 545DB9FE21511E6400CA77B8 /* SamplesTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
545DBA0821511E6400CA77B8 /* SamplesUITests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 545DBA1821511E6400CA77B8 /* Build configuration list for PBXNativeTarget "SamplesUITests" */;
buildPhases = (
545DBA0521511E6400CA77B8 /* Sources */,
545DBA0621511E6400CA77B8 /* Frameworks */,
545DBA0721511E6400CA77B8 /* Resources */,
);
buildRules = (
);
dependencies = (
545DBA0B21511E6400CA77B8 /* PBXTargetDependency */,
);
name = SamplesUITests;
productName = FloatingModalSampleUITests;
productReference = 545DBA0921511E6400CA77B8 /* SamplesUITests.xctest */;
productType = "com.apple.product-type.bundle.ui-testing";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
545DB9E221511E6300CA77B8 /* Project object */ = {
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 1000;
LastUpgradeCheck = 1000;
ORGANIZATIONNAME = scenee;
TargetAttributes = {
545DB9E921511E6300CA77B8 = {
CreatedOnToolsVersion = 10.0;
};
545DB9FD21511E6400CA77B8 = {
CreatedOnToolsVersion = 10.0;
TestTargetID = 545DB9E921511E6300CA77B8;
};
545DBA0821511E6400CA77B8 = {
CreatedOnToolsVersion = 10.0;
TestTargetID = 545DB9E921511E6300CA77B8;
};
};
};
buildConfigurationList = 545DB9E521511E6300CA77B8 /* Build configuration list for PBXProject "Samples" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 545DB9E121511E6300CA77B8;
productRefGroup = 545DB9EB21511E6300CA77B8 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
545DB9E921511E6300CA77B8 /* Samples */,
545DB9FD21511E6400CA77B8 /* SamplesTests */,
545DBA0821511E6400CA77B8 /* SamplesUITests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
545DB9E821511E6300CA77B8 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
545DB9F821511E6400CA77B8 /* LaunchScreen.storyboard in Resources */,
545DB9F521511E6400CA77B8 /* Assets.xcassets in Resources */,
545DB9F321511E6300CA77B8 /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
545DB9FC21511E6400CA77B8 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
545DBA0721511E6400CA77B8 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
545DB9E621511E6300CA77B8 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
54CDC5D8215BBE23007D205C /* UIComponents.swift in Sources */,
54B51116216AFE5F0033A6F3 /* UIExtensions.swift in Sources */,
545DB9F021511E6300CA77B8 /* ViewController.swift in Sources */,
545DB9EE21511E6300CA77B8 /* AppDelegate.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
545DB9FA21511E6400CA77B8 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
545DBA0321511E6400CA77B8 /* SampleTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
545DBA0521511E6400CA77B8 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
545DBA0E21511E6400CA77B8 /* SampleUITests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
545DBA0021511E6400CA77B8 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 545DB9E921511E6300CA77B8 /* Samples */;
targetProxy = 545DB9FF21511E6400CA77B8 /* PBXContainerItemProxy */;
};
545DBA0B21511E6400CA77B8 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 545DB9E921511E6300CA77B8 /* Samples */;
targetProxy = 545DBA0A21511E6400CA77B8 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
545DB9F121511E6300CA77B8 /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
545DB9F221511E6300CA77B8 /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
545DB9F621511E6400CA77B8 /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
545DB9F721511E6400CA77B8 /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
545DBA1021511E6400CA77B8 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_IDENTITY = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
545DBA1121511E6400CA77B8 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_IDENTITY = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
545DBA1321511E6400CA77B8 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = "";
INFOPLIST_FILE = Sources/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingPanelSample;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 4.2;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
545DBA1421511E6400CA77B8 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = "";
INFOPLIST_FILE = Sources/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingPanelSample;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 4.2;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
545DBA1621511E6400CA77B8 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
INFOPLIST_FILE = Tests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingModalSampleTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 4.2;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Samples.app/Samples";
};
name = Debug;
};
545DBA1721511E6400CA77B8 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
INFOPLIST_FILE = Tests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingModalSampleTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 4.2;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Samples.app/Samples";
};
name = Release;
};
545DBA1921511E6400CA77B8 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = J3D7L9FHSS;
INFOPLIST_FILE = UITests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingModalSampleUITests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 4.2;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_TARGET_NAME = FloatingModalSample;
};
name = Debug;
};
545DBA1A21511E6400CA77B8 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = J3D7L9FHSS;
INFOPLIST_FILE = UITests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingModalSampleUITests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 4.2;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_TARGET_NAME = FloatingModalSample;
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
545DB9E521511E6300CA77B8 /* Build configuration list for PBXProject "Samples" */ = {
isa = XCConfigurationList;
buildConfigurations = (
545DBA1021511E6400CA77B8 /* Debug */,
545DBA1121511E6400CA77B8 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
545DBA1221511E6400CA77B8 /* Build configuration list for PBXNativeTarget "Samples" */ = {
isa = XCConfigurationList;
buildConfigurations = (
545DBA1321511E6400CA77B8 /* Debug */,
545DBA1421511E6400CA77B8 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
545DBA1521511E6400CA77B8 /* Build configuration list for PBXNativeTarget "SamplesTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
545DBA1621511E6400CA77B8 /* Debug */,
545DBA1721511E6400CA77B8 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
545DBA1821511E6400CA77B8 /* Build configuration list for PBXNativeTarget "SamplesUITests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
545DBA1921511E6400CA77B8 /* Debug */,
545DBA1A21511E6400CA77B8 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 545DB9E221511E6300CA77B8 /* Project object */;
}
@@ -0,0 +1,91 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1000"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "545DB9E921511E6300CA77B8"
BuildableName = "Samples.app"
BlueprintName = "Samples"
ReferencedContainer = "container:Samples.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "545DB9E921511E6300CA77B8"
BuildableName = "Samples.app"
BlueprintName = "Samples"
ReferencedContainer = "container:Samples.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "545DB9E921511E6300CA77B8"
BuildableName = "Samples.app"
BlueprintName = "Samples"
ReferencedContainer = "container:Samples.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "545DB9E921511E6300CA77B8"
BuildableName = "Samples.app"
BlueprintName = "Samples"
ReferencedContainer = "container:Samples.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
@@ -0,0 +1,11 @@
//
// Created by Shin Yamamoto on 2018/09/18.
// Copyright © 2018 Shin Yamamoto. All rights reserved.
//
import UIKit
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
}
@@ -0,0 +1,98 @@
{
"images" : [
{
"idiom" : "iphone",
"size" : "20x20",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "20x20",
"scale" : "3x"
},
{
"idiom" : "iphone",
"size" : "29x29",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "29x29",
"scale" : "3x"
},
{
"idiom" : "iphone",
"size" : "40x40",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "40x40",
"scale" : "3x"
},
{
"idiom" : "iphone",
"size" : "60x60",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "60x60",
"scale" : "3x"
},
{
"idiom" : "ipad",
"size" : "20x20",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "20x20",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "29x29",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "29x29",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "40x40",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "40x40",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "76x76",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "76x76",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "83.5x83.5",
"scale" : "2x"
},
{
"idiom" : "ios-marketing",
"size" : "1024x1024",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
@@ -0,0 +1,6 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
}
}
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14313.18" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14283.14"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
</document>
@@ -0,0 +1,543 @@
<?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">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14460.20"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Navigation Controller-->
<scene sceneID="Cjh-iX-VQw">
<objects>
<navigationController id="RoN-h0-uBD" sceneMemberID="viewController">
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="hNW-5m-Omi">
<rect key="frame" x="0.0" y="20" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<connections>
<segue destination="jF4-A0-Eq6" kind="relationship" relationship="rootViewController" id="W9V-or-flQ"/>
</connections>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="lKu-or-aPl" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-705" y="27"/>
</scene>
<!--Samples-->
<scene sceneID="35L-Gs-Vts">
<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"/>
<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"/>
<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"/>
<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"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
</tableViewCell>
</prototypes>
</tableView>
</subviews>
<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"/>
</constraints>
<viewLayoutGuide key="safeArea" id="TkN-Oh-wF8"/>
</view>
<navigationItem key="navigationItem" title="Samples" id="wCF-su-7up"/>
<connections>
<outlet property="tableView" destination="7IS-PU-x0P" id="YFM-9W-eP4"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="eP2-DG-flv" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="57" y="27"/>
</scene>
<!--Item 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"/>
<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"/>
<state key="normal" title="Close"/>
<connections>
<action selector="closeWithSender:" destination="RpE-lI-27a" eventType="touchUpInside" id="hj3-Xv-6Gq"/>
<action selector="closeWithSender:" destination="bYI-y3-Rzb" eventType="touchUpInside" id="rg4-OH-Ojn"/>
</connections>
</button>
</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"/>
</constraints>
<viewLayoutGuide key="safeArea" id="2Cd-km-qEk"/>
</view>
<tabBarItem key="tabBarItem" tag="1" title="Item 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"/>
</scene>
<!--Item 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"/>
<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"/>
<state key="normal" title="Close"/>
<connections>
<action selector="closeWithSender:" destination="bYI-y3-Rzb" eventType="touchUpInside" id="YL4-GP-ZEZ"/>
<action selector="closeWithSender:" destination="lto-Zc-Vtp" eventType="touchUpInside" id="llo-9x-fQv"/>
</connections>
</button>
</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"/>
</constraints>
<viewLayoutGuide key="safeArea" id="f88-U8-Vja"/>
</view>
<tabBarItem key="tabBarItem" title="Item 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"/>
</scene>
<!--Tab Bar View Controller-->
<scene sceneID="nQ5-PV-qFw">
<objects>
<tabBarController storyboardIdentifier="TabBarViewController" id="c7K-XJ-TlT" customClass="TabBarViewController" customModule="Samples" customModuleProvider="target" sceneMemberID="viewController">
<tabBar key="tabBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="BPL-Dp-5pJ">
<rect key="frame" x="0.0" y="0.0" width="375" height="49"/>
<autoresizingMask key="autoresizingMask"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</tabBar>
<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"/>
</connections>
</tabBarController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Z9x-EI-p2b" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-183" y="806"/>
</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"/>
<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"/>
<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"/>
<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"/>
<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"/>
<state key="normal" title="Move to full"/>
<connections>
<action selector="moveToFullWithSender:" destination="bYI-y3-Rzb" eventType="touchUpInside" id="TDe-3J-gIR"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="2u5-cH-RAN">
<rect key="frame" x="0.0" y="74" width="85" height="30"/>
<state key="normal" title="Move to half"/>
<connections>
<action selector="moveToHalfWithSender:" destination="bYI-y3-Rzb" eventType="touchUpInside" id="12s-o7-Et5"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="M4A-iO-RIE">
<rect key="frame" x="0.0" y="148" width="77" height="30"/>
<state key="normal" title="Move to tip"/>
<connections>
<action selector="moveToTipWithSender:" destination="bYI-y3-Rzb" eventType="touchUpInside" id="BmL-91-9ai"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="szf-HE-QTk">
<rect key="frame" x="0.0" y="222" width="96" height="30"/>
<state key="normal" title="Update layout"/>
<connections>
<action selector="updateLayout:" destination="bYI-y3-Rzb" eventType="touchUpInside" id="Woz-a7-YMJ"/>
</connections>
</button>
</subviews>
</stackView>
</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 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"/>
</constraints>
<viewLayoutGuide key="safeArea" id="GBa-yx-8to"/>
</view>
<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"/>
</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"/>
<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"/>
<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"/>
<subviews>
<scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" alwaysBounceHorizontal="YES" pagingEnabled="YES" showsVerticalScrollIndicator="NO" bouncesZoom="NO" translatesAutoresizingMaskIntoConstraints="NO" id="xba-kG-VQ2">
<rect key="frame" x="0.0" y="0.0" width="375" height="242"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="WRe-tD-KTb">
<rect key="frame" x="0.0" y="0.0" width="1125" height="242"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="WuE-iq-0GW">
<rect key="frame" x="0.0" y="0.0" width="375" height="242"/>
<color key="backgroundColor" red="1" green="0.57810515169999999" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="w5Y-44-79g">
<rect key="frame" x="375" y="0.0" width="375" height="242"/>
<color key="backgroundColor" red="0.0078431372550000003" green="0.72156862749999995" blue="0.45882352939999999" alpha="1" colorSpace="calibratedRGB"/>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="jQf-k3-eAa">
<rect key="frame" x="750" y="0.0" width="375" height="242"/>
<color key="backgroundColor" red="0.016804177310000001" green="0.19835099580000001" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</view>
</subviews>
<constraints>
<constraint firstItem="w5Y-44-79g" firstAttribute="width" secondItem="WuE-iq-0GW" secondAttribute="width" id="kHy-eU-guw"/>
<constraint firstAttribute="height" constant="242" id="qzg-fI-j20"/>
<constraint firstItem="jQf-k3-eAa" firstAttribute="width" secondItem="WuE-iq-0GW" secondAttribute="width" id="zDe-Uj-FO0"/>
</constraints>
</stackView>
</subviews>
<constraints>
<constraint firstItem="WRe-tD-KTb" firstAttribute="leading" secondItem="xba-kG-VQ2" secondAttribute="leading" id="7QG-dB-afb"/>
<constraint firstAttribute="height" constant="242" id="Efw-D6-ksg"/>
<constraint firstAttribute="trailing" secondItem="WRe-tD-KTb" secondAttribute="trailing" id="ReM-cV-k0J"/>
<constraint firstItem="WRe-tD-KTb" firstAttribute="top" secondItem="xba-kG-VQ2" secondAttribute="top" id="Xla-QL-qwm"/>
<constraint firstItem="WuE-iq-0GW" firstAttribute="width" secondItem="xba-kG-VQ2" secondAttribute="width" id="qm0-cd-P69"/>
<constraint firstAttribute="bottom" secondItem="WRe-tD-KTb" secondAttribute="bottom" id="uha-Eo-lsv"/>
</constraints>
</scrollView>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="j8d-Tc-XQn">
<rect key="frame" x="0.0" y="242" width="375" height="242"/>
<color key="backgroundColor" red="0.0078431372550000003" green="0.72156862749999995" blue="0.45882352939999999" alpha="1" colorSpace="calibratedRGB"/>
<constraints>
<constraint firstAttribute="height" constant="242" id="Kw8-aw-DIp"/>
</constraints>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="bxy-HF-a7J">
<rect key="frame" x="0.0" y="484" width="375" height="242"/>
<color key="backgroundColor" red="1" green="0.2527923882" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="height" constant="242" id="AIb-xl-srX"/>
</constraints>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="ZzA-fs-Va5">
<rect key="frame" x="0.0" y="726" width="375" height="242"/>
<color key="backgroundColor" red="0.016804177310000001" green="0.19835099580000001" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="height" constant="242" id="TC1-jO-Wcz"/>
</constraints>
</view>
</subviews>
</stackView>
</subviews>
<constraints>
<constraint firstItem="lFR-Sp-Sj1" firstAttribute="width" secondItem="sBe-tN-uMi" secondAttribute="width" id="AtD-2C-97K"/>
<constraint firstAttribute="trailing" secondItem="lFR-Sp-Sj1" secondAttribute="trailing" id="F7t-Kr-VGd"/>
<constraint firstItem="lFR-Sp-Sj1" firstAttribute="leading" secondItem="sBe-tN-uMi" secondAttribute="leading" id="LzI-O9-5i0"/>
<constraint firstItem="lFR-Sp-Sj1" firstAttribute="top" secondItem="sBe-tN-uMi" secondAttribute="top" id="VwX-Hz-e8V"/>
<constraint firstAttribute="bottom" secondItem="lFR-Sp-Sj1" secondAttribute="bottom" id="hJt-0Z-dF3"/>
</constraints>
</scrollView>
</subviews>
<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="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"/>
</constraints>
<viewLayoutGuide key="safeArea" id="sL5-d5-za2"/>
<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"/>
<connections>
<outlet property="scrollView" destination="sBe-tN-uMi" id="h4S-Zl-cLO"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="QSd-gF-l5h" userLabel="First Responder" sceneMemberID="firstResponder"/>
<pongPressGestureRecognizer allowableMovement="10" minimumPressDuration="0.5" id="tOa-bf-zGz">
<connections>
<action selector="longPressed:" destination="LAe-jm-k6f" id="sE8-3l-Aos"/>
</connections>
</pongPressGestureRecognizer>
<tapGestureRecognizer id="SCk-hG-weZ">
<connections>
<action selector="tapped:" destination="LAe-jm-k6f" id="0Cw-vR-zRP"/>
</connections>
</tapGestureRecognizer>
<swipeGestureRecognizer direction="right" id="Fvp-Z6-eVc">
<connections>
<action selector="swipped:" destination="LAe-jm-k6f" id="Hav-7p-Tg8"/>
</connections>
</swipeGestureRecognizer>
</objects>
<point key="canvasLocation" x="1311" y="806"/>
</scene>
<!--Detail View Controller-->
<scene sceneID="b6k-zi-3wn">
<objects>
<viewController storyboardIdentifier="DetailViewController" id="YC8-ae-15L" customClass="DetailViewController" customModule="Samples" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="g7l-kO-y7q">
<rect key="frame" x="0.0" y="0.0" width="375" height="778"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<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"/>
<constraints>
<constraint firstAttribute="height" constant="44" id="0jg-5D-A1F"/>
<constraint firstAttribute="width" constant="44" id="1Cq-PA-wgW"/>
</constraints>
<connections>
<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"/>
<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"/>
<state key="normal" title="Show"/>
<connections>
<action selector="buttonPressed:" destination="YC8-ae-15L" eventType="touchUpInside" id="Mi1-o6-TWt"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="wmd-ab-Nz3">
<rect key="frame" x="0.0" y="52" width="114" height="30"/>
<state key="normal" title="Present Modallly"/>
<connections>
<action selector="buttonPressed:" destination="YC8-ae-15L" eventType="touchUpInside" id="tjH-Ev-kpx"/>
<segue destination="bYI-y3-Rzb" kind="presentation" identifier="PresentModallySegue" id="3yq-HE-Tgn"/>
</connections>
</button>
</subviews>
</stackView>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<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="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"/>
</constraints>
<viewLayoutGuide key="safeArea" id="tAi-nk-rDB"/>
<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"/>
<outletCollection property="gestureRecognizers" destination="Jg4-it-qJ5" appends="YES" id="ONf-5y-phY"/>
</connections>
</view>
<size key="freeformSize" width="375" height="778"/>
<connections>
<outlet property="closeButton" destination="noi-1a-5bZ" id="eWQ-ha-8y7"/>
<segue destination="bYI-y3-Rzb" kind="show" identifier="ShowSegue" id="r1P-2i-NDe"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Wqk-xl-O3I" userLabel="First Responder" sceneMemberID="firstResponder"/>
<tapGestureRecognizer id="6Ca-p8-7uF">
<connections>
<action selector="tapped:" destination="YC8-ae-15L" id="KFd-eT-RLn"/>
</connections>
</tapGestureRecognizer>
<swipeGestureRecognizer direction="right" id="SPY-Vr-XDT">
<connections>
<action selector="swipped:" destination="YC8-ae-15L" id="OFa-4C-8rI"/>
</connections>
</swipeGestureRecognizer>
<pongPressGestureRecognizer allowableMovement="10" minimumPressDuration="0.5" id="Jg4-it-qJ5">
<connections>
<action selector="longPressed:" destination="YC8-ae-15L" id="1G4-cf-RDE"/>
</connections>
</pongPressGestureRecognizer>
</objects>
<point key="canvasLocation" x="1440.8" y="-23.388305847076463"/>
</scene>
<!--Debug Text View Controller-->
<scene sceneID="Bkq-O7-q4A">
<objects>
<viewController storyboardIdentifier="ConsoleViewController" id="tvD-nO-QUb" customClass="DebugTextViewController" customModule="Samples" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="9YG-0j-Zzg">
<rect key="frame" x="0.0" y="0.0" width="375" height="778"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" showsHorizontalScrollIndicator="NO" bouncesZoom="NO" editable="NO" textAlignment="natural" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="rN1-HL-YHv">
<rect key="frame" x="0.0" y="17" width="375" height="761"/>
<color key="backgroundColor" white="1" alpha="0.5" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<string key="text">The standard Lorem Ipsum passage, used since the 1500s
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
Section 1.10.32 of "de Finibus Bonorum et Malorum", written by Cicero in 45 BC
"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?"
1914 translation by H. Rackham
"But I must explain to you how all this mistaken idea of denouncing pleasure and praising pain was born and I will give you a complete account of the system, and expound the actual teachings of the great explorer of the truth, the master-builder of human happiness. No one rejects, dislikes, or avoids pleasure itself, because it is pleasure, but because those who do not know how to pursue pleasure rationally encounter consequences that are extremely painful. Nor again is there anyone who loves or pursues or desires to obtain pain of itself, because it is pain, but because occasionally circumstances occur in which toil and pain can procure him some great pleasure. To take a trivial example, which of us ever undertakes laborious physical exercise, except to obtain some advantage from it? But who has any right to find fault with a man who chooses to enjoy a pleasure that has no annoying consequences, or one who avoids a pain that produces no resultant pleasure?"
Section 1.10.33 of "de Finibus Bonorum et Malorum", written by Cicero in 45 BC
"At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non provident, similique sunt in culpa qui officia deserunt mollitia animi, id est laborum et dolorum fuga. Et harum quidem rerum facilis est et expedita distinctio. Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo minus id quod maxime placeat facere possimus, omnis voluptas assumenda est, omnis dolor repellendus. Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet ut et voluptates repudiandae sint et molestiae non recusandae. Itaque earum rerum hic tenetur a sapiente delectus, ut aut reiciendis voluptatibus maiores alias consequatur aut perferendis doloribus asperiores repellat."
1914 translation by H. Rackham
"On the other hand, we denounce with righteous indignation and dislike men who are so beguiled and demoralized by the charms of pleasure of the moment, so blinded by desire, that they cannot foresee the pain and trouble that are bound to ensue; and equal blame belongs to those who fail in their duty through weakness of will, which is the same as saying through shrinking from toil and pain. These cases are perfectly simple and easy to distinguish. In a free hour, when our power of choice is untrammelled and when nothing prevents our being able to do what we like best, every pleasure is to be welcomed and every pain avoided. But in certain circumstances and owing to the claims of duty or the obligations of business it will frequently occur that pleasures have to be repudiated and annoyances accepted. The wise man therefore always holds in these matters to this principle of selection: he rejects pleasures to secure other greater pleasures, or else he endures pains to avoid worse pains."
The standard Lorem Ipsum passage, used since the 1500s
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
Section 1.10.32 of "de Finibus Bonorum et Malorum", written by Cicero in 45 BC
"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?"
1914 translation by H. Rackham
"But I must explain to you how all this mistaken idea of denouncing pleasure and praising pain was born and I will give you a complete account of the system, and expound the actual teachings of the great explorer of the truth, the master-builder of human happiness. No one rejects, dislikes, or avoids pleasure itself, because it is pleasure, but because those who do not know how to pursue pleasure rationally encounter consequences that are extremely painful. Nor again is there anyone who loves or pursues or desires to obtain pain of itself, because it is pain, but because occasionally circumstances occur in which toil and pain can procure him some great pleasure. To take a trivial example, which of us ever undertakes laborious physical exercise, except to obtain some advantage from it? But who has any right to find fault with a man who chooses to enjoy a pleasure that has no annoying consequences, or one who avoids a pain that produces no resultant pleasure?"
Section 1.10.33 of "de Finibus Bonorum et Malorum", written by Cicero in 45 BC
"At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non provident, similique sunt in culpa qui officia deserunt mollitia animi, id est laborum et dolorum fuga. Et harum quidem rerum facilis est et expedita distinctio. Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo minus id quod maxime placeat facere possimus, omnis voluptas assumenda est, omnis dolor repellendus. Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet ut et voluptates repudiandae sint et molestiae non recusandae. Itaque earum rerum hic tenetur a sapiente delectus, ut aut reiciendis voluptatibus maiores alias consequatur aut perferendis doloribus asperiores repellat."
1914 translation by H. Rackham
"On the other hand, we denounce with righteous indignation and dislike men who are so beguiled and demoralized by the charms of pleasure of the moment, so blinded by desire, that they cannot foresee the pain and trouble that are bound to ensue; and equal blame belongs to those who fail in their duty through weakness of will, which is the same as saying through shrinking from toil and pain. These cases are perfectly simple and easy to distinguish. In a free hour, when our power of choice is untrammelled and when nothing prevents our being able to do what we like best, every pleasure is to be welcomed and every pain avoided. But in certain circumstances and owing to the claims of duty or the obligations of business it will frequently occur that pleasures have to be repudiated and annoyances accepted. The wise man therefore always holds in these matters to this principle of selection: he rejects pleasures to secure other greater pleasures, or else he endures pains to avoid worse pains."</string>
<fontDescription key="fontDescription" name="CourierNewPSMT" family="Courier New" pointSize="14"/>
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
</textView>
</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 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"/>
</constraints>
<viewLayoutGuide key="safeArea" id="ix0-2W-gQN"/>
</view>
<size key="freeformSize" width="375" height="778"/>
<connections>
<outlet property="textView" destination="rN1-HL-YHv" id="gmr-Uf-jd8"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="x1h-y1-h8q" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="729" y="-23"/>
</scene>
</scenes>
<inferredMetricsTieBreakers>
<segue reference="3yq-HE-Tgn"/>
</inferredMetricsTieBreakers>
</document>
+45
View File
@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>
+103
View File
@@ -0,0 +1,103 @@
//
// Created by Shin Yamamoto on 2018/09/19.
// Copyright © 2018 Shin Yamamoto. All rights reserved.
//
import UIKit
@IBDesignable
class CloseButton: UIButton {
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
render()
}
override init(frame: CGRect) {
super.init(frame: frame)
render()
}
func render() {
self.backgroundColor = .clear
}
func p(_ p: CGFloat) -> CGFloat {
return p * (2.0 / 3.0)
}
override var isHighlighted: Bool { didSet { setNeedsDisplay() } }
override var isSelected: Bool { didSet { setNeedsDisplay() } }
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
context.setLineWidth(p(1.0))
let color = UIColor(displayP3Red: 0.76,
green: 0.77,
blue: 0.76,
alpha: 1.0)
context.setFillColor(color.cgColor)
context.beginPath()
context.addArc(center: CGPoint(x: rect.width * 0.5,
y: rect.height * 0.5),
radius: p(36.0) * 0.5,
startAngle: 0,
endAngle: CGFloat.pi * 2.0,
clockwise: true)
context.fillPath()
let highlightedColor = UIColor(displayP3Red: 0.53,
green: 0.53,
blue: 0.53,
alpha: 1.0)
let crossColor: UIColor = isHighlighted || isSelected ? highlightedColor : .white
context.setStrokeColor(crossColor.cgColor)
context.setBlendMode(.normal)
context.setLineWidth(p(3.5))
context.setLineCap(.round)
let offset = (rect.width - p(36.0)) * 0.5
context.beginPath()
context.addLines(between: [CGPoint(x: offset + p(12.0), y: offset + p(12.0)),
CGPoint(x: offset + p(24.0), y: offset + p(24.0))])
context.strokePath()
context.beginPath()
context.addLines(between: [CGPoint(x: offset + p(24.0), y: offset + p(12.0)),
CGPoint(x: offset + p(12.0), y: offset + p(24.0))])
context.strokePath()
}
}
@IBDesignable
class SafeAreaView: UIView {
override func prepareForInterfaceBuilder() {
let label = UILabel()
label.text = "Safe Area"
addSubview(label)
label.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
label.centerXAnchor.constraint(equalTo: self.centerXAnchor),
label.centerYAnchor.constraint(equalTo: self.centerYAnchor, constant: -4.0),
])
}
}
@IBDesignable
class OnSafeAreaView: UIView {
override func prepareForInterfaceBuilder() {
let label = UILabel()
label.text = "On Safe Area"
addSubview(label)
label.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
label.centerXAnchor.constraint(equalTo: self.centerXAnchor),
label.centerYAnchor.constraint(equalTo: self.centerYAnchor, constant: -4.0),
])
}
}
@@ -0,0 +1,25 @@
//
// Created by Shin Yamamoto on 2018/10/08.
// Copyright © 2018 Shin Yamamoto. All rights reserved.
//
import UIKit
extension UIView {
var layoutInsets: UIEdgeInsets {
if #available(iOS 11.0, *) {
return safeAreaInsets
} else {
return layoutMargins
}
}
var layoutGuide: UILayoutGuide {
if #available(iOS 11.0, *) {
return safeAreaLayoutGuide
} else {
return layoutMarginsGuide
}
}
}
@@ -0,0 +1,627 @@
//
// ViewController.swift
// FloatingModalSample
//
// Created by Shin Yamamoto on 2018/09/18.
// Copyright © 2018 Shin Yamamoto. All rights reserved.
//
import UIKit
import FloatingPanel
class SampleListViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, FloatingPanelControllerDelegate, FloatingPanelLayout {
@IBOutlet weak var tableView: UITableView!
enum Menu: Int, CaseIterable {
case trackingTableView
case trackingTextView
case showDetail
case showModal
case showTabBar
case showNestedScrollView
case showRemovablePanel
var name: String {
switch self {
case .trackingTableView: return "Scroll tracking(TableView)"
case .trackingTextView: return "Scroll tracking(TextView)"
case .showDetail: return "Show Detail Panel"
case .showModal: return "Show Modal"
case .showTabBar: return "Show Tab Bar"
case .showNestedScrollView: return "Show Nested ScrollView"
case .showRemovablePanel: return "Show Removable Panel"
}
}
var storyboardID: String? {
switch self {
case .trackingTableView: return nil
case .trackingTextView: return "ConsoleViewController"
case .showDetail: return "DetailViewController"
case .showModal: return "ModalViewController"
case .showTabBar: return "TabBarViewController"
case .showNestedScrollView: return "NestedScrollViewController"
case .showRemovablePanel: return "DetailViewController"
}
}
}
var mainPanelVC: FloatingPanelController!
var detailPanelVC: FloatingPanelController!
var currentMenu: Menu = .trackingTableView
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
tableView.delegate = self
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
let contentVC = DebugTableViewController()
addMainPanel(with: contentVC)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
}
func addMainPanel(with contentVC: UIViewController) {
// Initialize FloatingPanelController
mainPanelVC = FloatingPanelController()
mainPanelVC.delegate = self
mainPanelVC.isRemovalInteractionEnabled = (currentMenu == .showRemovablePanel)
// Initialize FloatingPanelController and add the view
mainPanelVC.surfaceView.cornerRadius = 6.0
mainPanelVC.surfaceView.shadowHidden = false
// Set a content view controller
mainPanelVC.set(contentViewController: contentVC)
// Track a scroll view
switch contentVC {
case let consoleVC as DebugTextViewController:
mainPanelVC.track(scrollView: consoleVC.textView)
case let contentVC as DebugTableViewController:
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)
}
@objc func dismissDetailPanelVC() {
detailPanelVC.removePanelFromParent(animated: true, completion: nil)
}
// MARK:- TableViewDatasource
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
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
return cell
}
// MARK:- TableViewDelegate
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let menu = Menu.allCases[indexPath.row]
let contentVC: UIViewController = {
guard let storyboardID = menu.storyboardID else { return DebugTableViewController() }
guard let vc = self.storyboard?.instantiateViewController(withIdentifier: storyboardID) else { fatalError() }
return vc
}()
self.currentMenu = menu
switch menu {
case .showDetail:
detailPanelVC?.removeFromParent()
// Initialize FloatingPanelController
detailPanelVC = FloatingPanelController()
// Initialize FloatingPanelController and add the view
detailPanelVC.surfaceView.cornerRadius = 6.0
detailPanelVC.surfaceView.shadowHidden = false
// Set a content view controller
detailPanelVC.set(contentViewController: contentVC)
// Add FloatingPanel to self.view
detailPanelVC.addPanel(toParent: self, belowView: nil, animated: true)
case .showModal, .showTabBar:
let modalVC = contentVC
present(modalVC, animated: true, completion: nil)
default:
detailPanelVC?.removePanelFromParent(animated: true, completion: nil)
mainPanelVC?.removePanelFromParent(animated: true) {
self.addMainPanel(with: contentVC)
}
}
}
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? {
if currentMenu == .showRemovablePanel {
return newCollection.verticalSizeClass == .compact ? RemovablePanelLandscapeLayout() : RemovablePanelLayout()
} else {
return self
}
}
var initialPosition: FloatingPanelPosition {
return .half
}
func insetFor(position: FloatingPanelPosition) -> CGFloat? {
switch position {
case .full: return UIScreen.main.bounds.height == 667.0 ? 18.0 : 16.0
case .half: return 262.0
case .tip: return 69.0
}
}
}
class RemovablePanelLayout: FloatingPanelLayout {
var initialPosition: FloatingPanelPosition {
return .half
}
var supportedPositions: Set<FloatingPanelPosition> {
return [.full, .half]
}
var bottomInteractionBuffer: CGFloat {
return 261.0 - 22.0
}
func insetFor(position: FloatingPanelPosition) -> CGFloat? {
switch position {
case .full: return 16.0
case .half: return 261.0
default: return nil
}
}
func backdropAlphaFor(position: FloatingPanelPosition) -> CGFloat {
return 0.3
}
}
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
}
}
func backdropAlphaFor(position: FloatingPanelPosition) -> CGFloat {
return 0.3
}
}
class NestedScrollViewController: UIViewController {
@IBOutlet weak var scrollView: UIScrollView!
@IBAction func longPressed(_ sender: Any) {
print("LongPressed!")
}
@IBAction func swipped(_ sender: Any) {
print("Swipped!")
}
@IBAction func tapped(_ sender: Any) {
print("Tapped!")
}
}
class DebugTextViewController: UIViewController, UITextViewDelegate {
@IBOutlet weak var textView: UITextView!
override func viewDidLoad() {
super.viewDidLoad()
textView.delegate = self
if #available(iOS 11.0, *) {
textView.contentInsetAdjustmentBehavior = .never
}
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
print("TextView --- ", scrollView.contentOffset, scrollView.contentInset)
if #available(iOS 11.0, *) {
print("TextView --- ", scrollView.adjustedContentInset)
}
}
@IBAction func close(sender: UIButton) {
// Now impossible
// dismiss(animated: true, completion: nil)
(self.parent as? FloatingPanelController)?.removePanelFromParent(animated: true, completion: nil)
}
}
class DebugTableViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
weak var tableView: UITableView!
var items: [String] = []
var itemHeight: CGFloat = 66.0
override func viewDidLoad() {
super.viewDidLoad()
let tableView = UITableView(frame: .zero,
style: .plain)
view.addSubview(tableView)
tableView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: view.topAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
tableView.leftAnchor.constraint(equalTo: view.leftAnchor),
tableView.rightAnchor.constraint(equalTo: view.rightAnchor)
])
tableView.dataSource = self
tableView.delegate = self
self.tableView = tableView
let stackView = UIStackView()
view.addSubview(stackView)
stackView.axis = .vertical
stackView.distribution = .fillEqually
stackView.alignment = .trailing
stackView.spacing = 10.0
stackView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
stackView.topAnchor.constraint(equalTo: view.topAnchor, constant: 22.0),
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 i in 0...100 {
items.append("Items \(i)")
}
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
}
@objc func animateScroll() {
tableView.scrollToRow(at: IndexPath(row: lround(Double(items.count) / 2.0),
section: 0),
at: .top, animated: true)
}
@objc func changeContentSize() {
let actionSheet = UIAlertController(title: "Change content size", message: "", preferredStyle: .actionSheet)
actionSheet.addAction(UIAlertAction(title: "Large", style: .default, handler: { (_) in
self.itemHeight = 66.0
self.changeItems(100)
}))
actionSheet.addAction(UIAlertAction(title: "Match", style: .default, handler: { (_) in
switch self.tableView.bounds.height {
case 585: // iPhone 6,7,8
self.itemHeight = self.tableView.bounds.height / 13.0
self.changeItems(13)
case 656: // iPhone {6,7,8} Plus
self.itemHeight = self.tableView.bounds.height / 16.0
self.changeItems(16)
default: // iPhone X family
self.itemHeight = self.tableView.bounds.height / 12.0
self.changeItems(12)
}
}))
actionSheet.addAction(UIAlertAction(title: "Short", style: .default, handler: { (_) in
self.itemHeight = 66.0
self.changeItems(3)
}))
self.present(actionSheet, animated: true, completion: nil)
}
func changeItems(_ count: Int) {
items.removeAll()
for i in 0..<count {
items.append("Items \(i)")
}
tableView.reloadData()
}
@objc func close(sender: UIButton) {
// Remove FloatingPanel from a view
(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
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return itemHeight
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
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)
}),
]
}
}
class DetailViewController: UIViewController {
@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)
}
@IBAction func buttonPressed(_ sender: UIButton) {
switch sender.titleLabel?.text {
case "Show":
performSegue(withIdentifier: "ShowSegue", sender: self)
case "Present Modally":
performSegue(withIdentifier: "PresentModallySegue", sender: self)
default:
break
}
}
@IBAction func tapped(_ sender: Any) {
print("Detail panel is tapped!")
}
@IBAction func swipped(_ sender: Any) {
print("Detail panel is swipped!")
}
@IBAction func longPressed(_ sender: Any) {
print("Detail panel is longPressed!")
}
}
class ModalViewController: UIViewController, FloatingPanelControllerDelegate {
var fpc: FloatingPanelController!
var consoleVC: DebugTextViewController!
@IBOutlet weak var safeAreaView: UIView!
var isNewlayout: Bool = false
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// Initialize FloatingPanelController
fpc = FloatingPanelController()
fpc.delegate = self
// Initialize FloatingPanelController and add the view
fpc.surfaceView.cornerRadius = 6.0
fpc.surfaceView.shadowHidden = false
// Set a content view controller and track the scroll view
let consoleVC = storyboard?.instantiateViewController(withIdentifier: "ConsoleViewController") as! DebugTextViewController
fpc.set(contentViewController: consoleVC)
fpc.track(scrollView: consoleVC.textView)
self.consoleVC = consoleVC
// Add FloatingPanel to self.view
fpc.addPanel(toParent: self, belowView: safeAreaView)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
// Remove FloatingPanel from a view
fpc.removePanelFromParent(animated: false)
}
@IBAction func close(sender: UIButton) {
dismiss(animated: true, completion: nil)
}
@IBAction func moveToFull(sender: UIButton) {
fpc.move(to: .full, animated: true)
}
@IBAction func moveToHalf(sender: UIButton) {
fpc.move(to: .half, animated: true)
}
@IBAction func moveToTip(sender: UIButton) {
fpc.move(to: .tip, animated: true)
}
@IBAction func updateLayout(_ sender: Any) {
isNewlayout = !isNewlayout
UIView.animate(withDuration: 0.5) {
self.fpc.updateLayout()
}
}
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? {
return (isNewlayout) ? ModalSecondLayout() : nil
}
}
class ModalSecondLayout: FloatingPanelLayout {
var initialPosition: FloatingPanelPosition {
return .half
}
func insetFor(position: FloatingPanelPosition) -> CGFloat? {
switch position {
case .full: return 18.0
case .half: return 262.0
case .tip: return 44.0
}
}
}
class TabBarViewController: UITabBarController {}
class TabBarContentViewController: UIViewController, FloatingPanelControllerDelegate {
var fpc: FloatingPanelController!
var consoleVC: DebugTextViewController!
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// Initialize FloatingPanelController
fpc = FloatingPanelController()
fpc.delegate = self
// Initialize FloatingPanelController and add the view
fpc.surfaceView.cornerRadius = 6.0
fpc.surfaceView.shadowHidden = false
// Set a content view controller and track the scroll view
let consoleVC = storyboard?.instantiateViewController(withIdentifier: "ConsoleViewController") as! DebugTextViewController
fpc.set(contentViewController: consoleVC)
fpc.track(scrollView: consoleVC.textView)
self.consoleVC = consoleVC
// Add FloatingPanel to self.view
fpc.addPanel(toParent: self)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
// Remove FloatingPanel from a view
fpc.removePanelFromParent(animated: false)
}
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? {
switch self.tabBarItem.tag {
case 0:
return OneTabBarPanelLayout()
case 1:
return TwoTabBarPanel2Layout()
default:
return nil
}
}
@IBAction func close(sender: UIButton) {
dismiss(animated: true, completion: nil)
}
}
extension FloatingPanelLayout {
func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint] {
if #available(iOS 11.0, *) {
return [
surfaceView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 0.0),
surfaceView.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor, constant: 0.0),
]
} else {
return [
surfaceView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 0.0),
surfaceView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 0.0),
]
}
}
}
class OneTabBarPanelLayout: FloatingPanelLayout {
var initialPosition: FloatingPanelPosition {
return .tip
}
var supportedPositions: Set<FloatingPanelPosition> {
return [.full, .tip]
}
func insetFor(position: FloatingPanelPosition) -> CGFloat? {
switch position {
case .full: return 16.0
case .tip: return 22.0
default: return nil
}
}
}
class TwoTabBarPanel2Layout: FloatingPanelLayout {
var initialPosition: FloatingPanelPosition {
return .half
}
var supportedPositions: Set<FloatingPanelPosition> {
return [.full, .half]
}
var bottomInteractionBuffer: CGFloat {
return 261.0 - 22.0
}
func insetFor(position: FloatingPanelPosition) -> CGFloat? {
switch position {
case .full: return 16.0
case .half: return 261.0
default: return nil
}
}
}
+22
View File
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>BNDL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
</dict>
</plist>
+34
View File
@@ -0,0 +1,34 @@
//
// FloatingModalSampleTests.swift
// FloatingModalSampleTests
//
// Created by Shin Yamamoto on 2018/09/18.
// Copyright © 2018 Shin Yamamoto. All rights reserved.
//
import XCTest
@testable import FloatingPanelSample
class SampleTests: XCTestCase {
override func setUp() {
// Put setup code here. This method is called before the invocation of each test method in the class.
}
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}
func testExample() {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct results.
}
func testPerformanceExample() {
// This is an example of a performance test case.
self.measure {
// Put the code you want to measure the time of here.
}
}
}
+22
View File
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>BNDL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
</dict>
</plist>
@@ -0,0 +1,31 @@
//
// Created by Shin Yamamoto on 2018/09/18.
// Copyright © 2018 Shin Yamamoto. All rights reserved.
//
import XCTest
class SampleUITests: XCTestCase {
override func setUp() {
// Put setup code here. This method is called before the invocation of each test method in the class.
// In UI tests it is usually best to stop immediately when a failure occurs.
continueAfterFailure = false
// UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method.
XCUIApplication().launch()
// In UI tests its important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
}
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}
func testExample() {
// Use recording to get started writing UI tests.
// Use XCTAssert and related functions to verify your tests produce the correct results.
}
}
@@ -303,7 +303,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = J3D7L9FHSS;
DEVELOPMENT_TEAM = "";
INFOPLIST_FILE = Stocks/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = (
@@ -322,7 +322,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = J3D7L9FHSS;
DEVELOPMENT_TEAM = "";
INFOPLIST_FILE = Stocks/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = (
+1 -1
View File
@@ -3,7 +3,7 @@
// Stocks
//
// Created by Shin Yamamoto on 2018/10/12.
// Copyright © 2018 scenee. All rights reserved.
// Copyright © 2018 Shin Yamamoto. All rights reserved.
//
import UIKit
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14313.18" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
<device id="retina5_9" orientation="portrait">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
@@ -15,11 +15,11 @@
<objects>
<viewController id="BYZ-38-t0r" customClass="ViewController" customModule="Stocks" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Uop-sw-I6p">
<rect key="frame" x="0.0" y="109" width="375" height="624.66666666666663"/>
<rect key="frame" x="0.0" y="85" width="375" height="537.5"/>
<subviews>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" placeholderIntrinsicWidth="375" placeholderIntrinsicHeight="625" image="stocks_list" translatesAutoresizingMaskIntoConstraints="NO" id="XJR-iK-fem">
<rect key="frame" x="0.0" y="0.0" width="375" height="625"/>
@@ -34,10 +34,10 @@
</constraints>
</scrollView>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="dFl-81-6ok">
<rect key="frame" x="0.0" y="733.66666666666663" width="375" height="78.333333333333371"/>
<rect key="frame" x="0.0" y="622.5" width="375" height="44.5"/>
<subviews>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="yahoo_bottom_bar" translatesAutoresizingMaskIntoConstraints="NO" id="NKr-gS-mpx">
<rect key="frame" x="0.0" y="0.0" width="375" height="44.333333333333336"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="44.5"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="44.5" id="B5t-ZF-qUj"/>
@@ -52,18 +52,17 @@
<constraint firstItem="NKr-gS-mpx" firstAttribute="leading" secondItem="dFl-81-6ok" secondAttribute="leading" id="T2r-kY-JYy"/>
</constraints>
</view>
<stackView opaque="NO" contentMode="scaleToFill" fixedFrame="YES" axis="vertical" alignment="top" spacing="-8" translatesAutoresizingMaskIntoConstraints="NO" id="f7r-Al-pIN">
<rect key="frame" x="16" y="44.000000000000014" width="153.33333333333334" height="56.666666666666664"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="top" spacing="-8" translatesAutoresizingMaskIntoConstraints="NO" id="f7r-Al-pIN">
<rect key="frame" x="16" y="20" width="153.5" height="57"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="STOCKS" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="PCG-Wl-fXa">
<rect key="frame" x="0.0" y="0.0" width="111.66666666666667" height="32.333333333333336"/>
<rect key="frame" x="0.0" y="0.0" width="111.5" height="32.5"/>
<fontDescription key="fontDescription" type="system" weight="heavy" pointSize="27"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="OCTOBER 5" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="XmK-pu-9g4">
<rect key="frame" x="0.0" y="24.333333333333332" width="153.33333333333334" height="32.333333333333343"/>
<rect key="frame" x="0.0" y="24.5" width="153.5" height="32.5"/>
<fontDescription key="fontDescription" type="system" weight="heavy" pointSize="27"/>
<color key="textColor" red="0.55308091640472412" green="0.55657511949539185" blue="0.57255202531814575" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
<nil key="highlightedColor"/>
@@ -75,10 +74,12 @@
<constraints>
<constraint firstItem="6Tk-OE-BBY" firstAttribute="trailing" secondItem="dFl-81-6ok" secondAttribute="trailing" id="20i-yz-AaQ"/>
<constraint firstItem="Uop-sw-I6p" firstAttribute="leading" secondItem="6Tk-OE-BBY" secondAttribute="leading" id="44w-r8-vYl"/>
<constraint firstItem="f7r-Al-pIN" firstAttribute="leading" secondItem="6Tk-OE-BBY" secondAttribute="leading" constant="16" id="4Bq-Km-eET"/>
<constraint firstItem="Uop-sw-I6p" firstAttribute="top" secondItem="6Tk-OE-BBY" secondAttribute="top" constant="65" id="CXL-Dk-8MM"/>
<constraint firstItem="Uop-sw-I6p" firstAttribute="trailing" secondItem="6Tk-OE-BBY" secondAttribute="trailing" id="CsO-WF-T8L"/>
<constraint firstItem="dFl-81-6ok" firstAttribute="top" secondItem="Uop-sw-I6p" secondAttribute="bottom" id="Cz0-dW-r9H"/>
<constraint firstAttribute="bottom" secondItem="dFl-81-6ok" secondAttribute="bottom" id="KGl-8W-5ja"/>
<constraint firstItem="f7r-Al-pIN" firstAttribute="top" secondItem="6Tk-OE-BBY" secondAttribute="top" constant="1.4210854715202004e-14" id="Qvt-vQ-PpT"/>
<constraint firstItem="dFl-81-6ok" firstAttribute="leading" secondItem="6Tk-OE-BBY" secondAttribute="leading" id="nlX-Ab-1aI"/>
<constraint firstItem="6Tk-OE-BBY" firstAttribute="bottom" secondItem="NKr-gS-mpx" secondAttribute="bottom" id="yeu-NH-Pmp"/>
</constraints>
+19 -36
View File
@@ -3,7 +3,7 @@
// Stocks
//
// Created by Shin Yamamoto on 2018/10/12.
// Copyright © 2018 scenee. All rights reserved.
// Copyright © 2018 Shin Yamamoto. All rights reserved.
//
import UIKit
@@ -34,11 +34,11 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate {
newsVC = storyboard?.instantiateViewController(withIdentifier: "News") as? NewsViewController
// Add a content view controller
fpc.show(newsVC, sender: self)
// Set a content view controller
fpc.set(contentViewController: newsVC)
fpc.track(scrollView: newsVC.scrollView)
fpc.add(toParent: self, belowView: bottomToolView, animated: false)
fpc.addPanel(toParent: self, belowView: bottomToolView, animated: false)
topBannerView.frame = .zero
topBannerView.alpha = 0.0
@@ -102,15 +102,14 @@ class NewsViewController: UIViewController {
// MARK: My custom layout
class FloatingPanelStocksLayout: FloatingPanelLayout {
public var supportedPositions: [FloatingPanelPosition] {
return [.full, .half, .tip]
}
public var initialPosition: FloatingPanelPosition {
var initialPosition: FloatingPanelPosition {
return .tip
}
public func insetFor(position: FloatingPanelPosition) -> CGFloat? {
var topInteractionBuffer: CGFloat { return 0.0 }
var bottomInteractionBuffer: CGFloat { return 0.0 }
func insetFor(position: FloatingPanelPosition) -> CGFloat? {
switch position {
case .full: return 56.0
case .half: return 262.0
@@ -118,14 +117,9 @@ class FloatingPanelStocksLayout: FloatingPanelLayout {
}
}
public func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint] {
return [
surfaceView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 0.0),
surfaceView.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor, constant: 0.0),
]
func backdropAlphaFor(position: FloatingPanelPosition) -> CGFloat {
return 0.0
}
var backdropAlpha: CGFloat = 0.0
}
// MARK: My custom behavior
@@ -135,27 +129,16 @@ class FloatingPanelStocksBehavior: FloatingPanelBehavior {
return 15.0
}
func interactionAnimator(to targetPosition: FloatingPanelPosition, with velocity: CGVector) -> UIViewPropertyAnimator {
let damping = self.damping(with: velocity)
let springTiming = UISpringTimingParameters(dampingRatio: damping,
initialVelocity: velocity)
let duration = getDuration(with: velocity)
return UIViewPropertyAnimator(duration: duration, timingParameters: springTiming)
func interactionAnimator(_ fpc: FloatingPanelController, to targetPosition: FloatingPanelPosition, with velocity: CGVector) -> UIViewPropertyAnimator {
let timing = timeingCurve(to: targetPosition, with: velocity)
return UIViewPropertyAnimator(duration: 0, timingParameters: timing)
}
private func getDuration(with velocity: CGVector) -> TimeInterval {
let dy = abs(velocity.dy)
switch dy {
case ..<1.0:
return 0.5
case 1.0..<velocityThreshold:
let a = ((dy - 1.0) / (velocityThreshold - 1.0))
return TimeInterval(0.5 - (0.25 * a))
case velocityThreshold...:
return 0.25
default:
fatalError()
}
private func timeingCurve(to: FloatingPanelPosition, with velocity: CGVector) -> UITimingCurveProvider {
let damping = self.damping(with: velocity)
return UISpringTimingParameters(dampingRatio: damping,
frequencyResponse: 0.4,
initialVelocity: velocity)
}
private func damping(with velocity: CGVector) -> CGFloat {
+3 -3
View File
@@ -1,10 +1,10 @@
Pod::Spec.new do |s|
s.name = "FloatingPanel"
s.version = "0.9"
s.summary = "FloatingPanel is a simple and easy-to-use UI component of a floating panel interface"
s.version = "1.2.2"
s.summary = "FloatingPanel is a clean and easy-to-use UI component of a floating panel interface."
s.description = <<-DESC
FloatingPanel is a simple and easy-to-use UI component for a new interface introduced in Apple Maps, Shortcuts and Stocks app.
FloatingPanel is a clean and easy-to-use UI component for a new interface introduced in Apple Maps, Shortcuts and Stocks app.
The new interface displays the related contents and utilities in parallel as a user wants.
DESC
s.homepage = "https://github.com/SCENEE/FloatingPanel"
+1 -1
View File
@@ -8,7 +8,7 @@
location = "group:Framework"
name = "Framework">
<FileRef
location = "group:/Users/shin/Workspace/scenee/FloatingPanel/Framework/FloatingPanel.xcodeproj">
location = "group:FloatingPanel.xcodeproj">
</FileRef>
</Group>
<Group
+518 -139
View File
@@ -1,5 +1,5 @@
//
// Copyright © 2018 scenee. All rights reserved.
// Copyright © 2018 Shin Yamamoto. All rights reserved.
//
import UIKit
@@ -7,50 +7,57 @@ import UIKit
///
/// FloatingPanel presentation model
///
class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate {
/* Cause 'terminating with uncaught exception of type NSException' error on Swift Playground
unowned let view: UIView
*/
let surfaceView: FloatingPanelSurfaceView
let backdropView: FloatingPanelBackdropView
private unowned let viewcontroller: FloatingPanelController
var layoutAdapter: FloatingPanelLayoutAdapter
var behavior: FloatingPanelBehavior
weak var scrollView: UIScrollView? {
didSet {
configureScrollable()
guard let scrollView = scrollView else { return }
scrollView.panGestureRecognizer.addTarget(self, action: #selector(handle(panGesture:)))
scrollBouncable = scrollView.bounces
scrollIndictorVisible = scrollView.showsVerticalScrollIndicator
}
}
weak var userScrollViewDelegate: UIScrollViewDelegate?
var safeAreaInsets: UIEdgeInsets! {
get {
return layoutAdapter.safeAreaInsets
}
set {
layoutAdapter.safeAreaInsets = newValue
}
get { return layoutAdapter.safeAreaInsets }
set { layoutAdapter.safeAreaInsets = newValue }
}
unowned let viewcontroller: FloatingPanelController
private(set) var state: FloatingPanelPosition = .tip {
didSet {
switch state {
case .full:
backdropView.alpha = layoutAdapter.layout.backdropAlpha
default:
backdropView.alpha = 0.0
}
configureScrollable()
}
didSet { viewcontroller.delegate?.floatingPanelDidChangePosition(viewcontroller) }
}
var layoutAdapter: FloatingPanelLayoutAdapter
var behavior: FloatingPanelBehavior
private var isBottomState: Bool {
let remains = layoutAdapter.layout.supportedPositions.filter { $0.rawValue > state.rawValue }
return remains.count == 0
}
let panGesture: FloatingPanelPanGestureRecognizer
var isRemovalInteractionEnabled: Bool = false
private var animator: UIViewPropertyAnimator?
private let panGesture: UIPanGestureRecognizer
private var initialFrame: CGRect = .zero
private var initialScrollOffset: CGPoint = .zero
private var transOffsetY: CGFloat = 0
private var interactionInProgress: Bool = false
// Scroll handling
private var stopScrollDeceleration: Bool = false
private var scrollBouncable = false
private var scrollIndictorVisible = false
// MARK: - Interface
init(_ vc: FloatingPanelController, layout: FloatingPanelLayout, behavior: FloatingPanelBehavior) {
viewcontroller = vc
surfaceView = vc.view as! FloatingPanelSurfaceView
@@ -58,10 +65,14 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
backdropView.backgroundColor = .black
backdropView.alpha = 0.0
self.layoutAdapter = FloatingPanelLayoutAdapter(surfaceView: surfaceView, layout: layout)
self.layoutAdapter = FloatingPanelLayoutAdapter(surfaceView: surfaceView,
backdropView: backdropView,
layout: layout)
self.behavior = behavior
panGesture = UIPanGestureRecognizer()
state = layoutAdapter.layout.initialPosition
panGesture = FloatingPanelPanGestureRecognizer()
if #available(iOS 11.0, *) {
panGesture.name = "FloatingPanelSurface"
@@ -74,7 +85,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
panGesture.delegate = self
}
func layoutViews(in vc: UIViewController) {
func setUpViews(in vc: UIViewController) {
unowned let view = vc.view!
view.insertSubview(backdropView, belowSubview: surfaceView)
@@ -84,11 +95,43 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
}
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) {
if to != .full {
lockScrollView()
}
if animated {
let animator = behavior.presentAnimator(from: state, to: to)
let animator: UIViewPropertyAnimator
switch (from, to) {
case (nil, 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):
animator = behavior.removeAnimator(self.viewcontroller, from: from)
case (nil, nil):
fatalError()
}
animator.addAnimations { [weak self] in
self?.updateLayout(to: to)
self?.state = to
guard let self = self else { return }
self.updateLayout(to: to)
if let to = to {
self.state = to
}
}
animator.addCompletion { _ in
completion?()
@@ -96,124 +139,262 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
animator.startAnimation()
} else {
self.updateLayout(to: to)
self.state = to
if let to = to {
self.state = to
}
completion?()
}
}
func present(animated: Bool, completion: (() -> Void)? = nil) {
self.layoutAdapter.activateLayout(of: nil)
move(to: layoutAdapter.layout.initialPosition, animated: animated, completion: completion)
// MARK: - Layout update
private func updateLayout(to target: FloatingPanelPosition?) {
self.layoutAdapter.activateLayout(of: target)
}
func dismiss(animated: Bool, completion: (() -> Void)? = nil) {
if animated {
let animator = behavior.dismissAnimator(from: state)
animator.addAnimations { [weak self] in
self?.updateLayout(to: nil)
}
animator.addCompletion { _ in
completion?()
}
animator.startAnimation()
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 nextY = layoutAdapter.positionY(for: next)
let preY = layoutAdapter.positionY(for: pre)
let nextAlpha = layoutAdapter.layout.backdropAlphaFor(position: next)
let preAlpha = layoutAdapter.layout.backdropAlphaFor(position: pre)
if preY == nextY {
return preAlpha
} else {
self.updateLayout(to: nil)
completion?()
return preAlpha + max(min(1.0, 1.0 - (nextY - currentY) / (nextY - preY) ), 0.0) * (nextAlpha - preAlpha)
}
}
// MARK: - UIGestureRecognizerDelegate
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
log.debug("gestureRecognizer", gestureRecognizer,
"shouldRecognizeSimultaneouslyWith", otherGestureRecognizer)
if #available(iOS 11.0, *) {
log.debug("gestureRecognizer",
String(describing: gestureRecognizer.name),
"shouldRecognizeSimultaneouslyWith",
String(describing: otherGestureRecognizer.name))
}
guard gestureRecognizer == panGesture else { return false }
switch (gestureRecognizer, otherGestureRecognizer) {
case (panGesture, scrollView?.panGestureRecognizer):
return state == .full
case (panGesture, is UIPanGestureRecognizer):
return false
default:
return true
}
/* log.debug("shouldRecognizeSimultaneouslyWith", otherGestureRecognizer) */
// 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
}
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
// Do not begin any gestures until the pan gesture fails at non-full position.
return gestureRecognizer == panGesture && state != .full
}
guard gestureRecognizer == panGesture else { return false }
public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
/* log.debug("shouldBeRequiredToFailBy", otherGestureRecognizer) */
// The tracking scroll view's gestures should begin without waiting for the pan gesture failure.
// `scrollView.gestureRecognizers` can contains the following gestures
// * UIScrollViewDelayedTouchesBeganGestureRecognizer
// * UIScrollViewPanGestureRecognizer (scrollView.panGestureRecognizer)
// * _UIDragAutoScrollGestureRecognizer
// * _UISwipeActionPanGestureRecognizer
// * UISwipeDismissalGestureRecognizer
if let scrollView = scrollView,
let scrollGestureRecognizers = scrollView.gestureRecognizers,
scrollGestureRecognizers.contains(otherGestureRecognizer) {
return false
}
// Long press gesture should begin without waiting for the pan gesture failure.
if otherGestureRecognizer is UILongPressGestureRecognizer {
return false
}
// Do not begin any other gestures until the pan gesture fails.
return true
}
private func configureScrollable() {
switch state {
case .full:
scrollView?.isScrollEnabled = true
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool {
guard gestureRecognizer == panGesture 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
}
}
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:
scrollView?.isScrollEnabled = false
// Should begin the pan gesture witout waiting tap/long press gestures fail
return false
}
}
@objc func handle(panGesture: UIPanGestureRecognizer) {
let translation = panGesture.translation(in: panGesture.view!.superview)
let velocity = panGesture.velocity(in: panGesture.view)
let location = panGesture.location(in: panGesture.view)
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
}
if #available(iOS 11.0, *) {
log.debug("Gesture >>>>", panGesture.name!)
}
if let scrollView = scrollView, scrollView.frame.contains(location), interactionInProgress == false {
log.debug("ScrollView.contentOffset >>>", scrollView.contentOffset)
if state == .full {
if scrollView.contentOffset.y > scrollView.contentOffsetZero.y {
return
// MARK: - Gesture handling
private let offsetThreshold: CGFloat = 5.0 // Optimal value from testing
@objc func handle(panGesture: UIPanGestureRecognizer) {
log.debug("Gesture >>>>", panGesture)
let velocity = panGesture.velocity(in: panGesture.view)
switch panGesture {
case scrollView?.panGestureRecognizer:
guard let scrollView = scrollView else { return }
log.debug("SrollPanGesture ScrollView.contentOffset >>>", scrollView.contentOffset.y, scrollView.contentSize, scrollView.bounds.size)
// Prevent scoll slip by the top bounce when the scroll view's height is
// less than the content's height
if scrollView.isDecelerating == false, scrollView.contentSize.height > scrollView.bounds.height {
scrollView.bounces = (scrollView.contentOffset.y > offsetThreshold)
}
if surfaceView.frame.minY > layoutAdapter.topY {
switch state {
case .full:
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.
// It causes a buggy scrolling deceleration because `state` becomes
// a target position in animating the panel on the interaction from full.
return
}
// Fix the scroll offset in moving the panel from half and tip.
scrollView.contentOffset.y = initialScrollOffset.y
}
if scrollView.isDecelerating {
return
// Always hide a scroll indicator at the non-top.
if interactionInProgress {
lockScrollView()
}
if interactionInProgress == false, velocity.y < 0 {
return
} else {
// Always show a scroll indicator at the top.
if interactionInProgress {
unlockScrollView()
}
}
scrollView.contentOffset.y = scrollView.contentOffsetZero.y
case panGesture:
let translation = panGesture.translation(in: panGesture.view!.superview)
let location = panGesture.location(in: panGesture.view)
log.debug(panGesture.state, ">>>", "translation: \(translation.y), velocity: \(velocity.y)")
if shouldScrollViewHandleTouch(scrollView, point: location, velocity: velocity) {
return
}
if let animator = self.animator {
animator.stopAnimation(true)
self.animator = nil
}
switch panGesture.state {
case .began:
panningBegan()
case .changed:
if interactionInProgress == false {
startInteraction(with: translation)
}
panningChange(with: translation)
case .ended, .cancelled, .failed:
panningEnd(with: translation, velocity: velocity)
case .possible:
break
}
default:
return
}
log.debug(panGesture.state, ">>>", "{ translation: \(translation), velocity: \(velocity) }")
switch panGesture.state {
case .began:
panningBegan()
case .changed:
panningChange(with: translation)
case .ended, .cancelled, .failed:
panningEnd(with: translation, velocity: velocity)
case .possible:
break
}
private func shouldScrollViewHandleTouch(_ scrollView: UIScrollView?, point: CGPoint, velocity: CGPoint) -> Bool {
// When no scrollView, nothing to handle.
guard let scrollView = scrollView else { return false }
// For _UISwipeActionPanGestureRecognizer
if let scrollGestureRecognizers = scrollView.gestureRecognizers {
for gesture in scrollGestureRecognizers {
guard gesture.state == .began || gesture.state == .changed
else { continue }
if gesture != scrollView.panGestureRecognizer {
return true
}
}
}
guard
state == .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.
!grabberAreaFrame.contains(point) // When point within grabber area, don't scroll.
else {
return false
}
log.debug("ScrollView.contentOffset >>>", scrollView.contentOffset.y)
let offset = scrollView.contentOffset.y - scrollView.contentOffsetZero.y
if abs(offset) > offsetThreshold {
return true
}
if scrollView.isDecelerating {
return true
}
if velocity.y < 0 {
return true
}
return false
}
private func panningBegan() {
// A user interaction does not always start from Began state of the pan gesture
// because it can be recognized in scrolling a content in a content view controller.
// So I don't nothing here.
log.debug("panningBegan \(initialFrame)")
// So do nothing here.
log.debug("panningBegan")
}
private func panningChange(with translation: CGPoint) {
log.debug("panningChange")
if interactionInProgress == false {
startInteraction(with: translation)
}
let currentY = getCurrentY(from: initialFrame, with: translation)
var frame = initialFrame
frame.origin.y = getCurrentY(from: initialFrame, with: translation)
frame.origin.y = currentY
surfaceView.frame = frame
backdropView.alpha = getBackdropAlpha(with: translation)
viewcontroller.delegate?.floatingPanelDidMove(viewcontroller)
backdropView.alpha = updateBackdropAlpha(with: translation)
}
private func panningEnd(with translation: CGPoint, velocity: CGPoint) {
@@ -222,35 +403,73 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
initialFrame = surfaceView.frame
}
stopScrollDeceleration = (surfaceView.frame.minY > layoutAdapter.topY) // Projecting the dragging to the scroll dragging or not
let targetPosition = self.targetPosition(with: translation, velocity: velocity)
let distance = self.distance(to: targetPosition, with: translation)
endInteraction(for: targetPosition)
if isRemovalInteractionEnabled, isBottomState {
if startRemovalAnimation(with: translation, velocity: velocity, distance: distance) {
return
}
}
viewcontroller.delegate?.floatingPanelDidEndDragging(viewcontroller, withVelocity: velocity, targetPosition: targetPosition)
viewcontroller.delegate?.floatingPanelWillBeginDecelerating(viewcontroller)
startAnimation(to: targetPosition, at: distance, with: velocity)
}
private func startRemovalAnimation(with translation: CGPoint, velocity: CGPoint, distance: CGFloat) -> 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 }
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 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
transOffsetY = translation.y
viewcontroller.delegate?.floatingPanelWillBeginDragging(viewcontroller)
if let scrollView = scrollView {
scrollView.isScrollEnabled = false
initialScrollOffset = scrollView.contentOffset
}
transOffsetY = translation.y
viewcontroller.delegate?.floatingPanelWillBeginDragging(viewcontroller)
interactionInProgress = true
}
private func endInteraction(for targetPosition: FloatingPanelPosition) {
log.debug("endInteraction for \(targetPosition)")
if let scrollView = scrollView {
if targetPosition == .full {
scrollView.isScrollEnabled = true
}
}
interactionInProgress = false
// Prevent to keep a scoll view indicator visible at the half/tip position
if targetPosition != .full {
lockScrollView()
}
}
private func getCurrentY(from rect: CGRect, with translation: CGPoint) -> CGFloat {
@@ -258,23 +477,28 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
let y = rect.offsetBy(dx: 0.0, dy: dy).origin.y
let topY = layoutAdapter.topY
let topInset = layoutAdapter.topInset
let topBuffer = layoutAdapter.layout.topInteractionBuffer
let bottomY = layoutAdapter.bottomY
let bottomBuffer = layoutAdapter.layout.bottomInteractionBuffer
return max(topY - topInset + topBuffer, min(bottomY + bottomBuffer, y))
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(topY - topBuffer, min(bottomY + bottomBuffer, y))
}
private func startAnimation(to targetPosition: FloatingPanelPosition, at distance: CGFloat, with velocity: CGPoint) {
let targetY = layoutAdapter.positionY(for: targetPosition)
let velocityVector = (distance != 0) ? CGVector(dx: 0, dy: velocity.y/distance) : .zero
let animator = behavior.interactionAnimator(to: targetPosition, with: velocityVector)
let velocityVector = (distance != 0) ? CGVector(dx: 0, dy: max(min(velocity.y/distance, 30.0), -30.0)) : .zero
let animator = behavior.interactionAnimator(self.viewcontroller, to: targetPosition, with: velocityVector)
animator.addAnimations { [weak self] in
guard let self = self else { return }
if self.state == targetPosition {
self.surfaceView.frame.origin.y = targetY
self.layoutAdapter.setBackdropAlpha(of: targetPosition)
} else {
self.updateLayout(to: targetPosition)
}
@@ -297,20 +521,14 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
log.debug("finishAnimation \(targetPosition)")
self.animator = nil
self.viewcontroller.delegate?.floatingPanelDidEndDecelerating(self.viewcontroller)
stopScrollDeceleration = false
// Don't unlock scroll view in animating view when presentation layer != model layer
if targetPosition == .full {
unlockScrollView()
}
}
private func updateLayout(to target: FloatingPanelPosition?) {
self.layoutAdapter.activateLayout(of: target)
}
private func updateBackdropAlpha(with translation: CGPoint) -> CGFloat {
let topY = layoutAdapter.topY
let middleY = layoutAdapter.middleY
let currentY = getCurrentY(from: initialFrame, with: translation)
return (1 - (currentY - topY) / (middleY - topY)) * layoutAdapter.layout.backdropAlpha
}
// Animation handling
private func distance(to targetPosition: FloatingPanelPosition, with translation: CGPoint) -> CGFloat {
let topY = layoutAdapter.topY
let middleY = layoutAdapter.middleY
@@ -326,6 +544,66 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
}
}
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 redirectionalPosition(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 ? .full : .half
case [.half, .tip]: return translation.y >= 0 ? .half : .tip
case [.full, .tip]: return translation.y >= 0 ? .full : .tip
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
}
}
}
// 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 {
@@ -335,27 +613,59 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
private func targetPosition(with translation: CGPoint, velocity: CGPoint) -> (FloatingPanelPosition) {
let currentY = getCurrentY(from: initialFrame, with: translation)
let supportedPositions = Set(layoutAdapter.layout.supportedPositions)
let supportedPositions: Set = layoutAdapter.layout.supportedPositions
assert(supportedPositions.count > 1)
if supportedPositions.count == 1 {
return state
}
switch supportedPositions {
case Set([.full, .half]):
case [.full, .half]:
return targetPosition(from: [.full, .half], at: currentY, velocity: velocity)
case [.half, .tip]:
return targetPosition(from: [.half, .tip], at: currentY, velocity: velocity)
case Set([.half, .tip]):
return targetPosition(from: [.half, .tip], at: currentY, velocity: velocity)
case Set([.full, .tip]):
case [.full, .tip]:
return targetPosition(from: [.full, .tip], at: currentY, velocity: velocity)
default:
/*
[topY|full]---[th1]---[middleY|default]---[th2]---[bottomY|collapsed]
[topY|full]---[th1]---[middleY|half]---[th2]---[bottomY|tip]
*/
let topY = layoutAdapter.topY
let middleY = layoutAdapter.middleY
let bottomY = layoutAdapter.bottomY
let th1 = (topY + middleY) / 2
let th2 = (middleY + bottomY) / 2
let target: FloatingPanelPosition
let forwardYDirection: Bool
switch state {
case .full:
target = .half
forwardYDirection = true
case .half:
if (currentY < middleY) {
target = .full
forwardYDirection = false
} else {
target = .tip
forwardYDirection = true
}
case .tip:
target = .half
forwardYDirection = false
}
let redirectionalProgress = max(min(behavior.redirectionalProgress(viewcontroller, from: state, to: target), 1.0), 0.0)
let th1: CGFloat
let th2: CGFloat
if forwardYDirection {
th1 = topY + (middleY - topY) * redirectionalProgress
th2 = middleY + (bottomY - middleY) * redirectionalProgress
} else {
th1 = middleY - (middleY - topY) * redirectionalProgress
th2 = bottomY - (bottomY - middleY) * redirectionalProgress
}
switch currentY {
case ..<th1:
@@ -395,7 +705,10 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
let topY = layoutAdapter.positionY(for: top)
let bottomY = layoutAdapter.positionY(for: bottom)
let th = (topY + bottomY) / 2
let target = top == state ? bottom : top
let redirectionalProgress = max(min(behavior.redirectionalProgress(viewcontroller, from: state, to: target), 1.0), 0.0)
let th = topY + (bottomY - topY) * redirectionalProgress
switch currentY {
case ..<th:
@@ -412,4 +725,70 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
}
}
}
// MARK: - ScrollView handling
private func lockScrollView() {
guard let scrollView = scrollView else { return }
scrollView.isDirectionalLockEnabled = true
scrollView.bounces = false
scrollView.showsVerticalScrollIndicator = false
}
private func unlockScrollView() {
guard let scrollView = scrollView else { return }
scrollView.isDirectionalLockEnabled = false
scrollView.bounces = scrollBouncable
scrollView.showsVerticalScrollIndicator = scrollIndictorVisible
}
// MARK: - UIScrollViewDelegate Intermediation
override func responds(to aSelector: Selector!) -> Bool {
return super.responds(to: aSelector) || userScrollViewDelegate?.responds(to: aSelector) == true
}
override func forwardingTarget(for aSelector: Selector!) -> Any? {
if userScrollViewDelegate?.responds(to: aSelector) == true {
return userScrollViewDelegate
} else {
return super.forwardingTarget(for: aSelector)
}
}
func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
if state != .full {
initialScrollOffset = scrollView.contentOffset
}
userScrollViewDelegate?.scrollViewDidEndScrollingAnimation?(scrollView)
}
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
if stopScrollDeceleration {
targetContentOffset.pointee = scrollView.contentOffset
stopScrollDeceleration = false
} else {
userScrollViewDelegate?.scrollViewWillEndDragging?(scrollView, withVelocity: velocity, targetContentOffset: targetContentOffset)
}
}
}
class FloatingPanelPanGestureRecognizer: UIPanGestureRecognizer {
override weak var delegate: UIGestureRecognizerDelegate? {
get {
return super.delegate
}
set {
guard newValue is FloatingPanel else {
let exception = NSException(name: .invalidArgumentException,
reason: "FloatingPanelController's built-in pan gesture recognizer must have its controller as its delegate.",
userInfo: nil)
exception.raise()
return
}
super.delegate = newValue
}
}
}
@@ -1,6 +1,6 @@
//
// Created by Shin Yamamoto on 2018/09/26.
// Copyright © 2018 scenee. All rights reserved.
// Copyright © 2018 Shin Yamamoto. All rights reserved.
//
import UIKit
+77 -36
View File
@@ -1,61 +1,102 @@
//
// Created by Shin Yamamoto on 2018/10/03.
// Copyright © 2018 scenee. All rights reserved.
// Copyright © 2018 Shin Yamamoto. All rights reserved.
//
import UIKit
public protocol FloatingPanelBehavior {
// Returns a UIViewPropertyAnimator object in interacting a floating panel by a user pan gesture
func interactionAnimator(to targetPosition: FloatingPanelPosition, with velocity: CGVector) -> UIViewPropertyAnimator
/// Returns the progress to redirect to the previous position
///
/// The progress is represented by a floating-point value between 0.0 and 1.0, inclusive, where 1.0 indicates the floating panel is impossible to move to the next posiiton. The default value is 0.5. Values less than 0.0 and greater than 1.0 are pinned to those limits.
func redirectionalProgress(_ fpc: FloatingPanelController, from: FloatingPanelPosition, to: FloatingPanelPosition) -> CGFloat
// Returns a UIViewPropertyAnimator object to present a floating panel
func presentAnimator(from: FloatingPanelPosition, to: FloatingPanelPosition) -> UIViewPropertyAnimator
// Returns a UIViewPropertyAnimator object to dismiss a floating panel
func dismissAnimator(from: FloatingPanelPosition) -> UIViewPropertyAnimator
/// Returns a UIViewPropertyAnimator object to project a floating panel to a position on finger up if the user dragged.
func interactionAnimator(_ fpc: FloatingPanelController, to targetPosition: FloatingPanelPosition, with velocity: CGVector) -> UIViewPropertyAnimator
/// Returns a UIViewPropertyAnimator object to add a floating panel to a position.
///
/// Its animator instance will be used to animate the surface view in `FloatingPanelController.addPanel(toParent:belowView:animated:)`.
/// Default is an animator with ease-in-out curve and 0.25 sec duration.
func addAnimator(_ fpc: FloatingPanelController, to: FloatingPanelPosition) -> UIViewPropertyAnimator
/// Returns a UIViewPropertyAnimator object to remove a floating panel from a position.
///
/// Its animator instance will be used to animate the surface view in `FloatingPanelController.removePanelFromParent(animated:completion:)`.
/// Default is an animator with ease-in-out curve and 0.25 sec duration.
func removeAnimator(_ fpc: FloatingPanelController, from: FloatingPanelPosition) -> UIViewPropertyAnimator
/// Returns a UIViewPropertyAnimator object to move a floating panel from a position to a position.
///
/// Its animator instance will be used to animate the surface view in `FloatingPanelController.move(to:animated:completion:)`.
/// Default is an animator with ease-in-out curve and 0.25 sec duration.
func moveAnimator(_ fpc: FloatingPanelController, from: FloatingPanelPosition, to: FloatingPanelPosition) -> UIViewPropertyAnimator
/// Returns a y-axis velocity to invoke a removal interaction at the bottom position.
///
/// Default is 10.0. This method is called when FloatingPanelController.isRemovalInteractionEnabled is true.
var removalVelocity: CGFloat { get }
/// Returns the threshold of the transition to invoke a removal interaction at the bottom position.
///
/// The progress is represented by a floating-point value between 0.0 and 1.0, inclusive, where 1.0 indicates the floating panel is impossible to invoke the removal interaction. The default value is 0.5. Values less than 0.0 and greater than 1.0 are pinned to those limits. This method is called when FloatingPanelController.isRemovalInteractionEnabled is true.
var removalProgress: CGFloat { get }
/// Returns a UIViewPropertyAnimator object to remove a floating panel with a velocity interactively at the bottom position.
///
/// Default is a spring animator with 1.0 damping ratio. This method is called when FloatingPanelController.isRemovalInteractionEnabled is true.
func removalInteractionAnimator(_ fpc: FloatingPanelController, with velocity: CGVector) -> UIViewPropertyAnimator
}
public extension FloatingPanelBehavior {
func presentAnimator(from: FloatingPanelPosition, to: FloatingPanelPosition) -> UIViewPropertyAnimator {
func redirectionalProgress(_ fpc: FloatingPanelController, from: FloatingPanelPosition, to: FloatingPanelPosition) -> CGFloat {
return 0.5
}
func addAnimator(_ fpc: FloatingPanelController, to: FloatingPanelPosition) -> UIViewPropertyAnimator {
return UIViewPropertyAnimator(duration: 0.25, curve: .easeInOut)
}
func dismissAnimator(from: FloatingPanelPosition) -> UIViewPropertyAnimator {
func removeAnimator(_ fpc: FloatingPanelController, from: FloatingPanelPosition) -> UIViewPropertyAnimator {
return UIViewPropertyAnimator(duration: 0.25, curve: .easeInOut)
}
func moveAnimator(_ fpc: FloatingPanelController, from: FloatingPanelPosition, to: FloatingPanelPosition) -> UIViewPropertyAnimator {
return UIViewPropertyAnimator(duration: 0.25, curve: .easeInOut)
}
var removalVelocity: CGFloat {
return 10.0
}
var removalProgress: CGFloat {
return 0.5
}
func removalInteractionAnimator(_ fpc: FloatingPanelController, with velocity: CGVector) -> UIViewPropertyAnimator {
log.debug("velocity", velocity)
let timing = UISpringTimingParameters(dampingRatio: 1.0,
frequencyResponse: 0.3,
initialVelocity: velocity)
return UIViewPropertyAnimator(duration: 0, timingParameters: timing)
}
}
class FloatingPanelDefaultBehavior: FloatingPanelBehavior {
func interactionAnimator(to targetPosition: FloatingPanelPosition, with velocity: CGVector) -> UIViewPropertyAnimator {
let timing = timeingCurve(to: targetPosition, with: velocity)
let duration = getDuration(with: velocity)
return UIViewPropertyAnimator(duration: duration, timingParameters: timing)
func interactionAnimator(_ fpc: FloatingPanelController, to targetPosition: FloatingPanelPosition, with velocity: CGVector) -> UIViewPropertyAnimator {
let timing = timeingCurve(with: velocity)
return UIViewPropertyAnimator(duration: 0, timingParameters: timing)
}
private func timeingCurve(with velocity: CGVector) -> UITimingCurveProvider {
log.debug("velocity", velocity)
let damping = self.getDamping(with: velocity)
return UISpringTimingParameters(dampingRatio: damping,
frequencyResponse: 0.3,
initialVelocity: velocity)
}
private let velocityThreshold: CGFloat = 8.0
private func getDuration(with velocity: CGVector) -> TimeInterval {
let dy = abs(velocity.dy)
switch dy {
case ..<1.0:
return 0.6
case 1.0..<velocityThreshold:
let a = ((dy - 1.0) / (velocityThreshold - 1.0))
return TimeInterval(0.6 - (0.2 * a))
case velocityThreshold...:
return 0.4
default:
fatalError()
}
}
private func timeingCurve(to: FloatingPanelPosition, with velocity: CGVector) -> UITimingCurveProvider {
log.debug("velocity", velocity)
let damping = self.getDamping(with: velocity)
let springTiming = UISpringTimingParameters(dampingRatio: damping,
initialVelocity: velocity)
return springTiming
}
private func getDamping(with velocity: CGVector) -> CGFloat {
let dy = abs(velocity.dy)
if dy > velocityThreshold {
+1 -1
View File
@@ -1,6 +1,6 @@
//
// Created by Shin Yamamoto on 2018/09/18.
// Copyright © 2018 scenee. All rights reserved.
// Copyright © 2018 Shin Yamamoto. All rights reserved.
//
#import <UIKit/UIKit.h>
+211 -51
View File
@@ -1,6 +1,6 @@
//
// Created by Shin Yamamoto on 2018/09/18.
// Copyright © 2018 scenee. All rights reserved.
// Copyright © 2018 Shin Yamamoto. All rights reserved.
//
import UIKit
@@ -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)
@@ -20,6 +22,11 @@ public protocol FloatingPanelControllerDelegate: class {
func floatingPanelDidEndDragging(_ vc: FloatingPanelController, withVelocity velocity: CGPoint, targetPosition: FloatingPanelPosition)
func floatingPanelWillBeginDecelerating(_ vc: FloatingPanelController) // called on finger up as we are moving
func floatingPanelDidEndDecelerating(_ vc: FloatingPanelController) // called when scroll view grinds to a halt
// called on start of dragging to remove its views from a parent view controller
func floatingPanelDidEndDraggingToRemove(_ vc: FloatingPanelController, withVelocity velocity: CGPoint)
// called when its views are removed from a parent view controller
func floatingPanelDidEndRemove(_ vc: FloatingPanelController)
}
public extension FloatingPanelControllerDelegate {
@@ -29,14 +36,18 @@ 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) {}
func floatingPanelWillBeginDecelerating(_ vc: FloatingPanelController) {}
func floatingPanelDidEndDecelerating(_ vc: FloatingPanelController) {}
func floatingPanelDidEndDraggingToRemove(_ vc: FloatingPanelController, withVelocity velocity: CGPoint) {}
func floatingPanelDidEndRemove(_ vc: FloatingPanelController) {}
}
public enum FloatingPanelPosition: Int {
public enum FloatingPanelPosition: Int, CaseIterable {
case full
case half
case tip
@@ -47,6 +58,12 @@ public enum FloatingPanelPosition: Int {
///
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
case never
}
/// The delegate of the floating panel controller object.
public weak var delegate: FloatingPanelControllerDelegate?
@@ -60,25 +77,72 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
return floatingPanel.backdropView
}
/// Returns the scroll view that the conroller tracks.
/// Returns the scroll view that the controller tracks.
public weak var scrollView: UIScrollView? {
return floatingPanel.scrollView
}
// The underlying gesture recognizer for pan gestures
public var panGestureRecognizer: UIPanGestureRecognizer {
return floatingPanel.panGesture
}
/// The current position of the floating panel controller's contents.
public var position: FloatingPanelPosition {
return floatingPanel.state
}
/// The layout object managed by the controller
public var layout: FloatingPanelLayout {
return floatingPanel.layoutAdapter.layout
}
/// The behavior object managed by the controller
public var behavior: FloatingPanelBehavior {
return floatingPanel.behavior
}
/// The content insets of the tracking scroll view derived from the safe area of the parent view
public var adjustedContentInsets: UIEdgeInsets {
return floatingPanel.layoutAdapter.adjustedContentInsets
}
/// The behavior for determining the adjusted content offsets.
///
/// This property specifies how the content area of the tracking scroll view is modified using `adjustedContentInsets`. The default value of this property is FloatingPanelController.ContentInsetAdjustmentBehavior.always.
public var contentInsetAdjustmentBehavior: ContentInsetAdjustmentBehavior = .always
/// A Boolean value that determines whether the removal interaction is enabled.
public var isRemovalInteractionEnabled: Bool {
set { floatingPanel.isRemovalInteractionEnabled = newValue }
get { return floatingPanel.isRemovalInteractionEnabled }
}
/// The view controller responsible for the content portion of the floating panel.
public var contentViewController: UIViewController? {
set { set(contentViewController: newValue) }
get { return _contentViewController }
}
private var _contentViewController: UIViewController?
private var floatingPanel: FloatingPanel!
private var layoutInsetsObservations: [NSKeyValueObservation] = []
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
floatingPanel = FloatingPanel(self,
layout: fetchLayout(for: self.traitCollection),
behavior: fetchBehavior(for: self.traitCollection))
}
/// Initialize a newly created a floating panel controller.
/// Initialize a newly created floating panel controller.
public init() {
super.init(nibName: nil, bundle: nil)
floatingPanel = FloatingPanel(self,
layout: fetchLayout(for: self.traitCollection),
behavior: fetchBehavior(for: self.traitCollection))
}
/// Creates the view that the controller manages.
@@ -89,25 +153,15 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
view.backgroundColor = .white
self.view = view as UIView
let layout = fetchLayout(for: self.traitCollection)
let behavior = fetchBehavior(for: self.traitCollection)
floatingPanel = FloatingPanel(self,
layout: layout,
behavior: behavior)
}
public override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) {
super.willTransition(to: newCollection, with: coordinator)
// Change layout for a new trait collection
floatingPanel.layoutAdapter.layout = fetchLayout(for: newCollection)
updateLayout(for: newCollection)
floatingPanel.behavior = fetchBehavior(for: newCollection)
guard let parent = parent else { fatalError() }
floatingPanel.layoutAdapter.prepareLayout(toParent: parent)
floatingPanel.layoutAdapter.activateLayout(of: floatingPanel.state)
}
public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
@@ -115,19 +169,18 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
guard previousTraitCollection != traitCollection else { return }
if let parent = parent {
floatingPanel.safeAreaInsets = parent.layoutInsets
self.update(safeAreaInsets: parent.layoutInsets)
}
floatingPanel.layoutAdapter.updateHeight()
floatingPanel.backdropView.isHidden = (traitCollection.verticalSizeClass == .compact)
}
public override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// Must set `parent.view.layoutInsets` to floatingPanel.safeAreaInsets` here
// because it ensures that parent.view.layoutInsets` has a correct value.
// 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 {
floatingPanel.safeAreaInsets = parent.layoutInsets
floatingPanel.backdropView.frame = parent.view.bounds
self.update(safeAreaInsets: parent.layoutInsets)
}
}
@@ -144,21 +197,49 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
return self.delegate?.floatingPanel(self, behaviorFor: traitCollection) ?? FloatingPanelDefaultBehavior()
}
// MARK: Container view controller responsibilities
private func update(safeAreaInsets: UIEdgeInsets) {
// preserve the current content offset
let contentOffset = scrollView?.contentOffset
/// Adds the controller as a child of the specified view controller.
floatingPanel.safeAreaInsets = safeAreaInsets
scrollView?.contentOffset = contentOffset ?? .zero
switch contentInsetAdjustmentBehavior {
case .always:
scrollView?.contentInset = adjustedContentInsets
scrollView?.scrollIndicatorInsets = adjustedContentInsets
default:
break
}
}
private func updateLayout(for traitCollection: UITraitCollection) {
floatingPanel.layoutAdapter.layout = fetchLayout(for: traitCollection)
guard let parent = parent else { return }
floatingPanel.layoutAdapter.prepareLayout(toParent: parent)
floatingPanel.layoutAdapter.activateLayout(of: floatingPanel.state)
}
// MARK: - Container view controller interface
/// 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 conatiner view controller object isn't applicable.
/// - belowView: Insert the surface view managed by the controller below the specified view. As default, the surface view will be added to the end of the parent list of subviews.
/// - parent: A parent view controller object that displays FloatingPanelController's view. A container view controller object isn't applicable.
/// - belowView: Insert the surface view managed by the controller below the specified view. By default, the surface view will be added to the end of the parent list of subviews.
/// - animated: Pass true to animate the presentation; otherwise, pass false.
public func add(toParent parent: UIViewController, belowView: UIView? = nil, animated: Bool = false) {
public func addPanel(toParent parent: UIViewController, belowView: UIView? = nil, animated: Bool = false) {
guard self.parent == nil else {
log.warning("Already added to a parent(\(parent))")
return
}
precondition((parent is UINavigationController) == false, "UINavigationController displays only one child view controller at a time.")
precondition((parent is UITableViewController) == false, "UITableViewController should not be the parent because the view hierarchy will be break in reusing cells.")
precondition((parent is UICollectionViewController) == false, "UICollectionViewController should not be the parent because the view hierarchy will be break in reusing cells.")
precondition((parent is UITabBarController) == false, "UITabBarController displays child view controllers with a radio-style selection interface")
precondition((parent is UISplitViewController) == false, "UISplitViewController manages two child view controllers in a master-detail interface")
precondition((parent is UITableViewController) == false, "UITableViewController should not be the parent because the view is a table view so that a floating panel doens't work well")
precondition((parent is UICollectionViewController) == false, "UICollectionViewController should not be the parent because the view is a collection view so that a floating panel doens't work well")
view.frame = parent.view.bounds
if let belowView = belowView {
@@ -167,29 +248,57 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
parent.view.addSubview(self.view)
}
layoutInsetsObservations.removeAll()
// 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()
}
parent.addChild(self)
// Must set a layout again here because `self.traitCollection` is applied correctly on it's added to a parent VC
// 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.layoutViews(in: parent)
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)
}
}
/// Removes the view controller from its parent.
/// Removes the controller and the managed view from its parent view controller
/// - Parameters:
/// - animated: Pass true to animate the presentation; otherwise, pass false.
/// - completion: The block to execute after the view controller is dismissed. This block has no return value and takes no parameters. You may specify nil for this parameter.
public func removeFromParent(animated: Bool = false, completion: (() -> Void)? = nil) {
guard self.parent != nil else { return }
public func removePanelFromParent(animated: Bool, completion: (() -> Void)? = nil) {
guard self.parent != nil else {
completion?()
return
}
layoutInsetsObservations.removeAll()
floatingPanel.dismiss(animated: animated) { [weak self] in
guard let self = self else { return }
self.willMove(toParent: nil)
self.view.removeFromSuperview()
self.backdropView.removeFromSuperview()
self.removeFromParent()
completion?()
}
@@ -199,30 +308,81 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
/// - Parameters:
/// - to: Pass a FloatingPanelPosition value to move the surface view to the position.
/// - animated: Pass true to animate the presentation; otherwise, pass false.
/// - completion: The block to execute after the view controller is dismissed. This block has no return value and takes no parameters. You may specify nil for this parameter.
/// - 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) {
floatingPanel.move(to: to, animated: animated, completion: completion)
}
/// Presents the specified view controller as the content view controller in the surface view interface.
public override func show(_ vc: UIViewController, sender: Any?) {
let surfaceView = self.view as! FloatingPanelSurfaceView
surfaceView.contentView.addSubview(vc.view)
vc.view.frame = surfaceView.contentView.bounds
vc.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
vc.view.topAnchor.constraint(equalTo: surfaceView.contentView.topAnchor, constant: 0.0),
vc.view.leftAnchor.constraint(equalTo: surfaceView.contentView.leftAnchor, constant: 0.0),
vc.view.rightAnchor.constraint(equalTo: surfaceView.contentView.rightAnchor, constant: 0.0),
vc.view.bottomAnchor.constraint(equalTo: surfaceView.contentView.bottomAnchor, constant: 0.0),
])
addChild(vc)
vc.didMove(toParent: self)
/// 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.view.removeFromSuperview()
vc.removeFromParent()
}
if let vc = contentViewController {
let surfaceView = self.view as! FloatingPanelSurfaceView
surfaceView.add(childView: vc.view)
addChild(vc)
vc.didMove(toParent: self)
}
_contentViewController = contentViewController
}
/// Tracks the specified scroll view for the inteface to correspond with the scroll pan gesture.
@available(*, unavailable, renamed: "set(contentViewController:)")
public override func show(_ vc: UIViewController, sender: Any?) {
if let target = self.parent?.targetViewController(forAction: #selector(UIViewController.show(_:sender:)), sender: sender) {
target.show(vc, sender: sender)
}
}
@available(*, unavailable, renamed: "set(contentViewController:)")
public override func showDetailViewController(_ vc: UIViewController, sender: Any?) {
if let target = self.parent?.targetViewController(forAction: #selector(UIViewController.showDetailViewController(_:sender:)), sender: sender) {
target.showDetailViewController(vc, sender: sender)
}
}
// MARK: - Scroll view tracking
/// Tracks the specified scroll view to correspond with the scroll.
///
/// - Attention:
/// The specified scroll view must be already assigned to the delegate property because the controller intermediates between the various delegate methods.
///
public func track(scrollView: UIScrollView) {
floatingPanel.scrollView = scrollView
if scrollView.delegate !== floatingPanel {
floatingPanel.userScrollViewDelegate = scrollView.delegate
scrollView.delegate = floatingPanel
}
switch contentInsetAdjustmentBehavior {
case .always:
if #available(iOS 11.0, *) {
scrollView.contentInsetAdjustmentBehavior = .never
} else {
children.forEach { (vc) in
vc.automaticallyAdjustsScrollViewInsets = false
}
}
default:
break
}
}
// MARK: - Utilities
/// Updates the layout object from the delegate and lays out the views managed
/// by the controller immediately.
///
/// This method updates the `FloatingPanelLayout` object from the delegate and
/// then it calls `layoutIfNeeded()` of the parent's 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)
}
/// Returns the y-coordinate of the point at the origin of the surface view
+139 -59
View File
@@ -1,46 +1,62 @@
//
// Created by Shin Yamamoto on 2018/09/27.
// Copyright © 2018 scenee. All rights reserved.
// Copyright © 2018 Shin Yamamoto. All rights reserved.
//
import UIKit
public protocol FloatingPanelLayout: class {
/// Returns the initial position of a floating panel
/// Returns the initial position of a floating panel.
var initialPosition: FloatingPanelPosition { get }
/// Returns an array of FloatingPanelPosition object to tell the applicable position the floating panel controller
var supportedPositions: [FloatingPanelPosition] { get }
/// Return the interaction buffer of full position. Default is 6.0.
/// Returns a set of FloatingPanelPosition objects to tell the applicable positions of the floating panel controller. Default is all of them.
var supportedPositions: Set<FloatingPanelPosition> { get }
/// Return the interaction buffer to the top from the top position. Default is 6.0.
var topInteractionBuffer: CGFloat { get }
/// Return the interaction buffer of full position. Default is 6.0.
/// Return the interaction buffer to the bottom from the bottom position. Default is 6.0.
var bottomInteractionBuffer: CGFloat { get }
/// Returns a CGFloat value for a floating panel position(full, half, tip).
/// A value for full position indicates an inset from the safe area top.
/// On the other hand, values fro half and tip positions indicate insets from the safe area bottom.
/// 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.
func insetFor(position: FloatingPanelPosition) -> CGFloat?
/// Returns layout constraints for a surface view of a floaitng panel.
/// The layout constraints must not include ones for topAnchor and bottomAnchor
/// because constarints for them will be added by the floating panel controller.
/// Returns X-axis and width layout constraints of the surface view of a floating panel.
/// You must not include any Y-axis and height layout constraints of the surface view
/// because their constraints will be configured by the floating panel controller.
/// By default, the width of a surface view fits a safe area.
func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint]
/// Return the backdrop alpha of black color in full position. Default is 0.3.
var backdropAlpha: CGFloat { get }
/// Returns a CGFloat value to determine the backdrop view's alpha for a position.
///
/// Default is 0.3 at full position, otherwise 0.0.
func backdropAlphaFor(position: FloatingPanelPosition) -> CGFloat
}
public extension FloatingPanelLayout {
var backdropAlpha: CGFloat { return 0.3 }
var topInteractionBuffer: CGFloat { return 6.0 }
var bottomInteractionBuffer: CGFloat { return 6.0 }
var supportedPositions: Set<FloatingPanelPosition> {
return Set(FloatingPanelPosition.allCases)
}
func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint] {
return [
surfaceView.leftAnchor.constraint(equalTo: view.sideLayoutGuide.leftAnchor, constant: 0.0),
surfaceView.rightAnchor.constraint(equalTo: view.sideLayoutGuide.rightAnchor, constant: 0.0),
]
}
func backdropAlphaFor(position: FloatingPanelPosition) -> CGFloat {
return position == .full ? 0.3 : 0.0
}
}
public class FloatingPanelDefaultLayout: FloatingPanelLayout {
public var supportedPositions: [FloatingPanelPosition] {
return [.full, .half, .tip]
}
public var initialPosition: FloatingPanelPosition {
return .half
}
@@ -52,20 +68,13 @@ public class FloatingPanelDefaultLayout: FloatingPanelLayout {
case .tip: return 69.0
}
}
public func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint] {
return [
surfaceView.leftAnchor.constraint(equalTo: view.sideLayoutGuide.leftAnchor, constant: 0.0),
surfaceView.rightAnchor.constraint(equalTo: view.sideLayoutGuide.rightAnchor, constant: 0.0),
]
}
}
public class FloatingPanelDefaultLandscapeLayout: FloatingPanelLayout {
public var initialPosition: FloatingPanelPosition {
return .tip
}
public var supportedPositions: [FloatingPanelPosition] {
public var supportedPositions: Set<FloatingPanelPosition> {
return [.full, .tip]
}
@@ -76,43 +85,52 @@ public class FloatingPanelDefaultLandscapeLayout: FloatingPanelLayout {
default: return nil
}
}
public func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint] {
return [
surfaceView.leftAnchor.constraint(equalTo: view.sideLayoutGuide.leftAnchor, constant: 8.0),
surfaceView.widthAnchor.constraint(equalToConstant: 291),
]
}
}
class FloatingPanelLayoutAdapter {
private weak var parent: UIViewController!
private weak var surfaceView: FloatingPanelSurfaceView!
private weak var backdropView: FloatingPanelBackdropView!
var layout: FloatingPanelLayout
var layout: FloatingPanelLayout {
didSet {
checkLayoutConsistance()
}
}
var safeAreaInsets: UIEdgeInsets = .zero
var safeAreaInsets: UIEdgeInsets = .zero {
didSet {
updateHeight()
checkLayoutConsistance()
}
}
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] = []
private var tipConstraints: [NSLayoutConstraint] = []
private var offConstraints: [NSLayoutConstraint] = []
private var heightConstraints: NSLayoutConstraint? = nil
private var heightConstraints: [NSLayoutConstraint] = []
var topInset: CGFloat {
private var fullInset: CGFloat {
return layout.insetFor(position: .full) ?? 0.0
}
var halfInset: CGFloat {
private var halfInset: CGFloat {
return layout.insetFor(position: .half) ?? 0.0
}
var tipInset: CGFloat {
private var tipInset: CGFloat {
return layout.insetFor(position: .tip) ?? 0.0
}
var topY: CGFloat {
return (safeAreaInsets.top + topInset)
if layout.supportedPositions.contains(.full) {
return (safeAreaInsets.top + fullInset)
} else {
return middleY
}
}
var middleY: CGFloat {
@@ -120,7 +138,22 @@ class FloatingPanelLayoutAdapter {
}
var bottomY: CGFloat {
return surfaceView.superview!.bounds.height - (safeAreaInsets.bottom + tipInset)
if layout.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 adjustedContentInsets: UIEdgeInsets {
return UIEdgeInsets(top: 0.0,
left: 0.0,
bottom: safeAreaInsets.bottom,
right: 0.0)
}
func positionY(for pos: FloatingPanelPosition) -> CGFloat {
@@ -134,27 +167,38 @@ class FloatingPanelLayoutAdapter {
}
}
init(surfaceView: FloatingPanelSurfaceView, layout: FloatingPanelLayout) {
init(surfaceView: FloatingPanelSurfaceView, backdropView: FloatingPanelBackdropView, layout: FloatingPanelLayout) {
self.layout = layout
self.surfaceView = surfaceView
// Verify layout configurations
assert(layout.supportedPositions.count > 1)
assert(layout.supportedPositions.contains(layout.initialPosition))
if halfInset > 0 {
assert(halfInset >= tipInset)
}
self.backdropView = backdropView
}
func prepareLayout(toParent parent: UIViewController) {
self.parent = parent
surfaceView.translatesAutoresizingMaskIntoConstraints = false
backdropView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.deactivate(fixedConstraints + fullConstraints + halfConstraints + tipConstraints + offConstraints)
fixedConstraints = layout.prepareLayout(surfaceView: surfaceView, in: parent.view!)
// Fixed constraints of surface and backdrop views
let surfaceConstraints = layout.prepareLayout(surfaceView: surfaceView, in: parent.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),
]
fixedConstraints = surfaceConstraints + backdropConstraints
// Flexible surface constarints for full, half, tip and off
fullConstraints = [
surfaceView.topAnchor.constraint(equalTo: parent.layoutGuide.topAnchor,
constant: topInset),
constant: fullInset),
]
halfConstraints = [
surfaceView.topAnchor.constraint(equalTo: parent.layoutGuide.bottomAnchor,
@@ -165,7 +209,7 @@ class FloatingPanelLayoutAdapter {
constant: -tipInset),
]
offConstraints = [
surfaceView.topAnchor.constraint(equalTo: parent.layoutGuide.bottomAnchor, constant: 0.0),
surfaceView.topAnchor.constraint(equalTo: parent.view.bottomAnchor, constant: 0.0),
]
}
@@ -178,18 +222,24 @@ class FloatingPanelLayoutAdapter {
}
}
if let heightConstraints = self.heightConstraints {
NSLayoutConstraint.deactivate([heightConstraints])
}
let heightConstraints = surfaceView.heightAnchor.constraint(equalToConstant: UIScreen.main.bounds.height + heightBuffer)
NSLayoutConstraint.activate([heightConstraints])
self.heightConstraints = heightConstraints
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)
]
NSLayoutConstraint.activate(heightConstraints)
surfaceView.set(bottomOverflow: heightBuffer)
}
func activateLayout(of state: FloatingPanelPosition?) {
defer {
surfaceView.superview!.layoutIfNeeded()
}
setBackdropAlpha(of: state)
NSLayoutConstraint.activate(fixedConstraints)
@@ -216,4 +266,34 @@ class FloatingPanelLayoutAdapter {
NSLayoutConstraint.activate(tipConstraints)
}
}
func setBackdropAlpha(of target: FloatingPanelPosition?) {
if let target = target {
self.backdropView.alpha = layout.backdropAlphaFor(position: target)
} else {
self.backdropView.alpha = 0.0
}
}
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 halfInset > 0 {
assert(halfInset > tipInset, "Invalid half and tip insets")
}
if fullInset > 0 {
assert(middleY > topY, "Invalid insets")
assert(bottomY > topY, "Invalid insets")
}
}
}
@@ -1,6 +1,6 @@
//
// Created by Shin Yamamoto on 2018/09/26.
// Copyright © 2018 scenee. All rights reserved.
// Copyright © 2018 Shin Yamamoto. All rights reserved.
//
import UIKit
@@ -13,20 +13,26 @@ public class FloatingPanelSurfaceView: UIView {
/// A GrabberHandleView object displayed at the top of the surface view
public var grabberHandle: GrabberHandleView!
/// The height of the grabber bar area
public static var topGrabberBarHeight: CGFloat {
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!
private var color: UIColor? = .white { didSet { setNeedsDisplay() } }
private var color: UIColor? = .white { didSet { setNeedsLayout() } }
private var bottomOverflow: CGFloat = 0.0 // Must not call setNeedsLayout()
public override var backgroundColor: UIColor? {
get { return color }
set {
color = newValue
setNeedsDisplay()
}
set { color = newValue }
}
/// The radius to use when drawing rounded corners
/// The radius to use when drawing top rounded corners.
///
/// `self.contentView` is masked with the top rounded corners automatically on iOS 11 and later.
/// On iOS 10, they are not automatically masked because of a UIVisualEffectView issue. See https://forums.developer.apple.com/thread/50854
public var cornerRadius: CGFloat = 0.0 { didSet { setNeedsLayout() } }
/// A Boolean indicating whether the surface shadow is displayed.
@@ -50,14 +56,11 @@ public class FloatingPanelSurfaceView: UIView {
/// The color of the surface border.
public var borderWidth: CGFloat = 0.0 { didSet { setNeedsLayout() } }
private var backgroundLayer: CAShapeLayer! { didSet { setNeedsLayout() } }
private var shadowLayer: CAShapeLayer! { didSet { setNeedsLayout() } }
public struct Default {
private struct Default {
public static let grabberTopPadding: CGFloat = 6.0
}
private var topGrabberBarHeight: CGFloat {
return Default.grabberTopPadding * 2 + GrabberHandleView.Default.height
}
override init(frame: CGRect) {
super.init(frame: frame)
@@ -71,11 +74,16 @@ public class FloatingPanelSurfaceView: UIView {
private func render() {
super.backgroundColor = .clear
self.clipsToBounds = false
let backgroundLayer = CAShapeLayer()
layer.insertSublayer(backgroundLayer, at: 0)
self.backgroundLayer = backgroundLayer
let contentView = FloatingPanelSurfaceContentView()
addSubview(contentView)
self.contentView = contentView as UIView
// contentView.backgroundColor = .lightGray
contentView.backgroundColor = color
contentView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
contentView.topAnchor.constraint(equalTo: topAnchor, constant: 0.0),
@@ -99,37 +107,69 @@ public class FloatingPanelSurfaceView: UIView {
public override func layoutSubviews() {
super.layoutSubviews()
updateShadowLayer()
// Don't use `contentView.layer.mask` because of UIVisualEffectView issue on ios10, https://forums.developer.apple.com/thread/50854
contentView.layer.cornerRadius = cornerRadius
contentView.clipsToBounds = true
updateLayers()
updateContentViewMask()
contentView.layer.borderColor = borderColor?.cgColor
contentView.layer.borderWidth = borderWidth
contentView.backgroundColor = color
}
private func updateShadowLayer() {
if shadowLayer != nil {
shadowLayer.removeFromSuperlayer()
}
shadowLayer = makeShadowLayer()
layer.insertSublayer(shadowLayer, at: 0)
}
private func makeShadowLayer() -> CAShapeLayer {
private func updateLayers() {
log.debug("SurfaceView bounds", bounds)
let shadowLayer = CAShapeLayer()
let path = UIBezierPath(roundedRect: bounds,
var rect = bounds
rect.size.height += bottomOverflow // Expand the height for overflow buffer
let path = UIBezierPath(roundedRect: rect,
byRoundingCorners: [.topLeft, .topRight],
cornerRadii: CGSize(width: cornerRadius, height: cornerRadius))
shadowLayer.path = path.cgPath
shadowLayer.fillColor = color?.cgColor
backgroundLayer.path = path.cgPath
backgroundLayer.fillColor = color?.cgColor
if shadowHidden == false {
shadowLayer.shadowPath = shadowLayer.path
shadowLayer.shadowColor = shadowColor.cgColor
shadowLayer.shadowOffset = shadowOffset
shadowLayer.shadowOpacity = shadowOpacity
shadowLayer.shadowRadius = shadowRadius
layer.shadowColor = shadowColor.cgColor
layer.shadowOffset = shadowOffset
layer.shadowOpacity = shadowOpacity
layer.shadowRadius = shadowRadius
}
return shadowLayer
}
private func updateContentViewMask() {
if #available(iOS 11, *) {
// Don't use `contentView.clipToBounds` because it prevents content view from expanding the height of a subview of it
// for the bottom overflow like Auto Layout settings of UIVisualEffectView in Main.storyborad of Example/Maps.
// Because the bottom of contentView must be fit to the bottom of a screen to work the `safeLayoutGuide` of a content VC.
let maskLayer = CAShapeLayer()
var rect = bounds
rect.size.height += bottomOverflow
let path = UIBezierPath(roundedRect: rect,
byRoundingCorners: [.topLeft, .topRight],
cornerRadii: CGSize(width: cornerRadius, height: cornerRadius))
maskLayer.path = path.cgPath
contentView.layer.mask = maskLayer
} 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
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),
])
}
}
+7 -1
View File
@@ -1,6 +1,6 @@
//
// Created by Shin Yamamoto on 2018/09/19.
// Copyright © 2018 scenee. All rights reserved.
// Copyright © 2018 Shin Yamamoto. All rights reserved.
//
import UIKit
@@ -24,8 +24,14 @@ public class GrabberHandleView: UIView {
self.backgroundColor = Default.barColor
render()
}
private func render() {
self.layer.masksToBounds = true
self.layer.cornerRadius = frame.size.height * 0.5
}
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let view = super.hitTest(point, with: event)
return view == self ? nil : view
}
}
+1 -1
View File
@@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<string>1.1.0</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
</dict>
+1 -1
View File
@@ -1,6 +1,6 @@
//
// Created by Shin Yamamoto on 2018/10/09.
// Copyright © 2018 scenee. All rights reserved.
// Copyright © 2018 Shin Yamamoto. All rights reserved.
//
import Foundation
+13 -2
View File
@@ -1,6 +1,6 @@
//
// Created by Shin Yamamoto on 2018/09/18.
// Copyright © 2018 scenee. All rights reserved.
// Copyright © 2018 Shin Yamamoto. All rights reserved.
//
import UIKit
@@ -50,6 +50,8 @@ protocol SideLayoutGuideProvider {
extension UIView: SideLayoutGuideProvider {}
extension UILayoutGuide: SideLayoutGuideProvider {}
// The reason why UIView has no extensions of safe area insets and top/bottom guides
// is for iOS10 compat.
extension UIView {
var sideLayoutGuide: SideLayoutGuideProvider {
if #available(iOS 11.0, *) {
@@ -75,6 +77,15 @@ extension UIGestureRecognizer.State: CustomDebugStringConvertible {
extension UIScrollView {
var contentOffsetZero: CGPoint {
return CGPoint(x: 0.0, y: 0.0 + contentInset.top)
return CGPoint(x: 0.0, y: 0.0 - contentInset.top)
}
}
extension UISpringTimingParameters {
public convenience init(dampingRatio: CGFloat, frequencyResponse: CGFloat, initialVelocity: CGVector = .zero) {
let mass = 1 as CGFloat
let stiffness = pow(2 * .pi / frequencyResponse, 2) * mass
let damp = 4 * .pi * dampingRatio * mass / frequencyResponse
self.init(mass: mass, stiffness: stiffness, damping: damp, initialVelocity: initialVelocity)
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
//
// Created by Shin Yamamoto on 2018/09/18.
// Copyright © 2018 scenee. All rights reserved.
// Copyright © 2018 Shin Yamamoto. All rights reserved.
//
import XCTest
+212 -56
View File
@@ -1,11 +1,46 @@
[![Build Status](https://travis-ci.org/SCENEE/FloatingPanel.svg?branch=master)](https://travis-ci.org/SCENEE/FloatingPanel)
[![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.2](https://img.shields.io/badge/Swift-4.2-orange.svg?style=flat)](https://swift.org/)
# FloatingPanel
FloatingPanel is a simple and easy-to-use UI component for a new interface introduced in Apple Maps, Shortcuts and Stocks app.
The new interface displays the related contents and utilities in parallel as a user wants.
![Maps](https://github.com/SCENEE/FloatingPanel/blob/master/assets/maps.gif)
![Stocks](https://github.com/SCENEE/FloatingPanel/blob/master/assets/stocks.gif)
![Maps(Landscape)](https://github.com/SCENEE/FloatingPanel/blob/master/assets/maps-landscape.gif)
<!-- TOC -->
- [Features](#features)
- [Requirements](#requirements)
- [Installation](#installation)
- [CocoaPods](#cocoapods)
- [Carthage](#carthage)
- [Getting Started](#getting-started)
- [Usage](#usage)
- [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)
- [Customize the behavior with `FloatingPanelBehavior` protocol](#customize-the-behavior-with-floatingpanelbehavior-protocol)
- [Modify your floating panel's interaction](#modify-your-floating-panels-interaction)
- [Use a custom grabber handle](#use-a-custom-grabber-handle)
- [Add tap gestures to the surface or backdrop views](#add-tap-gestures-to-the-surface-or-backdrop-views)
- [Create an additional floating panel for a detail](#create-an-additional-floating-panel-for-a-detail)
- [Move a position with an animation](#move-a-position-with-an-animation)
- [Work your contents together with a floating panel behavior](#work-your-contents-together-with-a-floating-panel-behavior)
- [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)
- [Author](#author)
- [License](#license)
<!-- /TOC -->
## Features
@@ -13,7 +48,7 @@ The new interface displays the related contents and utilities in parallel as a u
- [x] Fluid animation and gesture handling
- [x] Scroll view tracking
- [x] Common UI elements: Grabber handle, Backdrop and Surface rounding corners
- [x] 2 or 3 anchor positions(full, half, tip)
- [x] 1~3 anchor positions(full, half, tip)
- [x] Layout customization for all trait environments(i.e. Landscape orientation support)
- [x] Behavior customization
- [x] Free from common issues of Auto Layout and gesture handling
@@ -58,25 +93,27 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
// Initialize FloatingPanelController and add the view
// Initialize a `FloatingPanelController` object.
fpc = FloatingPanelController()
fpc.delegate = self
// Add a content view controller
// Assign self as the delegate of the controller.
fpc.delegate = self // Optional
// Set a content view controller.
let contentVC = ContentViewController()
fpc.show(contentVC, sender: nil)
fpc.set(contentViewController: contentVC)
// Track a scroll view in the Content VC.
// Track a scroll view(or the siblings) in the content view controller.
fpc.track(scrollView: contentVC.tableView)
// Add FloatingPanel to self.view
fpc.add(toParent: self)
// Add and show the views managed by the `FloatingPanelController` object to self.view.
fpc.addPanel(toParent: self)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
// Remove FloatingPanel from self.view
fpc.removeFromParent()
// Remove the views managed by the `FloatingPanelController` object from self.view.
fpc.removePanelFromParent()
}
...
}
@@ -84,50 +121,41 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate {
## Usage
### Move a positon with an animation
### Customize the layout with `FloatingPanelLayout` protocol
Move a floating panel to the top and middle of a view while opening and closeing a search bar like Apple Maps.
```swift
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
...
fpc.move(to: .half, animated: true)
}
func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
...
fpc.move(to: .full, animated: true)
}
```
### Make your contents correspond with FloatingPanel behavior
```swift
class ViewController: UIViewController, FloatingPanelControllerDelegate {
...
func floatingPanelWillBeginDragging(_ vc: FloatingPanelController) {
if vc.position == .full {
searchVC.searchBar.showsCancelButton = false
searchVC.searchBar.resignFirstResponder()
}
}
func floatingPanelDidEndDragging(_ vc: FloatingPanelController, withVelocity velocity: CGPoint, targetPosition: FloatingPanelPosition) {
if targetPosition != .full {
searchVC.hideHeader()
}
}
...
}
```
### Support your landscape layout with a `FloatingPanelLayout` object
#### Change the initial position and height
```swift
class ViewController: UIViewController, FloatingPanelControllerDelegate {
...
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? {
return (newCollection.verticalSizeClass == .compact) ? FloatingPanelLandscapeLayout() : nil
return MyFloatingPanelLayout()
}
...
}
class MyFloatingPanelLayout: FloatingPanelLayout {
public var initialPosition: FloatingPanelPosition {
return .tip
}
public func insetFor(position: FloatingPanelPosition) -> CGFloat? {
switch position {
case .full: return 16.0 // A top inset from safe area
case .half: return 216.0 // A bottom inset from the safe area
case .tip: return 44.0 // A bottom inset from the safe area
}
}
}
```
#### Support your landscape layout
```swift
class ViewController: UIViewController, FloatingPanelControllerDelegate {
...
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? {
return (newCollection.verticalSizeClass == .compact) ? FloatingPanelLandscapeLayout() : nil // Returning nil indicates to use the default layout
}
...
}
@@ -136,7 +164,7 @@ class FloatingPanelLandscapeLayout: FloatingPanelLayout {
public var initialPosition: FloatingPanelPosition {
return .tip
}
public var supportedPositions: [FloatingPanelPosition] {
public var supportedPositions: Set<FloatingPanelPosition> {
return [.full, .tip]
}
@@ -157,7 +185,9 @@ class FloatingPanelLandscapeLayout: FloatingPanelLayout {
}
```
### Modify your floating panel's interaction with a `FloatingPanelBehavior` object
### Customize the behavior with `FloatingPanelBehavior` protocol
#### Modify your floating panel's interaction
```swift
class ViewController: UIViewController, FloatingPanelControllerDelegate {
@@ -174,7 +204,7 @@ class FloatingPanelStocksBehavior: FloatingPanelBehavior {
return 15.0
}
func interactionAnimator(to targetPosition: FloatingPanelPosition, with velocity: CGVector) -> UIViewPropertyAnimator {
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)
@@ -183,6 +213,45 @@ class FloatingPanelStocksBehavior: FloatingPanelBehavior {
}
```
### Use a custom grabber handle
```swift
class ViewController: UIViewController {
...
override func viewDidLoad() {
...
let myGrabberHandleView = MyGrabberHandleView()
fpc.surfaceView.grabberHandle.isHidden = true
fpc.surfaceView.addSubview(myGrabberHandleView)
}
...
}
```
### Add tap gestures to the surface or backdrop views
```swift
class ViewController: UIViewController, FloatingPanelControllerDelegate {
...
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
@@ -195,27 +264,114 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate {
self.searchPanelVC = FloatingPanelController()
let searchVC = SearchViewController()
self.searchPanelVC.show(searchVC, sender: nil)
self.searchPanelVC.set(contentViewController: searchVC)
self.searchPanelVC.track(scrollView: contentVC.tableView)
self.searchPanelVC.add(toParent: self)
self.searchPanelVC.addPanel(toParent: self)
// Setup Detail panel
self.detailPanelVC = FloatingPanelController()
let contentVC = ContentViewController()
self.detailPanelVC.show(contentVC, sender: nil)
self.detailPanelVC.set(contentViewController: contentVC)
self.detailPanelVC.track(scrollView: contentVC.scrollView)
self.detailPanelVC.add(toParent: self)
self.detailPanelVC.addPanel(toParent: self)
}
...
}
```
### Move a position with an animation
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 searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
...
fpc.move(to: .full, animated: true)
}
```
### Work your contents together with a floating panel behavior
```swift
class ViewController: UIViewController, FloatingPanelControllerDelegate {
...
func floatingPanelWillBeginDragging(_ vc: FloatingPanelController) {
if vc.position == .full {
searchVC.searchBar.showsCancelButton = false
searchVC.searchBar.resignFirstResponder()
}
}
func floatingPanelDidEndDragging(_ vc: FloatingPanelController, withVelocity velocity: CGPoint, targetPosition: FloatingPanelPosition) {
if targetPosition != .full {
searchVC.hideHeader()
}
}
...
}
```
## Notes
### '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.
`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.
By the way, a content view controller can present a view controller modally with `present(_:animated:completion:)` or 'Present Modally' segue.
However, sometimes you want to show a destination view controller of 'Show' or 'Show Detail' segue with another floating panel. It's possible to override `show(_:sender)` of the master VC!
Here is an example.
```swift
class ViewController: UIViewController {
var fpc: FloatingPanelController!
var secondFpc: FloatingPanelController!
...
override func show(_ vc: UIViewController, sender: Any?) {
secondFpc = FloatingPanelController()
secondFpc.set(contentViewController: vc)
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.
It's a great way to decouple between a floating panel and the content VC.
### 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.
So you need to draw top rounding corners of your content. Here is an example in Examples/Maps.
```swift
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if #available(iOS 10, *) {
visualEffectView.layer.cornerRadius = 9.0
visualEffectView.clipsToBounds = true
}
}
```
* 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.
## Author
Shin Yamamoto, shin@scenee.com
Shin Yamamoto <shin@scenee.com>
## License
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB