Compare commits
52 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b6e8928b1a | |||
| 3a3d53424c | |||
| 71f419a3cd | |||
| 0e27410460 | |||
| 29185a47bd | |||
| 6821b26706 | |||
| 5bdbe0f0ea | |||
| afe2a9bced | |||
| c0d88af234 | |||
| 3b4c1bd51c | |||
| 3c9f556533 | |||
| 7f1a74825d | |||
| 22d46c5260 | |||
| 466aaf21dd | |||
| 56e71ac580 | |||
| f45b6aaa3f | |||
| e473c3c440 | |||
| 8f2be39bf4 | |||
| 92c10830ff | |||
| dcb89f58c3 | |||
| d39c4b54d1 | |||
| 504182ceae | |||
| bc1cfe444b | |||
| 0e0f773df7 | |||
| be2be99537 | |||
| afcf1ced36 | |||
| 5b33d3d5ff | |||
| dbef6a691a | |||
| dd238884bf | |||
| 046ed3df5b | |||
| 4996ce1a84 | |||
| b8f7ff825d | |||
| 6fb9a9b3a2 | |||
| 34ebb3bf19 | |||
| dbf665526d | |||
| 6c7f529eff | |||
| c414d3a2a6 | |||
| 8eba647d75 | |||
| c3568067b7 | |||
| 72580f089d | |||
| c508ec892d | |||
| ff2d4a48f1 | |||
| 62364eb6d5 | |||
| ce5469a69d | |||
| 93c31fd71d | |||
| 461f637818 | |||
| 6b3b18b8ed | |||
| b5ca468397 | |||
| 80956bfac6 | |||
| 5d382c440f | |||
| d28c939a4c | |||
| 8bd02145cf |
@@ -7,15 +7,8 @@ jobs:
|
||||
steps:
|
||||
- checkout
|
||||
- run: xcodebuild clean test -scheme FloatingPanel -workspace FloatingPanel.xcworkspace -destination 'platform=iOS Simulator,OS=14.5,name=iPhone 12 Pro'
|
||||
test-ios13_7-iPhone_11_Pro:
|
||||
macos:
|
||||
xcode: 12.5.1
|
||||
steps:
|
||||
- checkout
|
||||
- run: xcodebuild clean test -scheme FloatingPanel -workspace FloatingPanel.xcworkspace -destination 'platform=iOS Simulator,OS=13.7,name=iPhone 11 Pro'
|
||||
|
||||
workflows:
|
||||
test:
|
||||
jobs:
|
||||
- test-ios14_5-iPhone_12_Pro
|
||||
- test-ios13_7-iPhone_11_Pro
|
||||
|
||||
+86
-41
@@ -11,65 +11,74 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ${{ matrix.runsOn }}
|
||||
runs-on: ${{ matrix.runs-on }}
|
||||
env:
|
||||
DEVELOPER_DIR: /Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- swift: "5.10"
|
||||
xcode: "15.4"
|
||||
runs-on: macos-14
|
||||
- swift: "5.9"
|
||||
xcode: "15.2"
|
||||
runs-on: macos-13
|
||||
- swift: "5.8"
|
||||
xcode: "14.3"
|
||||
runsOn: macos-13
|
||||
xcode: "14.3.1"
|
||||
runs-on: macos-13
|
||||
- swift: "5.7"
|
||||
xcode: "14.1"
|
||||
runsOn: macos-12
|
||||
runs-on: macos-12
|
||||
- swift: "5.6"
|
||||
xcode: "13.4.1"
|
||||
runsOn: macos-12
|
||||
runs-on: macos-12
|
||||
- swift: "5.5"
|
||||
xcode: "13.2.1"
|
||||
runsOn: macos-11
|
||||
- swift: "5.4"
|
||||
xcode: "12.5.1"
|
||||
runsOn: macos-11
|
||||
- swift: "5.3"
|
||||
xcode: "12.4"
|
||||
runsOn: macos-11
|
||||
- swift: "5.2"
|
||||
xcode: "11.7"
|
||||
runsOn: macos-11
|
||||
runs-on: macos-12
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Building in Swift ${{ matrix.swift }}
|
||||
run: xcodebuild -scheme FloatingPanel SWIFT_VERSION=${{ matrix.swift }} clean build
|
||||
|
||||
test:
|
||||
runs-on: ${{ matrix.runsOn }}
|
||||
runs-on: ${{ matrix.runs-on }}
|
||||
env:
|
||||
DEVELOPER_DIR: /Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: "17.5"
|
||||
xcode: "15.4"
|
||||
sim: "iPhone 15 Pro"
|
||||
parallel: NO # Stop random test job failures
|
||||
runs-on: macos-14
|
||||
- os: "16.4"
|
||||
xcode: "14.3.1"
|
||||
sim: "iPhone 14 Pro"
|
||||
runsOn: macos-13
|
||||
parallel: NO # Stop random test job failures
|
||||
runs-on: macos-13
|
||||
- os: "15.5"
|
||||
xcode: "13.4.1"
|
||||
sim: "iPhone 13 Pro"
|
||||
runsOn: macos-12
|
||||
parallel: NO # Stop random test job failures
|
||||
runs-on: macos-12
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Testing in iOS ${{ matrix.os }}
|
||||
run: xcodebuild clean test -scheme FloatingPanel -workspace FloatingPanel.xcworkspace -destination 'platform=iOS Simulator,OS=${{ matrix.os }},name=${{ matrix.sim }}'
|
||||
run: |
|
||||
xcodebuild clean test \
|
||||
-workspace FloatingPanel.xcworkspace \
|
||||
-scheme FloatingPanel \
|
||||
-destination 'platform=iOS Simulator,OS=${{ matrix.os }},name=${{ matrix.sim }}' \
|
||||
-parallel-testing-enabled '${{ matrix.parallel }}'
|
||||
timeout-minutes: 20
|
||||
|
||||
example:
|
||||
runs-on: macos-12
|
||||
runs-on: macos-14
|
||||
env:
|
||||
DEVELOPER_DIR: /Applications/Xcode_14.1.app/Contents/Developer
|
||||
DEVELOPER_DIR: /Applications/Xcode_15.4.app/Contents/Developer
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -79,40 +88,76 @@ jobs:
|
||||
- example: "Stocks"
|
||||
- example: "Samples"
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Building ${{ matrix.example }}
|
||||
run: xcodebuild -workspace FloatingPanel.xcworkspace -scheme ${{ matrix.example }} -sdk iphonesimulator clean build
|
||||
run: |
|
||||
xcodebuild clean build \
|
||||
-workspace FloatingPanel.xcworkspace \
|
||||
-scheme ${{ matrix.example }} \
|
||||
-sdk iphonesimulator
|
||||
|
||||
swiftpm:
|
||||
runs-on: macos-12
|
||||
runs-on: macos-14
|
||||
env:
|
||||
DEVELOPER_DIR: /Applications/Xcode_14.1.app/Contents/Developer
|
||||
DEVELOPER_DIR: /Applications/Xcode_15.4.app/Contents/Developer
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform: [iphoneos, iphonesimulator]
|
||||
arch: [x86_64, arm64]
|
||||
exclude:
|
||||
- platform: iphoneos
|
||||
arch: x86_64
|
||||
include:
|
||||
# 17.2
|
||||
- platform: iphoneos
|
||||
sys: "ios17.2"
|
||||
- platform: iphonesimulator
|
||||
sys: "ios17.2-simulator"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: "Swift Package Manager build"
|
||||
run: |
|
||||
xcrun swift build \
|
||||
--sdk "$(xcrun --sdk ${{ matrix.platform }} --show-sdk-path)" \
|
||||
-Xswiftc "-target" -Xswiftc "${{ matrix.arch }}-apple-${{ matrix.sys }}"
|
||||
|
||||
swiftpm_old:
|
||||
runs-on: ${{ matrix.runs-on }}
|
||||
env:
|
||||
DEVELOPER_DIR: /Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
# 16.4
|
||||
- target: "x86_64-apple-ios16.4-simulator"
|
||||
xcode: "14.3.1"
|
||||
runs-on: macos-13
|
||||
- target: "arm64-apple-ios16.4-simulator"
|
||||
xcode: "14.3.1"
|
||||
runs-on: macos-13
|
||||
# 15.7
|
||||
- target: "x86_64-apple-ios15.7-simulator"
|
||||
xcode: "14.1"
|
||||
runs-on: macos-12
|
||||
- target: "arm64-apple-ios15.7-simulator"
|
||||
# 16.1
|
||||
- target: "x86_64-apple-ios16.1-simulator"
|
||||
- target: "arm64-apple-ios16.1-simulator"
|
||||
xcode: "14.1"
|
||||
runs-on: macos-12
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: "Swift Package Manager build"
|
||||
run: swift build -Xswiftc "-sdk" -Xswiftc "`xcrun --sdk iphonesimulator --show-sdk-path`" -Xswiftc "-target" -Xswiftc "${{ matrix.target }}"
|
||||
|
||||
carthage:
|
||||
runs-on: macos-11
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: "Carthage build"
|
||||
run: carthage build --use-xcframeworks --no-skip-current
|
||||
run: |
|
||||
swift build \
|
||||
-Xswiftc "-sdk" -Xswiftc "`xcrun --sdk iphonesimulator --show-sdk-path`" \
|
||||
-Xswiftc "-target" -Xswiftc "${{ matrix.target }}"
|
||||
|
||||
cocoapods:
|
||||
runs-on: macos-12
|
||||
runs-on: macos-14
|
||||
env:
|
||||
DEVELOPER_DIR: /Applications/Xcode_15.4.app/Contents/Developer
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: "CocoaPods: pod lib lint"
|
||||
run: pod lib lint --allow-warnings
|
||||
- name: "CocoaPods: pod spec lint"
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="19455" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="21701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
|
||||
<device id="retina6_12" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="19454"/>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21679"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
@@ -11,16 +13,16 @@
|
||||
<objects>
|
||||
<viewController id="BYZ-38-t0r" customClass="MainViewController" customModule="Maps" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
|
||||
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<mapView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" mapType="standard" translatesAutoresizingMaskIntoConstraints="NO" id="5Jw-n2-Cpw">
|
||||
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
|
||||
</mapView>
|
||||
<visualEffectView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="d9i-3g-8Ja">
|
||||
<rect key="frame" x="0.0" y="0.0" width="600" height="0.0"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="393" height="59"/>
|
||||
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="lMa-xa-AVV">
|
||||
<rect key="frame" x="0.0" y="0.0" width="600" height="0.0"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="393" height="59"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
</view>
|
||||
<blurEffect style="prominent"/>
|
||||
@@ -52,31 +54,31 @@
|
||||
<objects>
|
||||
<viewController storyboardIdentifier="SearchViewController" useStoryboardIdentifierAsRestorationIdentifier="YES" id="0S1-Lk-JgE" customClass="SearchViewController" customModule="Maps" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="Ncl-E9-yRn">
|
||||
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
|
||||
<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="600" height="600"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
|
||||
<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="600" height="600"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<searchBar contentMode="redraw" searchBarStyle="minimal" translatesAutoresizingMaskIntoConstraints="NO" id="Zcj-SE-gb8">
|
||||
<rect key="frame" x="0.0" y="6" width="600" height="51"/>
|
||||
<rect key="frame" x="0.0" y="6" width="393" height="56"/>
|
||||
<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="61" width="600" height="539"/>
|
||||
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" showsHorizontalScrollIndicator="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="393" height="786"/>
|
||||
<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="600" height="116"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="393" height="116"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" distribution="equalSpacing" baselineRelativeArrangement="YES" translatesAutoresizingMaskIntoConstraints="NO" id="era-8w-yA1">
|
||||
<rect key="frame" x="24" y="10.5" width="552" height="97.5"/>
|
||||
<rect key="frame" x="24" y="10.666666666666664" width="345" height="97.333333333333343"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="6" translatesAutoresizingMaskIntoConstraints="NO" id="auI-1v-Yfk">
|
||||
<rect key="frame" x="0.0" y="0.0" width="60" height="97.5"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="60" height="97.333333333333329"/>
|
||||
<subviews>
|
||||
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="food" translatesAutoresizingMaskIntoConstraints="NO" id="ErN-bC-qTx">
|
||||
<rect key="frame" x="0.0" y="0.0" width="60" height="60"/>
|
||||
@@ -86,7 +88,7 @@
|
||||
</constraints>
|
||||
</imageView>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Food & Drinks" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="nx2-fW-xAm">
|
||||
<rect key="frame" x="0.0" y="66" width="60" height="31.5"/>
|
||||
<rect key="frame" x="0.0" y="66" width="60" height="31.333333333333329"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="13"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
@@ -94,7 +96,7 @@
|
||||
</subviews>
|
||||
</stackView>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="6" translatesAutoresizingMaskIntoConstraints="NO" id="0vd-sD-XKv">
|
||||
<rect key="frame" x="164" y="0.0" width="60" height="97.5"/>
|
||||
<rect key="frame" x="95" y="0.0" width="60" height="97.333333333333329"/>
|
||||
<subviews>
|
||||
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="shopping" translatesAutoresizingMaskIntoConstraints="NO" id="xcm-St-HAo">
|
||||
<rect key="frame" x="0.0" y="0.0" width="60" height="60"/>
|
||||
@@ -104,7 +106,7 @@
|
||||
</constraints>
|
||||
</imageView>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="1000" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="H7q-q2-ga5">
|
||||
<rect key="frame" x="0.0" y="66" width="60" height="31.5"/>
|
||||
<rect key="frame" x="0.0" y="66" width="60" height="31.333333333333329"/>
|
||||
<string key="text">Shopping
|
||||
</string>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="13"/>
|
||||
@@ -114,7 +116,7 @@
|
||||
</subviews>
|
||||
</stackView>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="6" translatesAutoresizingMaskIntoConstraints="NO" id="Jd8-YL-b5s">
|
||||
<rect key="frame" x="328" y="0.0" width="60" height="97.5"/>
|
||||
<rect key="frame" x="190" y="0.0" width="60" height="97.333333333333329"/>
|
||||
<subviews>
|
||||
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="fun" translatesAutoresizingMaskIntoConstraints="NO" id="bMJ-Jn-Gi8">
|
||||
<rect key="frame" x="0.0" y="0.0" width="60" height="60"/>
|
||||
@@ -124,7 +126,7 @@
|
||||
</constraints>
|
||||
</imageView>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="kKh-45-FZ2">
|
||||
<rect key="frame" x="0.0" y="66" width="60" height="31.5"/>
|
||||
<rect key="frame" x="0.0" y="66" width="60" height="31.333333333333329"/>
|
||||
<string key="text">Fun
|
||||
</string>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="13"/>
|
||||
@@ -134,7 +136,7 @@
|
||||
</subviews>
|
||||
</stackView>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="6" translatesAutoresizingMaskIntoConstraints="NO" id="dTL-e1-Arz">
|
||||
<rect key="frame" x="492" y="0.0" width="60" height="97.5"/>
|
||||
<rect key="frame" x="285" y="0.0" width="60" height="97.333333333333329"/>
|
||||
<subviews>
|
||||
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="travel" translatesAutoresizingMaskIntoConstraints="NO" id="8h3-fo-pC3">
|
||||
<rect key="frame" x="0.0" y="0.0" width="60" height="60"/>
|
||||
@@ -144,7 +146,7 @@
|
||||
</constraints>
|
||||
</imageView>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="WBT-Vj-7QA">
|
||||
<rect key="frame" x="0.0" y="66" width="60" height="31.5"/>
|
||||
<rect key="frame" x="0.0" y="66" width="60" height="31.333333333333329"/>
|
||||
<string key="text">Travel
|
||||
</string>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="13"/>
|
||||
@@ -166,10 +168,10 @@
|
||||
</view>
|
||||
<prototypes>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" selectionStyle="blue" hidesAccessoryWhenEditing="NO" indentationLevel="1" indentationWidth="0.0" reuseIdentifier="Cell" rowHeight="70" id="LzC-B9-Adb" customClass="SearchCell" customModule="Maps" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="160.5" width="600" height="70"/>
|
||||
<rect key="frame" x="0.0" y="166" width="393" height="70"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="LzC-B9-Adb" id="evr-60-laS">
|
||||
<rect key="frame" x="0.0" y="0.0" width="600" height="70"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="393" height="70"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="like" translatesAutoresizingMaskIntoConstraints="NO" id="GEk-yE-lLq">
|
||||
@@ -181,16 +183,16 @@
|
||||
</constraints>
|
||||
</imageView>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" distribution="fillEqually" spacing="2" translatesAutoresizingMaskIntoConstraints="NO" id="Gfl-Oy-rsy">
|
||||
<rect key="frame" x="57" y="12" width="528" height="46"/>
|
||||
<rect key="frame" x="57" y="12" width="321" height="46"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" tag="1" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Favorites" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Spf-8L-Ne6">
|
||||
<rect key="frame" x="0.0" y="0.0" width="528" height="22"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="321" height="22"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="20"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="0 Places" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="gyo-3V-7U8">
|
||||
<rect key="frame" x="0.0" y="24" width="528" height="22"/>
|
||||
<rect key="frame" x="0.0" y="24" width="321" height="22"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="13"/>
|
||||
<color key="textColor" red="0.57647058819999997" green="0.57647058819999997" blue="0.57647058819999997" alpha="1" colorSpace="calibratedRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
@@ -255,18 +257,18 @@
|
||||
<objects>
|
||||
<viewController storyboardIdentifier="DetailViewController" id="Tp2-MF-IFz" customClass="DetailViewController" customModule="Maps" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="FmO-AT-4Y7">
|
||||
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<visualEffectView opaque="NO" contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="c3d-2e-0b1">
|
||||
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
|
||||
<rect key="frame" x="-1" y="-1" width="393" height="852"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="9fL-a5-0LS">
|
||||
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" showsHorizontalScrollIndicator="NO" showsVerticalScrollIndicator="NO" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="kP7-56-wlG">
|
||||
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</tableView>
|
||||
</subviews>
|
||||
|
||||
@@ -156,6 +156,14 @@ class SearchPanelPhoneDelegate: NSObject, FloatingPanelControllerDelegate, UIGes
|
||||
self.owner = owner
|
||||
}
|
||||
|
||||
func floatingPanel(
|
||||
_ fpc: FloatingPanelController,
|
||||
shouldAllowToScroll scrollView: UIScrollView,
|
||||
in state: FloatingPanelState
|
||||
) -> Bool {
|
||||
return state == .full || state == .half
|
||||
}
|
||||
|
||||
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout {
|
||||
switch newCollection.verticalSizeClass {
|
||||
case .compact:
|
||||
@@ -215,17 +223,10 @@ class SearchPanelLandscapeLayout: FloatingPanelLayout {
|
||||
.tip: FloatingPanelLayoutAnchor(absoluteInset: 69.0, edge: .bottom, referenceGuide: .safeArea),
|
||||
]
|
||||
func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint] {
|
||||
if #available(iOS 11.0, *) {
|
||||
return [
|
||||
surfaceView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 8.0),
|
||||
surfaceView.widthAnchor.constraint(equalToConstant: 291),
|
||||
]
|
||||
} else {
|
||||
return [
|
||||
surfaceView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 8.0),
|
||||
surfaceView.widthAnchor.constraint(equalToConstant: 291),
|
||||
]
|
||||
}
|
||||
return [
|
||||
surfaceView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 8.0),
|
||||
surfaceView.widthAnchor.constraint(equalToConstant: 291),
|
||||
]
|
||||
}
|
||||
func backdropAlpha(for state: FloatingPanelState) -> CGFloat {
|
||||
return 0.0
|
||||
|
||||
@@ -81,9 +81,6 @@ class SearchViewController: UIViewController, UITableViewDataSource {
|
||||
|
||||
var items: [LocationItem] = []
|
||||
|
||||
// For iOS 10 only
|
||||
private lazy var shadowLayer: CAShapeLayer = CAShapeLayer()
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
tableView.dataSource = self
|
||||
@@ -92,29 +89,6 @@ class SearchViewController: UIViewController, UITableViewDataSource {
|
||||
hideHeader(animated: false)
|
||||
}
|
||||
|
||||
override func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
if #available(iOS 11, *) {
|
||||
} else {
|
||||
// Exmaple: Add rounding corners on iOS 10
|
||||
visualEffectView.layer.cornerRadius = 9.0
|
||||
visualEffectView.clipsToBounds = true
|
||||
|
||||
// Exmaple: Add shadow manually on iOS 10
|
||||
view.layer.insertSublayer(shadowLayer, at: 0)
|
||||
let rect = visualEffectView.frame
|
||||
let path = UIBezierPath(roundedRect: rect,
|
||||
byRoundingCorners: [.topLeft, .topRight],
|
||||
cornerRadii: CGSize(width: 9.0, height: 9.0))
|
||||
shadowLayer.frame = visualEffectView.frame
|
||||
shadowLayer.shadowPath = path.cgPath
|
||||
shadowLayer.shadowColor = UIColor.black.cgColor
|
||||
shadowLayer.shadowOffset = CGSize(width: 0.0, height: 1.0)
|
||||
shadowLayer.shadowOpacity = 0.2
|
||||
shadowLayer.shadowRadius = 3.0
|
||||
}
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return items.count
|
||||
}
|
||||
@@ -130,16 +104,16 @@ class SearchViewController: UIViewController, UITableViewDataSource {
|
||||
}
|
||||
|
||||
func showHeader(animated: Bool) {
|
||||
changeHeader(height: 116.0, aniamted: animated)
|
||||
changeHeader(height: 116.0, animated: animated)
|
||||
}
|
||||
|
||||
func hideHeader(animated: Bool) {
|
||||
changeHeader(height: 0.0, aniamted: animated)
|
||||
changeHeader(height: 0.0, animated: animated)
|
||||
}
|
||||
|
||||
private func changeHeader(height: CGFloat, aniamted: Bool) {
|
||||
private func changeHeader(height: CGFloat, animated: Bool) {
|
||||
guard let headerView = tableView.tableHeaderView, headerView.bounds.height != height else { return }
|
||||
if aniamted == false {
|
||||
if animated == false {
|
||||
updateHeader(height: height)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
5442E24A25FC53C100A26F43 /* DebugTextViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5442E24925FC53C100A26F43 /* DebugTextViewController.swift */; };
|
||||
5442E25225FC541700A26F43 /* NestedScrollViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5442E25125FC541700A26F43 /* NestedScrollViewController.swift */; };
|
||||
54496C59263A7E5A0031E0C8 /* UseCaseController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54496C58263A7E5A0031E0C8 /* UseCaseController.swift */; };
|
||||
544BC56826CC918200D0A436 /* AdaptiveLayoutTestViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 544BC56726CC918200D0A436 /* AdaptiveLayoutTestViewController.swift */; };
|
||||
544BC56826CC918200D0A436 /* TableViewControllerForAdaptiveLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 544BC56726CC918200D0A436 /* TableViewControllerForAdaptiveLayout.swift */; };
|
||||
545DB9EE21511E6300CA77B8 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DB9ED21511E6300CA77B8 /* AppDelegate.swift */; };
|
||||
545DB9F021511E6300CA77B8 /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DB9EF21511E6300CA77B8 /* MainViewController.swift */; };
|
||||
545DB9F321511E6300CA77B8 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 545DB9F121511E6300CA77B8 /* Main.storyboard */; };
|
||||
@@ -30,8 +30,11 @@
|
||||
549D23CC233C7779008EF4D7 /* FloatingPanel.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 549D23CA233C7779008EF4D7 /* FloatingPanel.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
54B51116216AFE5F0033A6F3 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B51115216AFE5F0033A6F3 /* Extensions.swift */; };
|
||||
54CDC5D8215BBE23007D205C /* SupplementaryViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CDC5D7215BBE23007D205C /* SupplementaryViews.swift */; };
|
||||
54E58CB72BF8A8D900408EA9 /* CollectionViewControllerForAdaptiveLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54E58CB62BF8A8D900408EA9 /* CollectionViewControllerForAdaptiveLayout.swift */; };
|
||||
54EAD35B263A75EB006A36EA /* PanelLayouts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54EAD35A263A75EB006A36EA /* PanelLayouts.swift */; };
|
||||
54EAD365263A765F006A36EA /* PagePanelController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54EAD364263A765F006A36EA /* PagePanelController.swift */; };
|
||||
54F185822BF4AD0000916F57 /* DebugListCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54F185812BF4AD0000916F57 /* DebugListCollectionViewController.swift */; };
|
||||
54F185842BF4B82E00916F57 /* UnavailableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54F185832BF4B82E00916F57 /* UnavailableViewController.swift */; };
|
||||
5D82A6AD28D1843C006A44BA /* libswiftCoreGraphics.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 5D82A6AC28D18438006A44BA /* libswiftCoreGraphics.tbd */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
@@ -61,7 +64,7 @@
|
||||
5442E24925FC53C100A26F43 /* DebugTextViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugTextViewController.swift; sourceTree = "<group>"; };
|
||||
5442E25125FC541700A26F43 /* NestedScrollViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NestedScrollViewController.swift; sourceTree = "<group>"; };
|
||||
54496C58263A7E5A0031E0C8 /* UseCaseController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UseCaseController.swift; sourceTree = "<group>"; };
|
||||
544BC56726CC918200D0A436 /* AdaptiveLayoutTestViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdaptiveLayoutTestViewController.swift; sourceTree = "<group>"; };
|
||||
544BC56726CC918200D0A436 /* TableViewControllerForAdaptiveLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewControllerForAdaptiveLayout.swift; sourceTree = "<group>"; };
|
||||
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 /* MainViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = "<group>"; };
|
||||
@@ -74,8 +77,11 @@
|
||||
549D23CA233C7779008EF4D7 /* FloatingPanel.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = FloatingPanel.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
54B51115216AFE5F0033A6F3 /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = "<group>"; };
|
||||
54CDC5D7215BBE23007D205C /* SupplementaryViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupplementaryViews.swift; sourceTree = "<group>"; };
|
||||
54E58CB62BF8A8D900408EA9 /* CollectionViewControllerForAdaptiveLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionViewControllerForAdaptiveLayout.swift; sourceTree = "<group>"; };
|
||||
54EAD35A263A75EB006A36EA /* PanelLayouts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PanelLayouts.swift; sourceTree = "<group>"; };
|
||||
54EAD364263A765F006A36EA /* PagePanelController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PagePanelController.swift; sourceTree = "<group>"; };
|
||||
54F185812BF4AD0000916F57 /* DebugListCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugListCollectionViewController.swift; sourceTree = "<group>"; };
|
||||
54F185832BF4B82E00916F57 /* UnavailableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnavailableViewController.swift; sourceTree = "<group>"; };
|
||||
5D82A6AC28D18438006A44BA /* libswiftCoreGraphics.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libswiftCoreGraphics.tbd; path = usr/lib/swift/libswiftCoreGraphics.tbd; sourceTree = SDKROOT; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
@@ -95,17 +101,18 @@
|
||||
5442E22225FC519700A26F43 /* ContentViewControllers */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
54E58CB52BF8A88600408EA9 /* AdaptiveLayout */,
|
||||
5442E23F25FC533800A26F43 /* DebugTableViewController.swift */,
|
||||
5442E24925FC53C100A26F43 /* DebugTextViewController.swift */,
|
||||
54F185812BF4AD0000916F57 /* DebugListCollectionViewController.swift */,
|
||||
5442E23325FC528400A26F43 /* DetailViewController.swift */,
|
||||
5442E24325FC538200A26F43 /* InspectorViewController.swift */,
|
||||
5442E22325FC51AF00A26F43 /* ImageViewController.swift */,
|
||||
5442E25125FC541700A26F43 /* NestedScrollViewController.swift */,
|
||||
5442E23925FC52CD00A26F43 /* ModalViewController.swift */,
|
||||
5442E22725FC51E200A26F43 /* MultiPanelController.swift */,
|
||||
5442E22B25FC521F00A26F43 /* SettingsViewController.swift */,
|
||||
5442E22F25FC525200A26F43 /* TabBarViewController.swift */,
|
||||
544BC56726CC918200D0A436 /* AdaptiveLayoutTestViewController.swift */,
|
||||
54F185832BF4B82E00916F57 /* UnavailableViewController.swift */,
|
||||
);
|
||||
path = ContentViewControllers;
|
||||
sourceTree = "<group>";
|
||||
@@ -157,6 +164,16 @@
|
||||
path = UseCases;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
54E58CB52BF8A88600408EA9 /* AdaptiveLayout */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5442E22325FC51AF00A26F43 /* ImageViewController.swift */,
|
||||
544BC56726CC918200D0A436 /* TableViewControllerForAdaptiveLayout.swift */,
|
||||
54E58CB62BF8A8D900408EA9 /* CollectionViewControllerForAdaptiveLayout.swift */,
|
||||
);
|
||||
path = AdaptiveLayout;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
5D82A6AB28D18438006A44BA /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -238,15 +255,17 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
5442E23425FC528400A26F43 /* DetailViewController.swift in Sources */,
|
||||
54E58CB72BF8A8D900408EA9 /* CollectionViewControllerForAdaptiveLayout.swift in Sources */,
|
||||
54496C59263A7E5A0031E0C8 /* UseCaseController.swift in Sources */,
|
||||
54CDC5D8215BBE23007D205C /* SupplementaryViews.swift in Sources */,
|
||||
54B51116216AFE5F0033A6F3 /* Extensions.swift in Sources */,
|
||||
544BC56826CC918200D0A436 /* AdaptiveLayoutTestViewController.swift in Sources */,
|
||||
544BC56826CC918200D0A436 /* TableViewControllerForAdaptiveLayout.swift in Sources */,
|
||||
5442E24A25FC53C100A26F43 /* DebugTextViewController.swift in Sources */,
|
||||
546341AC25C6426500CA0596 /* CustomState.swift in Sources */,
|
||||
5442E23A25FC52CD00A26F43 /* ModalViewController.swift in Sources */,
|
||||
5442E22425FC51AF00A26F43 /* ImageViewController.swift in Sources */,
|
||||
5442E24025FC533800A26F43 /* DebugTableViewController.swift in Sources */,
|
||||
54F185822BF4AD0000916F57 /* DebugListCollectionViewController.swift in Sources */,
|
||||
5442E25225FC541700A26F43 /* NestedScrollViewController.swift in Sources */,
|
||||
5442E22825FC51E200A26F43 /* MultiPanelController.swift in Sources */,
|
||||
546341A125C6415100CA0596 /* UseCase.swift in Sources */,
|
||||
@@ -257,6 +276,7 @@
|
||||
54EAD365263A765F006A36EA /* PagePanelController.swift in Sources */,
|
||||
5442E24425FC538200A26F43 /* InspectorViewController.swift in Sources */,
|
||||
5442E22C25FC521F00A26F43 /* SettingsViewController.swift in Sources */,
|
||||
54F185842BF4B82E00916F57 /* UnavailableViewController.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="21701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="RoN-h0-uBD">
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="32700.99.1234" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="RoN-h0-uBD">
|
||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21679"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22685"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="Stack View standard spacing" minToolsVersion="9.0"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||
@@ -801,10 +801,10 @@ Section 1.10.33 of "de Finibus Bonorum et Malorum", written by Cicero in 45 BC
|
||||
</objects>
|
||||
<point key="canvasLocation" x="-2.1739130434782612" y="733.92857142857144"/>
|
||||
</scene>
|
||||
<!--Adaptive Layout Test View Controller-->
|
||||
<!--Table View Controller For Adaptive Layout-->
|
||||
<scene sceneID="rDI-lU-wEx">
|
||||
<objects>
|
||||
<viewController storyboardIdentifier="AdaptiveLayoutTestViewController" id="5nC-6E-bXf" customClass="AdaptiveLayoutTestViewController" customModule="Samples" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<viewController storyboardIdentifier="TableViewControllerForAdaptiveLayout" id="5nC-6E-bXf" customClass="TableViewControllerForAdaptiveLayout" customModule="Samples" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="jXL-Ss-NCJ">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="778"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
@@ -861,13 +861,13 @@ Section 1.10.33 of "de Finibus Bonorum et Malorum", written by Cicero in 45 BC
|
||||
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</systemColor>
|
||||
<systemColor name="systemOrangeColor">
|
||||
<color red="1" green="0.58431372549019611" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<color red="1" green="0.58431372550000005" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</systemColor>
|
||||
<systemColor name="systemPurpleColor">
|
||||
<color red="0.68627450980392157" green="0.32156862745098042" blue="0.87058823529411766" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<color red="0.68627450980000004" green="0.32156862749999998" blue="0.87058823529999996" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</systemColor>
|
||||
<systemColor name="systemTealColor">
|
||||
<color red="0.18823529411764706" green="0.69019607843137254" blue="0.7803921568627451" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<color red="0.18823529410000001" green="0.69019607839999997" blue="0.78039215689999997" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</systemColor>
|
||||
</resources>
|
||||
</document>
|
||||
|
||||
+175
@@ -0,0 +1,175 @@
|
||||
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
|
||||
|
||||
import UIKit
|
||||
import FloatingPanel
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
class CollectionViewControllerForAdaptiveLayout: UIViewController {
|
||||
class PanelLayout: FloatingPanelLayout {
|
||||
let position: FloatingPanelPosition = .bottom
|
||||
let initialState: FloatingPanelState = .full
|
||||
|
||||
private unowned var targetGuide: UILayoutGuide
|
||||
|
||||
init(targetGuide: UILayoutGuide) {
|
||||
self.targetGuide = targetGuide
|
||||
}
|
||||
|
||||
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
|
||||
return [
|
||||
.full: FloatingPanelAdaptiveLayoutAnchor(
|
||||
absoluteOffset: 0.0,
|
||||
contentLayout: targetGuide,
|
||||
referenceGuide: .superview,
|
||||
contentBoundingGuide: .safeArea
|
||||
),
|
||||
.half: FloatingPanelAdaptiveLayoutAnchor(
|
||||
fractionalOffset: 0.5,
|
||||
contentLayout: targetGuide,
|
||||
referenceGuide: .superview,
|
||||
contentBoundingGuide: .safeArea
|
||||
),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
enum LayoutType {
|
||||
case flow
|
||||
case compositional
|
||||
}
|
||||
|
||||
weak var collectionView: UICollectionView!
|
||||
var layoutType: LayoutType = .flow
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
setupCollectionView()
|
||||
}
|
||||
|
||||
private func setupCollectionView() {
|
||||
let collectionViewLayout = {
|
||||
switch layoutType {
|
||||
case .flow:
|
||||
CollectionViewLayoutFactory.flowLayout
|
||||
case .compositional:
|
||||
CollectionViewLayoutFactory.compositionalLayout
|
||||
}
|
||||
}()
|
||||
let collectionView = IntrinsicCollectionView(
|
||||
frame: .zero,
|
||||
collectionViewLayout: collectionViewLayout
|
||||
)
|
||||
|
||||
collectionView.delegate = self
|
||||
collectionView.dataSource = self
|
||||
collectionView.backgroundColor = .yellow
|
||||
collectionView.register(Cell.self, forCellWithReuseIdentifier: Cell.reuseIdentifier)
|
||||
|
||||
view.addSubview(collectionView)
|
||||
self.collectionView = collectionView
|
||||
|
||||
collectionView.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
collectionView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||
collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
extension CollectionViewControllerForAdaptiveLayout: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
|
||||
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||
return 5 // Only three cells needed to fill the space
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: Cell.reuseIdentifier, for: indexPath) as! Cell
|
||||
cell.configure(text: "Item \(indexPath.row)")
|
||||
return cell
|
||||
}
|
||||
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
|
||||
let width = collectionView.frame.width
|
||||
return CGSize(width: width, height: 100)
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
extension CollectionViewControllerForAdaptiveLayout {
|
||||
enum CollectionViewLayoutFactory {
|
||||
static var flowLayout: UICollectionViewLayout {
|
||||
let layout = UICollectionViewFlowLayout()
|
||||
layout.scrollDirection = .vertical
|
||||
layout.minimumLineSpacing = 8 // Vertical spacing between rows
|
||||
return layout
|
||||
}
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
static var compositionalLayout: UICollectionViewLayout {
|
||||
UICollectionViewCompositionalLayout { (sectionIndex: Int, layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in
|
||||
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(100))
|
||||
let item = NSCollectionLayoutItem(layoutSize: itemSize)
|
||||
|
||||
let group = NSCollectionLayoutGroup.horizontal(layoutSize: itemSize, subitems: [item])
|
||||
|
||||
let section = NSCollectionLayoutSection(group: group)
|
||||
section.interGroupSpacing = 8 // Spacing between each group/item
|
||||
section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)
|
||||
|
||||
return section
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private final class Cell: UICollectionViewCell {
|
||||
static let reuseIdentifier = "Cell"
|
||||
|
||||
private let label: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textAlignment = .center
|
||||
label.textColor = .white
|
||||
return label
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
commonInit()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
commonInit()
|
||||
}
|
||||
|
||||
private func commonInit() {
|
||||
backgroundColor = .systemBlue
|
||||
addSubview(label)
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
label.centerXAnchor.constraint(equalTo: centerXAnchor),
|
||||
label.centerYAnchor.constraint(equalTo: centerYAnchor)
|
||||
])
|
||||
}
|
||||
|
||||
func configure(text: String) {
|
||||
label.text = text
|
||||
}
|
||||
}
|
||||
|
||||
private final class IntrinsicCollectionView: UICollectionView {
|
||||
override public var contentSize: CGSize {
|
||||
didSet {
|
||||
invalidateIntrinsicContentSize()
|
||||
}
|
||||
}
|
||||
|
||||
override public var intrinsicContentSize: CGSize {
|
||||
layoutIfNeeded()
|
||||
return CGSize(width: UIView.noIntrinsicMetric, height: contentSize.height)
|
||||
}
|
||||
}
|
||||
}
|
||||
-1
@@ -37,7 +37,6 @@ final class ImageViewController: UIViewController {
|
||||
case withHeaderFooter
|
||||
}
|
||||
|
||||
@available(iOS 11.0, *)
|
||||
func layoutGuideFor(mode: Mode) -> UILayoutGuide {
|
||||
switch mode {
|
||||
case .onlyImage:
|
||||
+1
-2
@@ -3,7 +3,7 @@
|
||||
import UIKit
|
||||
import FloatingPanel
|
||||
|
||||
final class AdaptiveLayoutTestViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
|
||||
final class TableViewControllerForAdaptiveLayout: UIViewController, UITableViewDataSource, UITableViewDelegate {
|
||||
class PanelLayout: FloatingPanelLayout {
|
||||
let position: FloatingPanelPosition = .bottom
|
||||
let initialState: FloatingPanelState = .full
|
||||
@@ -75,7 +75,6 @@ final class AdaptiveLayoutTestViewController: UIViewController, UITableViewDataS
|
||||
}
|
||||
|
||||
class IntrinsicTableView: UITableView {
|
||||
|
||||
override var contentSize:CGSize {
|
||||
didSet {
|
||||
invalidateIntrinsicContentSize()
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
|
||||
|
||||
import UIKit
|
||||
|
||||
@available(iOS 14, *)
|
||||
class DebugListCollectionViewController: UIViewController {
|
||||
|
||||
enum Section {
|
||||
case main
|
||||
}
|
||||
|
||||
var dataSource: UICollectionViewDiffableDataSource<Section, Int>! = nil
|
||||
var collectionView: UICollectionView! = nil
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
navigationItem.title = "List"
|
||||
configureHierarchy()
|
||||
configureDataSource()
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 14, *)
|
||||
extension DebugListCollectionViewController {
|
||||
/// - Tag: List
|
||||
private func createLayout() -> UICollectionViewLayout {
|
||||
var config = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
|
||||
config.trailingSwipeActionsConfigurationProvider = { indexPath -> UISwipeActionsConfiguration? in
|
||||
return UISwipeActionsConfiguration(
|
||||
actions: [UIContextualAction(
|
||||
style: .destructive,
|
||||
title: "Delete",
|
||||
handler: { _, _, completion in
|
||||
// Do nothing now
|
||||
}
|
||||
)]
|
||||
)
|
||||
}
|
||||
return UICollectionViewCompositionalLayout.list(using: config)
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 14, *)
|
||||
extension DebugListCollectionViewController {
|
||||
private func configureHierarchy() {
|
||||
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: createLayout())
|
||||
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
view.addSubview(collectionView)
|
||||
collectionView.delegate = self
|
||||
}
|
||||
private func configureDataSource() {
|
||||
|
||||
let cellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, Int> { (cell, indexPath, item) in
|
||||
var content = cell.defaultContentConfiguration()
|
||||
content.text = "\(item)"
|
||||
cell.contentConfiguration = content
|
||||
}
|
||||
|
||||
dataSource = UICollectionViewDiffableDataSource<Section, Int>(collectionView: collectionView) {
|
||||
(collectionView: UICollectionView, indexPath: IndexPath, identifier: Int) -> UICollectionViewCell? in
|
||||
|
||||
return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: identifier)
|
||||
}
|
||||
|
||||
var snapshot = NSDiffableDataSourceSnapshot<Section, Int>()
|
||||
snapshot.appendSections([.main])
|
||||
snapshot.appendItems(Array(0..<94))
|
||||
dataSource.apply(snapshot, animatingDifferences: false)
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 14, *)
|
||||
extension DebugListCollectionViewController: UICollectionViewDelegate {
|
||||
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||
collectionView.deselectItem(at: indexPath, animated: true)
|
||||
}
|
||||
}
|
||||
@@ -11,9 +11,7 @@ final class DebugTextViewController: UIViewController, UITextViewDelegate {
|
||||
textView.delegate = self
|
||||
print("viewDidLoad: TextView --- ", textView.contentOffset, textView.contentInset)
|
||||
|
||||
if #available(iOS 11.0, *) {
|
||||
textView.contentInsetAdjustmentBehavior = .never
|
||||
}
|
||||
textView.contentInsetAdjustmentBehavior = .never
|
||||
}
|
||||
|
||||
override func viewWillLayoutSubviews() {
|
||||
@@ -32,9 +30,7 @@ final class DebugTextViewController: UIViewController, UITextViewDelegate {
|
||||
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
print("TextView --- ", scrollView.contentOffset, scrollView.contentInset)
|
||||
if #available(iOS 11.0, *) {
|
||||
print("TextView --- ", scrollView.adjustedContentInset)
|
||||
}
|
||||
print("TextView --- ", scrollView.adjustedContentInset)
|
||||
}
|
||||
|
||||
@IBAction func toggleTopMargin(_ sender: UISwitch) {
|
||||
|
||||
@@ -14,21 +14,17 @@ final class SettingsViewController: InspectableViewController {
|
||||
|
||||
override func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
if #available(iOS 11.0, *) {
|
||||
let prefersLargeTitles = navigationController!.navigationBar.prefersLargeTitles
|
||||
largeTitlesSwitch.setOn(prefersLargeTitles, animated: false)
|
||||
} else {
|
||||
largeTitlesSwitch.isEnabled = false
|
||||
}
|
||||
let prefersLargeTitles = navigationController!.navigationBar.prefersLargeTitles
|
||||
largeTitlesSwitch.setOn(prefersLargeTitles, animated: false)
|
||||
|
||||
let isTranslucent = navigationController!.navigationBar.isTranslucent
|
||||
translucentSwitch.setOn(isTranslucent, animated: false)
|
||||
}
|
||||
|
||||
@IBAction func toggleLargeTitle(_ sender: UISwitch) {
|
||||
if #available(iOS 11.0, *) {
|
||||
navigationController?.navigationBar.prefersLargeTitles = sender.isOn
|
||||
}
|
||||
navigationController?.navigationBar.prefersLargeTitles = sender.isOn
|
||||
}
|
||||
|
||||
@IBAction func toggleTranslucent(_ sender: UISwitch) {
|
||||
// White non-translucent navigation bar, supports dark appearance
|
||||
if #available(iOS 15, *) {
|
||||
|
||||
@@ -238,13 +238,8 @@ class ThreeTabBarPanelLayout: FloatingPanelLayout {
|
||||
}
|
||||
|
||||
func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint] {
|
||||
if #available(iOS 11.0, *) {
|
||||
leftConstraint = surfaceView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 0.0)
|
||||
rightConstraint = surfaceView.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor, constant: 0.0)
|
||||
} else {
|
||||
leftConstraint = surfaceView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 0.0)
|
||||
rightConstraint = surfaceView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 0.0)
|
||||
}
|
||||
leftConstraint = surfaceView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 0.0)
|
||||
rightConstraint = surfaceView.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor, constant: 0.0)
|
||||
return [ leftConstraint, rightConstraint ]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
|
||||
|
||||
import UIKit
|
||||
|
||||
class UnavailableViewController: UIViewController {
|
||||
weak var label: UILabel!
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
let label = UILabel()
|
||||
label.text = "Unavailable content"
|
||||
label.numberOfLines = 0
|
||||
label.textAlignment = .center
|
||||
label.frame = view.bounds
|
||||
label.autoresizingMask = [
|
||||
.flexibleTopMargin,
|
||||
.flexibleLeftMargin,
|
||||
.flexibleBottomMargin,
|
||||
.flexibleRightMargin
|
||||
]
|
||||
view.addSubview(label)
|
||||
self.label = label
|
||||
}
|
||||
}
|
||||
@@ -33,22 +33,10 @@ class CustomLayoutGuide: LayoutGuideProvider {
|
||||
|
||||
extension UIViewController {
|
||||
var layoutInsets: UIEdgeInsets {
|
||||
if #available(iOS 11.0, *) {
|
||||
return view.safeAreaInsets
|
||||
} else {
|
||||
return UIEdgeInsets(top: topLayoutGuide.length,
|
||||
left: 0.0,
|
||||
bottom: bottomLayoutGuide.length,
|
||||
right: 0.0)
|
||||
}
|
||||
return view.safeAreaInsets
|
||||
}
|
||||
|
||||
var layoutGuide: LayoutGuideProvider {
|
||||
if #available(iOS 11.0, *) {
|
||||
return view!.safeAreaLayoutGuide
|
||||
} else {
|
||||
return CustomLayoutGuide(topAnchor: topLayoutGuide.bottomAnchor,
|
||||
bottomAnchor: bottomLayoutGuide.topAnchor)
|
||||
}
|
||||
return view.safeAreaLayoutGuide
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,14 +15,11 @@ extension MainViewController {
|
||||
tableView.dataSource = self
|
||||
tableView.delegate = self
|
||||
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
|
||||
automaticallyAdjustsScrollViewInsets = false
|
||||
|
||||
let searchController = UISearchController(searchResultsController: nil)
|
||||
if #available(iOS 11.0, *) {
|
||||
navigationItem.searchController = searchController
|
||||
navigationItem.hidesSearchBarWhenScrolling = false
|
||||
navigationItem.largeTitleDisplayMode = .automatic
|
||||
}
|
||||
navigationItem.searchController = searchController
|
||||
navigationItem.hidesSearchBarWhenScrolling = false
|
||||
navigationItem.largeTitleDisplayMode = .automatic
|
||||
var insets = UIEdgeInsets.zero
|
||||
insets.bottom += 69.0
|
||||
tableView.contentInset = insets
|
||||
@@ -33,12 +30,10 @@ extension MainViewController {
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
if #available(iOS 11.0, *) {
|
||||
if let observation = navigationController?.navigationBar.observe(\.prefersLargeTitles, changeHandler: { (bar, _) in
|
||||
self.tableView.reloadData()
|
||||
}) {
|
||||
observations.append(observation)
|
||||
}
|
||||
if let observation = navigationController?.navigationBar.observe(\.prefersLargeTitles, changeHandler: { (bar, _) in
|
||||
self.tableView.reloadData()
|
||||
}) {
|
||||
observations.append(observation)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,12 +51,8 @@ extension MainViewController {
|
||||
|
||||
extension MainViewController: UITableViewDataSource {
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
if #available(iOS 11.0, *) {
|
||||
if navigationController?.navigationBar.prefersLargeTitles == true {
|
||||
return UseCase.allCases.count + 30
|
||||
} else {
|
||||
return UseCase.allCases.count
|
||||
}
|
||||
if navigationController?.navigationBar.prefersLargeTitles == true {
|
||||
return UseCase.allCases.count + 30
|
||||
} else {
|
||||
return UseCase.allCases.count
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import UIKit
|
||||
enum UseCase: Int, CaseIterable {
|
||||
case trackingTableView
|
||||
case trackingTextView
|
||||
case trackingCollectionViewList
|
||||
case showDetail
|
||||
case showModal
|
||||
case showPanelModal
|
||||
@@ -22,14 +23,18 @@ enum UseCase: Int, CaseIterable {
|
||||
case showNavigationController
|
||||
case showTopPositionedPanel
|
||||
case showAdaptivePanel
|
||||
case showAdaptivePanelWithCustomGuide
|
||||
case showAdaptivePanelWithTableView
|
||||
case showAdaptivePanelWithCollectionView
|
||||
case showAdaptivePanelWithCompositionalCollectionView
|
||||
case showCustomStatePanel
|
||||
case showCustomBackdrop
|
||||
}
|
||||
|
||||
extension UseCase {
|
||||
var name: String {
|
||||
switch self {
|
||||
case .trackingTableView: return "Scroll tracking(TableView)"
|
||||
case .trackingCollectionViewList: return "Scroll tracking(List CollectionView)"
|
||||
case .trackingTextView: return "Scroll tracking(TextView)"
|
||||
case .showDetail: return "Show Detail Panel"
|
||||
case .showModal: return "Show Modal"
|
||||
@@ -48,8 +53,11 @@ extension UseCase {
|
||||
case .showNavigationController: return "Show Navigation Controller"
|
||||
case .showTopPositionedPanel: return "Show Top Positioned Panel"
|
||||
case .showAdaptivePanel: return "Show Adaptive Panel"
|
||||
case .showAdaptivePanelWithCustomGuide: return "Show Adaptive Panel (Custom Layout Guide)"
|
||||
case .showAdaptivePanelWithTableView: return "Show Adaptive Panel (TableView)"
|
||||
case .showAdaptivePanelWithCollectionView: return "Show Adaptive Panel (CollectionView)"
|
||||
case .showAdaptivePanelWithCompositionalCollectionView: return "Show Adaptive Panel (Compositional CollectionView)"
|
||||
case .showCustomStatePanel: return "Show Panel with Custom state"
|
||||
case .showCustomBackdrop: return "Show Panel with Custom Backdrop"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,6 +71,13 @@ extension UseCase {
|
||||
private var content: Content {
|
||||
switch self {
|
||||
case .trackingTableView: return .viewController(DebugTableViewController())
|
||||
case .trackingCollectionViewList:
|
||||
if #available(iOS 14, *) {
|
||||
return .viewController(DebugListCollectionViewController())
|
||||
} else {
|
||||
let msg = "UICollectionLayoutListConfiguration is unavailable.\nBuild this app on iOS 14 and later."
|
||||
return makeUnavailableViewContent(message: msg)
|
||||
}
|
||||
case .trackingTextView: return .storyboard("ConsoleViewController") // Storyboard only
|
||||
case .showDetail: return .storyboard(String(describing: DetailViewController.self))
|
||||
case .showModal: return .storyboard(String(describing: ModalViewController.self))
|
||||
@@ -81,8 +96,19 @@ extension UseCase {
|
||||
case .showNavigationController: return .storyboard("RootNavigationController") // Storyboard only
|
||||
case .showTopPositionedPanel: return .viewController(DebugTableViewController())
|
||||
case .showAdaptivePanel: return .storyboard(String(describing: ImageViewController.self))
|
||||
case .showAdaptivePanelWithCustomGuide: return .storyboard(String(describing: AdaptiveLayoutTestViewController.self))
|
||||
case .showAdaptivePanelWithTableView: return .storyboard(String(describing: TableViewControllerForAdaptiveLayout.self))
|
||||
case .showAdaptivePanelWithCollectionView,
|
||||
.showAdaptivePanelWithCompositionalCollectionView:
|
||||
if #available(iOS 13, *) {
|
||||
let vc = CollectionViewControllerForAdaptiveLayout()
|
||||
vc.layoutType = self == .showAdaptivePanelWithCollectionView ? .flow : .compositional
|
||||
return .viewController(vc)
|
||||
} else {
|
||||
let msg = "Compositional layout is unavailable.\nBuild this app on iOS 13 and later."
|
||||
return makeUnavailableViewContent(message: msg)
|
||||
}
|
||||
case .showCustomStatePanel: return .viewController(DebugTableViewController())
|
||||
case .showCustomBackdrop: return .viewController(UIViewController())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,4 +121,11 @@ extension UseCase {
|
||||
return vc
|
||||
}
|
||||
}
|
||||
|
||||
private func makeUnavailableViewContent(message: String) -> Content {
|
||||
let vc = UnavailableViewController()
|
||||
vc.loadViewIfNeeded()
|
||||
vc.label.text = message
|
||||
return .viewController(vc)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,30 @@ extension UseCaseController {
|
||||
fpc.ext_trackScrollView(in: contentVC)
|
||||
addMain(panel: fpc)
|
||||
|
||||
case .trackingCollectionViewList:
|
||||
let fpc = FloatingPanelController()
|
||||
fpc.delegate = self
|
||||
fpc.contentInsetAdjustmentBehavior = .always
|
||||
fpc.surfaceView.appearance = {
|
||||
let appearance = SurfaceAppearance()
|
||||
appearance.cornerRadius = 6.0
|
||||
return appearance
|
||||
}()
|
||||
|
||||
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(UseCaseController.handleSurface(tapGesture:)))
|
||||
tapGesture.cancelsTouchesInView = false
|
||||
tapGesture.numberOfTapsRequired = 2
|
||||
// Prevents a delay to response a tap in menus of DebugTableViewController.
|
||||
tapGesture.delaysTouchesEnded = false
|
||||
fpc.surfaceView.addGestureRecognizer(tapGesture)
|
||||
|
||||
fpc.set(contentViewController: contentVC)
|
||||
if #available(iOS 14, *),
|
||||
let scrollView = (fpc.contentViewController as? DebugListCollectionViewController)?.collectionView {
|
||||
fpc.track(scrollView: scrollView)
|
||||
}
|
||||
addMain(panel: fpc)
|
||||
|
||||
case .trackingTextView:
|
||||
let fpc = FloatingPanelController()
|
||||
fpc.delegate = self
|
||||
@@ -239,13 +263,13 @@ extension UseCaseController {
|
||||
fpc.set(contentViewController: contentVC)
|
||||
fpc.ext_trackScrollView(in: contentVC)
|
||||
if case let contentVC as ImageViewController = contentVC {
|
||||
let mode: ImageViewController.Mode = (useCase == .showAdaptivePanelWithCustomGuide) ? .withHeaderFooter : .onlyImage
|
||||
let mode: ImageViewController.Mode = (useCase == .showAdaptivePanelWithTableView) ? .withHeaderFooter : .onlyImage
|
||||
let layoutGuide = contentVC.layoutGuideFor(mode: mode)
|
||||
fpc.layout = ImageViewController.PanelLayout(targetGuide: layoutGuide)
|
||||
}
|
||||
addMain(panel: fpc)
|
||||
|
||||
case .showAdaptivePanelWithCustomGuide:
|
||||
case .showAdaptivePanelWithTableView:
|
||||
let fpc = FloatingPanelController()
|
||||
fpc.isRemovalInteractionEnabled = true
|
||||
fpc.contentInsetAdjustmentBehavior = .always
|
||||
@@ -255,10 +279,25 @@ extension UseCaseController {
|
||||
return appearance
|
||||
}()
|
||||
|
||||
|
||||
fpc.set(contentViewController: contentVC)
|
||||
fpc.ext_trackScrollView(in: contentVC)
|
||||
fpc.layout = AdaptiveLayoutTestViewController.PanelLayout(targetGuide: contentVC.view.makeBoundsLayoutGuide())
|
||||
fpc.track(scrollView: (contentVC as! TableViewControllerForAdaptiveLayout).tableView)
|
||||
fpc.layout = TableViewControllerForAdaptiveLayout.PanelLayout(targetGuide: contentVC.view.makeBoundsLayoutGuide())
|
||||
addMain(panel: fpc)
|
||||
|
||||
case .showAdaptivePanelWithCollectionView, .showAdaptivePanelWithCompositionalCollectionView:
|
||||
let fpc = FloatingPanelController()
|
||||
fpc.isRemovalInteractionEnabled = true
|
||||
fpc.contentInsetAdjustmentBehavior = .always
|
||||
fpc.surfaceView.appearance = {
|
||||
let appearance = SurfaceAppearance()
|
||||
appearance.cornerRadius = 6.0
|
||||
return appearance
|
||||
}()
|
||||
fpc.set(contentViewController: contentVC)
|
||||
if #available(iOS 13, *) {
|
||||
fpc.track(scrollView: (contentVC as! CollectionViewControllerForAdaptiveLayout).collectionView)
|
||||
fpc.layout = CollectionViewControllerForAdaptiveLayout.PanelLayout(targetGuide: contentVC.view.makeBoundsLayoutGuide())
|
||||
}
|
||||
addMain(panel: fpc)
|
||||
|
||||
case .showCustomStatePanel:
|
||||
@@ -273,6 +312,52 @@ extension UseCaseController {
|
||||
fpc.set(contentViewController: contentVC)
|
||||
fpc.ext_trackScrollView(in: contentVC)
|
||||
addMain(panel: fpc)
|
||||
|
||||
case .showCustomBackdrop:
|
||||
class BlurBackdropView: BackdropView {
|
||||
var effectView: UIVisualEffectView!
|
||||
override var alpha: CGFloat {
|
||||
set {
|
||||
effectView.alpha = newValue
|
||||
}
|
||||
get {
|
||||
effectView.alpha
|
||||
}
|
||||
}
|
||||
override init() {
|
||||
super.init()
|
||||
|
||||
let effect = UIBlurEffect(style: .prominent)
|
||||
let effectView = UIVisualEffectView(effect: effect)
|
||||
addSubview(effectView)
|
||||
effectView.frame = bounds
|
||||
effectView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
self.effectView = effectView
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
class CustomBottomLayout: FloatingPanelBottomLayout {
|
||||
override var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] {
|
||||
return [
|
||||
.full: FloatingPanelLayoutAnchor(absoluteInset: 100.0, edge: .top, referenceGuide: .safeArea),
|
||||
.half: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .safeArea),
|
||||
.tip: FloatingPanelLayoutAnchor(fractionalInset: 0.1, edge: .bottom, referenceGuide: .safeArea),
|
||||
]
|
||||
}
|
||||
override func backdropAlpha(for state: FloatingPanelState) -> CGFloat {
|
||||
return state == .full ? 0.8 : 0.0
|
||||
}
|
||||
}
|
||||
|
||||
let fpc = FloatingPanelController()
|
||||
fpc.delegate = self
|
||||
fpc.set(contentViewController: contentVC)
|
||||
fpc.backdropView = BlurBackdropView()
|
||||
fpc.layout = CustomBottomLayout()
|
||||
addMain(panel: fpc)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -322,8 +407,16 @@ extension UseCaseController {
|
||||
}
|
||||
|
||||
extension UseCaseController: FloatingPanelControllerDelegate {
|
||||
func floatingPanel(
|
||||
_ fpc: FloatingPanelController,
|
||||
shouldAllowToScroll scrollView: UIScrollView,
|
||||
in state: FloatingPanelState
|
||||
) -> Bool {
|
||||
return state == .full || state == .half
|
||||
}
|
||||
|
||||
func floatingPanel(_ vc: FloatingPanelController, contentOffsetForPinning trackingScrollView: UIScrollView) -> CGPoint {
|
||||
if useCase == .showNavigationController, #available(iOS 11.0, *) {
|
||||
if useCase == .showNavigationController {
|
||||
// 148.0 is the SafeArea's top value for a navigation bar with a large title.
|
||||
return CGPoint(x: 0.0, y: 0.0 - trackingScrollView.contentInset.top - 148.0)
|
||||
}
|
||||
@@ -404,7 +497,7 @@ private extension FloatingPanelController {
|
||||
case let contentVC as ImageViewController:
|
||||
track(scrollView: contentVC.scrollView)
|
||||
|
||||
case let contentVC as AdaptiveLayoutTestViewController:
|
||||
case let contentVC as TableViewControllerForAdaptiveLayout:
|
||||
track(scrollView: contentVC.tableView)
|
||||
|
||||
default:
|
||||
|
||||
@@ -81,7 +81,7 @@ class MainViewController: UIViewController, FloatingPanelControllerDelegate {
|
||||
}
|
||||
|
||||
private func hideStockTickerBanner() {
|
||||
// Dimiss top bar with dissolve animation
|
||||
// Dismiss top bar with dissolve animation
|
||||
UIView.animate(withDuration: 0.25) {
|
||||
self.topBannerView.alpha = 0.0
|
||||
self.labelStackView.alpha = 1.0
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
Pod::Spec.new do |s|
|
||||
|
||||
s.name = "FloatingPanel"
|
||||
s.version = "2.7.0"
|
||||
s.version = "2.8.6"
|
||||
s.summary = "FloatingPanel is a clean and easy-to-use UI component of a floating panel interface."
|
||||
s.description = <<-DESC
|
||||
FloatingPanel is a clean and easy-to-use UI component for a new interface introduced in Apple Maps, Shortcuts and Stocks app.
|
||||
The new interface displays the related contents and utilities in parallel as a user wants.
|
||||
DESC
|
||||
s.homepage = "https://github.com/SCENEE/FloatingPanel"
|
||||
s.homepage = "https://github.com/scenee/FloatingPanel"
|
||||
s.author = "Shin Yamamoto"
|
||||
s.social_media_url = "https://twitter.com/scenee"
|
||||
|
||||
s.platform = :ios, "10.0"
|
||||
s.source = { :git => "https://github.com/SCENEE/FloatingPanel.git", :tag => s.version.to_s }
|
||||
s.platform = :ios, "11.0"
|
||||
s.source = { :git => "https://github.com/scenee/FloatingPanel.git", :tag => s.version.to_s }
|
||||
s.source_files = "Sources/*.swift"
|
||||
s.swift_version = '5.0'
|
||||
|
||||
|
||||
@@ -462,12 +462,13 @@
|
||||
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||
INFOPLIST_FILE = Sources/Info.plist;
|
||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingPanel;
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
SKIP_INSTALL = YES;
|
||||
@@ -493,12 +494,13 @@
|
||||
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||
INFOPLIST_FILE = Sources/Info.plist;
|
||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingPanel;
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
SKIP_INSTALL = YES;
|
||||
@@ -521,6 +523,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingPanelTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
@@ -541,6 +544,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingPanelTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
@@ -627,16 +631,17 @@
|
||||
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||
INFOPLIST_FILE = Sources/Info.plist;
|
||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingPanel;
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "TEST DEBUG __FP_LOG";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "TEST DEBUG FP_LOG";
|
||||
SWIFT_COMPILATION_MODE = singlefile;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
@@ -657,6 +662,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingPanelTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ import PackageDescription
|
||||
let package = Package(
|
||||
name: "FloatingPanel",
|
||||
platforms: [
|
||||
.iOS(.v10)
|
||||
.iOS(.v11)
|
||||
],
|
||||
products: [
|
||||
// Products define the executables and libraries produced by a package, and make them visible to other packages.
|
||||
|
||||
@@ -2,14 +2,13 @@
|
||||
[](https://cocoapods.org/pods/FloatingPanel)
|
||||
[](https://cocoapods.org/pods/FloatingPanel)
|
||||

|
||||
[](https://github.com/Carthage/Carthage)
|
||||
|
||||
# FloatingPanel
|
||||
|
||||
FloatingPanel is a simple and easy-to-use UI component designed for a user interface featured in Apple Maps, Shortcuts and Stocks app.
|
||||
The user interface displays related content and utilities alongside the main content.
|
||||
|
||||
Please see also [the API reference](https://floatingpanel.github.io/2.7.0/documentation/floatingpanel/) for more details.
|
||||
Please see also [the API reference@SPI](https://swiftpackageindex.com/scenee/FloatingPanel/2.8.6/documentation/floatingpanel) for more details.
|
||||
|
||||

|
||||

|
||||
@@ -21,47 +20,46 @@ Please see also [the API reference](https://floatingpanel.github.io/2.7.0/docume
|
||||
- [Features](#features)
|
||||
- [Requirements](#requirements)
|
||||
- [Installation](#installation)
|
||||
- [CocoaPods](#cocoapods)
|
||||
- [Carthage](#carthage)
|
||||
- [Swift Package Manager](#swift-package-manager)
|
||||
- [CocoaPods](#cocoapods)
|
||||
- [Swift Package Manager](#swift-package-manager)
|
||||
- [Getting Started](#getting-started)
|
||||
- [Add a floating panel as a child view controller](#add-a-floating-panel-as-a-child-view-controller)
|
||||
- [Present a floating panel as a modality](#present-a-floating-panel-as-a-modality)
|
||||
- [Add a floating panel as a child view controller](#add-a-floating-panel-as-a-child-view-controller)
|
||||
- [Present a floating panel as a modality](#present-a-floating-panel-as-a-modality)
|
||||
- [View hierarchy](#view-hierarchy)
|
||||
- [Usage](#usage)
|
||||
- [Show/Hide a floating panel in a view with your view hierarchy](#showhide-a-floating-panel-in-a-view-with-your-view-hierarchy)
|
||||
- [Scale the content view when the surface position changes](#scale-the-content-view-when-the-surface-position-changes)
|
||||
- [Customize the layout with `FloatingPanelLayout` protocol](#customize-the-layout-with-floatingpanellayout-protocol)
|
||||
- [Change the initial layout](#change-the-initial-layout)
|
||||
- [Update your panel layout](#update-your-panel-layout)
|
||||
- [Support your landscape layout](#support-your-landscape-layout)
|
||||
- [Use the intrinsic size of a content in your panel layout](#use-the-intrinsic-size-of-a-content-in-your-panel-layout)
|
||||
- [Specify an anchor for each state by an inset of the `FloatingPanelController.view` frame](#specify-an-anchor-for-each-state-by-an-inset-of-the-floatingpanelcontrollerview-frame)
|
||||
- [Change the backdrop alpha](#change-the-backdrop-alpha)
|
||||
- [Using custome panel states](#using-custome-panel-states)
|
||||
- [Customize the behavior with `FloatingPanelBehavior` protocol](#customize-the-behavior-with-floatingpanelbehavior-protocol)
|
||||
- [Modify your floating panel's interaction](#modify-your-floating-panels-interaction)
|
||||
- [Activate the rubber-band effect on panel edges](#activate-the-rubber-band-effect-on-panel-edges)
|
||||
- [Manage the projection of a pan gesture momentum](#manage-the-projection-of-a-pan-gesture-momentum)
|
||||
- [Specify the panel move's boundary](#specify-the-panel-moves-boundary)
|
||||
- [Customize the surface design](#customize-the-surface-design)
|
||||
- [Modify your surface appearance](#modify-your-surface-appearance)
|
||||
- [Use a custom grabber handle](#use-a-custom-grabber-handle)
|
||||
- [Customize layout of the grabber handle](#customize-layout-of-the-grabber-handle)
|
||||
- [Customize content padding from surface edges](#customize-content-padding-from-surface-edges)
|
||||
- [Customize margins of the surface edges](#customize-margins-of-the-surface-edges)
|
||||
- [Customize gestures](#customize-gestures)
|
||||
- [Suppress the panel interaction](#suppress-the-panel-interaction)
|
||||
- [Add tap gestures to the surface view](#add-tap-gestures-to-the-surface-view)
|
||||
- [Interrupt the delegate methods of `FloatingPanelController.panGestureRecognizer`](#interrupt-the-delegate-methods-of-floatingpanelcontrollerpangesturerecognizer)
|
||||
- [Create an additional floating panel for a detail](#create-an-additional-floating-panel-for-a-detail)
|
||||
- [Move a position with an animation](#move-a-position-with-an-animation)
|
||||
- [Work your contents together with a floating panel behavior](#work-your-contents-together-with-a-floating-panel-behavior)
|
||||
- [Enabling the tap-to-dismiss action of the backdrop view](#enabling-the-tap-to-dismiss-action-of-the-backdrop-view)
|
||||
- [Show/Hide a floating panel in a view with your view hierarchy](#showhide-a-floating-panel-in-a-view-with-your-view-hierarchy)
|
||||
- [Scale the content view when the surface position changes](#scale-the-content-view-when-the-surface-position-changes)
|
||||
- [Customize the layout with `FloatingPanelLayout` protocol](#customize-the-layout-with-floatingpanellayout-protocol)
|
||||
- [Change the initial layout](#change-the-initial-layout)
|
||||
- [Update your panel layout](#update-your-panel-layout)
|
||||
- [Support your landscape layout](#support-your-landscape-layout)
|
||||
- [Use the intrinsic size of a content in your panel layout](#use-the-intrinsic-size-of-a-content-in-your-panel-layout)
|
||||
- [Specify an anchor for each state by an inset of the `FloatingPanelController.view` frame](#specify-an-anchor-for-each-state-by-an-inset-of-the-floatingpanelcontrollerview-frame)
|
||||
- [Change the backdrop alpha](#change-the-backdrop-alpha)
|
||||
- [Using custome panel states](#using-custome-panel-states)
|
||||
- [Customize the behavior with `FloatingPanelBehavior` protocol](#customize-the-behavior-with-floatingpanelbehavior-protocol)
|
||||
- [Modify your floating panel's interaction](#modify-your-floating-panels-interaction)
|
||||
- [Activate the rubber-band effect on panel edges](#activate-the-rubber-band-effect-on-panel-edges)
|
||||
- [Manage the projection of a pan gesture momentum](#manage-the-projection-of-a-pan-gesture-momentum)
|
||||
- [Specify the panel move's boundary](#specify-the-panel-moves-boundary)
|
||||
- [Customize the surface design](#customize-the-surface-design)
|
||||
- [Modify your surface appearance](#modify-your-surface-appearance)
|
||||
- [Use a custom grabber handle](#use-a-custom-grabber-handle)
|
||||
- [Customize layout of the grabber handle](#customize-layout-of-the-grabber-handle)
|
||||
- [Customize content padding from surface edges](#customize-content-padding-from-surface-edges)
|
||||
- [Customize margins of the surface edges](#customize-margins-of-the-surface-edges)
|
||||
- [Customize gestures](#customize-gestures)
|
||||
- [Suppress the panel interaction](#suppress-the-panel-interaction)
|
||||
- [Add tap gestures to the surface view](#add-tap-gestures-to-the-surface-view)
|
||||
- [Interrupt the delegate methods of `FloatingPanelController.panGestureRecognizer`](#interrupt-the-delegate-methods-of-floatingpanelcontrollerpangesturerecognizer)
|
||||
- [Create an additional floating panel for a detail](#create-an-additional-floating-panel-for-a-detail)
|
||||
- [Move a position with an animation](#move-a-position-with-an-animation)
|
||||
- [Work your contents together with a floating panel behavior](#work-your-contents-together-with-a-floating-panel-behavior)
|
||||
- [Enabling the tap-to-dismiss action of the backdrop view](#enabling-the-tap-to-dismiss-action-of-the-backdrop-view)
|
||||
- [Allow to scroll content of the tracking scroll view in addition to the most expanded state](#allow-to-scroll-content-of-the-tracking-scroll-view-in-addition-to-the-most-expanded-state)
|
||||
- [Notes](#notes)
|
||||
- ['Show' or 'Show Detail' Segues from `FloatingPanelController`'s content view controller](#show-or-show-detail-segues-from-floatingpanelcontrollers-content-view-controller)
|
||||
- [UISearchController issue](#uisearchcontroller-issue)
|
||||
- [FloatingPanelSurfaceView's issue on iOS 10](#floatingpanelsurfaceviews-issue-on-ios-10)
|
||||
- ['Show' or 'Show Detail' Segues from `FloatingPanelController`'s content view controller](#show-or-show-detail-segues-from-floatingpanelcontrollers-content-view-controller)
|
||||
- [UISearchController issue](#uisearchcontroller-issue)
|
||||
- [Maintainer](#maintainer)
|
||||
- [License](#license)
|
||||
|
||||
@@ -93,10 +91,6 @@ Examples can be found here:
|
||||
|
||||
FloatingPanel is written in Swift 5.0+ and compatible with iOS 11.0+.
|
||||
|
||||
While it still supports iOS 10, it is recommended to use this library on iOS 11+.
|
||||
|
||||
:pencil2: If you'd like to use Swift 4.0, please use FloatingPanel v1.
|
||||
|
||||
## Installation
|
||||
|
||||
### CocoaPods
|
||||
@@ -108,16 +102,6 @@ it, simply add the following line to your Podfile:
|
||||
pod 'FloatingPanel'
|
||||
```
|
||||
|
||||
:pencil2: FloatingPanel v1.7.0 or later requires CocoaPods v1.7.0+ for `swift_versions` support.
|
||||
|
||||
### Carthage
|
||||
|
||||
For [Carthage](https://github.com/Carthage/Carthage), add the following to your `Cartfile`:
|
||||
|
||||
```ogdl
|
||||
github "scenee/FloatingPanel"
|
||||
```
|
||||
|
||||
### Swift Package Manager
|
||||
|
||||
Follow [this doc](https://developer.apple.com/documentation/swift_packages/adding_package_dependencies_to_your_app).
|
||||
@@ -168,7 +152,8 @@ self.present(fpc, animated: true, completion: nil)
|
||||
|
||||
You can show a floating panel over UINavigationController from the container view controllers as a modality of `.overCurrentContext` style.
|
||||
|
||||
:pencil2: FloatingPanelController has the custom presentation controller. If you would like to customize the presentation/dismissal, please see [Transitioning](https://github.com/SCENEE/FloatingPanel/blob/master/Sources/Transitioning.swift).
|
||||
> [!NOTE]
|
||||
> FloatingPanelController has the custom presentation controller. If you would like to customize the presentation/dismissal, please see [Transitioning](https://github.com/SCENEE/FloatingPanel/blob/master/Sources/Transitioning.swift).
|
||||
|
||||
## View hierarchy
|
||||
|
||||
@@ -252,7 +237,8 @@ fpc.contentMode = .fitToBounds
|
||||
|
||||
Otherwise, `FloatingPanelController` fixes the content by the height of the top most position.
|
||||
|
||||
:pencil2: In `.fitToBounds` mode, the surface height changes as following a user interaction so that you have a responsibility to configure Auto Layout constrains not to break the layout of a content view by the elastic surface height.
|
||||
> [!NOTE]
|
||||
> In `.fitToBounds` mode, the surface height changes as following a user interaction so that you have a responsibility to configure Auto Layout constrains not to break the layout of a content view by the elastic surface height.
|
||||
|
||||
### Customize the layout with `FloatingPanelLayout` protocol
|
||||
|
||||
@@ -350,7 +336,8 @@ class IntrinsicPanelLayout: FloatingPanelLayout {
|
||||
}
|
||||
```
|
||||
|
||||
:pencil2: `FloatingPanelIntrinsicLayout` is deprecated on v1.
|
||||
> [!WARNING]
|
||||
> `FloatingPanelIntrinsicLayout` is deprecated on v1.
|
||||
|
||||
#### Specify an anchor for each state by an inset of the `FloatingPanelController.view` frame
|
||||
|
||||
@@ -367,7 +354,8 @@ class MyFullScreenLayout: FloatingPanelLayout {
|
||||
}
|
||||
```
|
||||
|
||||
:pencil2: `FloatingPanelFullScreenLayout` is deprecated on v1.
|
||||
> [!WARNING]
|
||||
> `FloatingPanelFullScreenLayout` is deprecated on v1.
|
||||
|
||||
#### Change the backdrop alpha
|
||||
|
||||
@@ -431,7 +419,8 @@ class CustomPanelBehavior: FloatingPanelBehavior {
|
||||
}
|
||||
```
|
||||
|
||||
:pencil2: `floatingPanel(_ vc:behaviorFor:)` is deprecated on v1.
|
||||
> [!WARNING]
|
||||
> `floatingPanel(_ vc:behaviorFor:)` is deprecated on v1.
|
||||
|
||||
#### Activate the rubber-band effect on panel edges
|
||||
|
||||
@@ -473,7 +462,8 @@ func floatingPanelDidMove(_ vc: FloatingPanelController) {
|
||||
}
|
||||
```
|
||||
|
||||
:pencil2: `{top,bottom}InteractionBuffer` property is removed from `FloatingPanelLayout` since v2.
|
||||
> [!WARNING]
|
||||
> `{top,bottom}InteractionBuffer` property is removed from `FloatingPanelLayout` since v2.
|
||||
|
||||
### Customize the surface design
|
||||
|
||||
@@ -514,7 +504,8 @@ fpc.surfaceView.grabberHandlePadding = 10.0
|
||||
fpc.surfaceView.grabberHandleSize = .init(width: 44.0, height: 12.0)
|
||||
```
|
||||
|
||||
:pencil2: Note that `grabberHandleSize` width and height are reversed in the left/right position.
|
||||
> [!NOTE]
|
||||
> `grabberHandleSize` width and height are reversed in the left/right position.
|
||||
|
||||
#### Customize content padding from surface edges
|
||||
|
||||
@@ -663,6 +654,24 @@ The tap-to-dismiss action is disabled by default. So it needs to be enabled as b
|
||||
fpc.backdropView.dismissalTapGestureRecognizer.isEnabled = true
|
||||
```
|
||||
|
||||
### Allow to scroll content of the tracking scroll view in addition to the most expanded state
|
||||
|
||||
Just define conditions to allow content scrolling in `floatingPanel(:_:shouldAllowToScroll:in)` delegate method. If the returned value is true, the scroll content scrolls when its scroll position is not at the top of the content.
|
||||
|
||||
```swift
|
||||
class MyViewController: FloatingPanelControllerDelegate {
|
||||
...
|
||||
|
||||
func floatingPanel(
|
||||
_ fpc: FloatingPanelController,
|
||||
shouldAllowToScroll trackingScrollView: UIScrollView,
|
||||
in state: FloatingPanelState
|
||||
) -> Bool {
|
||||
return state == .full || state == .half
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
### 'Show' or 'Show Detail' Segues from `FloatingPanelController`'s content view controller
|
||||
@@ -703,21 +712,6 @@ It's a great way to decouple between a floating panel and the content VC.
|
||||
|
||||
Because `UISearchController` automatically presents itself modally when a user interacts with the search bar, and then it swaps the superview of the search bar to the view managed by itself while it displays. As a result, `FloatingPanelController` can't control the search bar when it's active, as you can see from [the screen shot](https://github.com/SCENEE/FloatingPanel/issues/248#issuecomment-521263831).
|
||||
|
||||
### FloatingPanelSurfaceView's issue on iOS 10
|
||||
|
||||
* On iOS 10, `FloatingPanelSurfaceView.cornerRadius` isn't not automatically masked with the top rounded corners because of `UIVisualEffectView` issue. See https://forums.developer.apple.com/thread/50854.
|
||||
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 App's Auto Layout settings of `UIVisualEffectView` in Main.storyboard.
|
||||
|
||||
## Maintainer
|
||||
|
||||
Shin Yamamoto <shin@scenee.com> | [@scenee](https://twitter.com/scenee)
|
||||
|
||||
@@ -4,7 +4,7 @@ import UIKit
|
||||
|
||||
/// A view that presents a backdrop interface behind a panel.
|
||||
@objc(FloatingPanelBackdropView)
|
||||
public class BackdropView: UIView {
|
||||
open class BackdropView: UIView {
|
||||
|
||||
/// The gesture recognizer for tap gestures to dismiss a panel.
|
||||
///
|
||||
@@ -12,14 +12,14 @@ public class BackdropView: UIView {
|
||||
/// To dismiss a panel by tap gestures on the backdrop, `dismissalTapGestureRecognizer.isEnabled` is set to true.
|
||||
@objc public var dismissalTapGestureRecognizer: UITapGestureRecognizer
|
||||
|
||||
init() {
|
||||
public init() {
|
||||
dismissalTapGestureRecognizer = UITapGestureRecognizer()
|
||||
dismissalTapGestureRecognizer.isEnabled = false
|
||||
super.init(frame: .zero)
|
||||
addGestureRecognizer(dismissalTapGestureRecognizer)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
required public init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,12 +25,16 @@ public protocol FloatingPanelBehavior {
|
||||
@objc optional
|
||||
var momentumProjectionRate: CGFloat { get }
|
||||
|
||||
/// Asks the behavior if a panel should project a momentum of a user interaction to move the proposed position.
|
||||
/// Asks the behavior if a panel should project a momentum of a user interaction to move the
|
||||
/// proposed state.
|
||||
///
|
||||
/// The default implementation of this method returns true. This method is called for a layout to support all positions(tip, half and full).
|
||||
/// Therefore, `proposedState` can only be `FloatingPanelState.tip` or `FloatingPanelState.full`.
|
||||
/// The default implementation of this method returns `false`. This method is called for called
|
||||
/// for all states defined by the current layout object.
|
||||
@objc optional
|
||||
func shouldProjectMomentum(_ fpc: FloatingPanelController, to proposedState: FloatingPanelState) -> Bool
|
||||
func shouldProjectMomentum(
|
||||
_ fpc: FloatingPanelController,
|
||||
to proposedState: FloatingPanelState
|
||||
) -> Bool
|
||||
|
||||
/// Returns the progress to redirect to the previous position.
|
||||
///
|
||||
|
||||
+64
-43
@@ -96,6 +96,35 @@ import os.log
|
||||
@objc(floatingPanel:contentOffsetForPinningScrollView:)
|
||||
optional
|
||||
func floatingPanel(_ fpc: FloatingPanelController, contentOffsetForPinning trackingScrollView: UIScrollView) -> CGPoint
|
||||
|
||||
/// Returns a Boolean value that determines whether the tracking scroll view should
|
||||
/// scroll or not
|
||||
///
|
||||
///
|
||||
/// If you return true, the scroll content scrolls when its scroll position is not
|
||||
/// at the top of the content. If the delegate doesn’t implement this method, its
|
||||
/// content can be scrolled only in the most expanded state.
|
||||
///
|
||||
/// Basically, the decision to scroll is based on the `state` property like the
|
||||
/// following code.
|
||||
/// ```swift
|
||||
/// func floatingPanel(
|
||||
/// _ fpc: FloatingPanelController,
|
||||
/// shouldAllowToScroll scrollView: UIScrollView,
|
||||
/// in state: FloatingPanelState
|
||||
/// ) -> Bool {
|
||||
/// return state == .full || state == .half
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - Attention: It is recommended that this method always returns the most expanded state(i.e.
|
||||
/// .full). If it excludes the state, the panel might do unexpected behaviors.
|
||||
@objc(floatingPanel:shouldAllowToScroll:in:)
|
||||
optional func floatingPanel(
|
||||
_ fpc: FloatingPanelController,
|
||||
shouldAllowToScroll scrollView: UIScrollView,
|
||||
in state: FloatingPanelState
|
||||
) -> Bool
|
||||
}
|
||||
|
||||
///
|
||||
@@ -120,7 +149,7 @@ open class FloatingPanelController: UIViewController {
|
||||
}
|
||||
|
||||
/// The delegate of a panel controller object.
|
||||
@objc
|
||||
@objc
|
||||
public weak var delegate: FloatingPanelControllerDelegate?{
|
||||
didSet{
|
||||
didUpdateDelegate()
|
||||
@@ -129,14 +158,15 @@ open class FloatingPanelController: UIViewController {
|
||||
|
||||
/// Returns the surface view managed by the controller object. It's the same as `self.view`.
|
||||
@objc
|
||||
public var surfaceView: SurfaceView! {
|
||||
public var surfaceView: SurfaceView {
|
||||
return floatingPanel.surfaceView
|
||||
}
|
||||
|
||||
/// Returns the backdrop view managed by the controller object.
|
||||
@objc
|
||||
public var backdropView: BackdropView! {
|
||||
return floatingPanel.backdropView
|
||||
public var backdropView: BackdropView {
|
||||
set { floatingPanel.backdropView = newValue }
|
||||
get { return floatingPanel.backdropView }
|
||||
}
|
||||
|
||||
/// Returns the scroll view that the controller tracks.
|
||||
@@ -201,7 +231,7 @@ open class FloatingPanelController: UIViewController {
|
||||
/// The behavior for determining the adjusted content offsets.
|
||||
///
|
||||
/// This property specifies how the content area of the tracking scroll view is modified using ``adjustedContentInsets``. The default value of this property is FloatingPanelController.ContentInsetAdjustmentBehavior.always.
|
||||
@objc
|
||||
@objc
|
||||
public var contentInsetAdjustmentBehavior: ContentInsetAdjustmentBehavior = .always
|
||||
|
||||
/// A Boolean value that determines whether the removal interaction is enabled.
|
||||
@@ -301,19 +331,19 @@ open class FloatingPanelController: UIViewController {
|
||||
|
||||
open override func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
if #available(iOS 11.0, *) {
|
||||
// Ensure to update the static constraint of a panel after rotating a device in static mode
|
||||
if contentMode == .static {
|
||||
floatingPanel.layoutAdapter.updateStaticConstraint()
|
||||
}
|
||||
} else {
|
||||
// Because {top,bottom}LayoutGuide is managed as a view
|
||||
if floatingPanel.isAttracting == false {
|
||||
self.update(safeAreaInsets: fp_safeAreaInsets)
|
||||
}
|
||||
// Ensure to update the static constraint of a panel after rotating a device in static mode
|
||||
if contentMode == .static {
|
||||
floatingPanel.layoutAdapter.updateStaticConstraint()
|
||||
}
|
||||
}
|
||||
|
||||
open override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
// Need to call this method just after the view appears, as the safe area is not
|
||||
// correctly set before this time, for example, `show(animated:completion:)`.
|
||||
floatingPanel.adjustScrollContentInsetIfNeeded()
|
||||
}
|
||||
|
||||
open override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
|
||||
super.viewWillTransition(to: size, with: coordinator)
|
||||
|
||||
@@ -395,6 +425,7 @@ open class FloatingPanelController: UIViewController {
|
||||
}
|
||||
|
||||
floatingPanel.layoutAdapter.updateStaticConstraint()
|
||||
floatingPanel.adjustScrollContentInsetIfNeeded()
|
||||
|
||||
if let contentOffset = contentOffset {
|
||||
trackingScrollView?.contentOffset = contentOffset
|
||||
@@ -445,28 +476,23 @@ open class FloatingPanelController: UIViewController {
|
||||
// Must apply the current layout here
|
||||
activateLayout(forceLayout: true)
|
||||
|
||||
if #available(iOS 11.0, *) {
|
||||
// Must track the safeAreaInsets of `self.view` to update the layout.
|
||||
// There are 2 reasons.
|
||||
// 1. This or the parent VC doesn't call viewSafeAreaInsetsDidChange() on the bottom
|
||||
// inset's update expectedly.
|
||||
// 2. The safe area top inset can be variable on the large title navigation bar(iOS11+).
|
||||
// That's why it needs the observation to keep `adjustedContentInsets` correct.
|
||||
safeAreaInsetsObservation = self.view.observe(\.safeAreaInsets, options: [.initial, .new, .old]) { [weak self] (_, change) in
|
||||
// Use `self.view.safeAreaInsets` because `change.newValue` can be nil in particular case when
|
||||
// is reported in https://github.com/SCENEE/FloatingPanel/issues/330
|
||||
guard let self = self, change.oldValue != self.view.safeAreaInsets else { return }
|
||||
// Must track the safeAreaInsets of `self.view` to update the layout.
|
||||
// There are 2 reasons.
|
||||
// 1. This or the parent VC doesn't call viewSafeAreaInsetsDidChange() on the bottom
|
||||
// inset's update expectedly.
|
||||
// 2. The safe area top inset can be variable on the large title navigation bar(iOS11+).
|
||||
// That's why it needs the observation to keep `adjustedContentInsets` correct.
|
||||
safeAreaInsetsObservation = self.view.observe(\.safeAreaInsets, options: [.initial, .new, .old]) { [weak self] (_, change) in
|
||||
// Use `self.view.safeAreaInsets` because `change.newValue` can be nil in particular case when
|
||||
// is reported in https://github.com/SCENEE/FloatingPanel/issues/330
|
||||
guard let self = self, change.oldValue != self.view.safeAreaInsets else { return }
|
||||
|
||||
// Sometimes the bounding rectangle of the controlled view becomes invalid when the screen is rotated.
|
||||
// This results in its safeAreaInsets change. In that case, `self.update(safeAreaInsets:)` leads
|
||||
// an unsatisfied constraints error. So this method should not be called with those bounds.
|
||||
guard self.view.bounds.height > 0 && self.view.bounds.width > 0 else { return }
|
||||
// Sometimes the bounding rectangle of the controlled view becomes invalid when the screen is rotated.
|
||||
// This results in its safeAreaInsets change. In that case, `self.update(safeAreaInsets:)` leads
|
||||
// an unsatisfied constraints error. So this method should not be called with those bounds.
|
||||
guard self.view.bounds.height > 0 && self.view.bounds.width > 0 else { return }
|
||||
|
||||
self.update(safeAreaInsets: self.view.safeAreaInsets)
|
||||
}
|
||||
} else {
|
||||
// KVOs for topLayoutGuide & bottomLayoutGuide are not effective.
|
||||
// Instead, update(safeAreaInsets:) is called at `viewDidLayoutSubviews()`
|
||||
self.update(safeAreaInsets: self.view.safeAreaInsets)
|
||||
}
|
||||
|
||||
move(to: floatingPanel.layoutAdapter.initialState,
|
||||
@@ -598,18 +624,12 @@ open class FloatingPanelController: UIViewController {
|
||||
|
||||
switch contentInsetAdjustmentBehavior {
|
||||
case .always:
|
||||
if #available(iOS 11.0, *) {
|
||||
scrollView.contentInsetAdjustmentBehavior = .never
|
||||
} else {
|
||||
children.forEach { (vc) in
|
||||
vc.automaticallyAdjustsScrollViewInsets = false
|
||||
}
|
||||
}
|
||||
scrollView.contentInsetAdjustmentBehavior = .never
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// [Experimental] Allows the panel to move as its tracking scroll view bounces.
|
||||
///
|
||||
/// This method must be called in the delegate method, `UIScrollViewDelegate.scrollViewDidScroll(_:)`,
|
||||
@@ -713,6 +733,7 @@ private var originalDismissImp: IMP?
|
||||
private typealias DismissFunction = @convention(c) (AnyObject, Selector, Bool, (() -> Void)?) -> Void
|
||||
extension FloatingPanelController {
|
||||
private static let dismissSwizzling: Void = {
|
||||
guard originalDismissImp == nil else { return }
|
||||
let aClass: AnyClass! = UIViewController.self //object_getClass(vc)
|
||||
if let originalMethod = class_getInstanceMethod(aClass, #selector(dismiss(animated:completion:))),
|
||||
let swizzledImp = class_getMethodImplementation(aClass, #selector(__swizzled_dismiss(animated:completion:))) {
|
||||
|
||||
+191
-105
@@ -10,7 +10,13 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
private weak var ownerVC: FloatingPanelController?
|
||||
|
||||
let surfaceView: SurfaceView
|
||||
let backdropView: BackdropView
|
||||
var backdropView: BackdropView {
|
||||
didSet {
|
||||
backdropView.dismissalTapGestureRecognizer
|
||||
.addTarget(self, action: #selector(handleBackdrop(tapGesture:)))
|
||||
}
|
||||
}
|
||||
|
||||
let layoutAdapter: LayoutAdapter
|
||||
let behaviorAdapter: BehaviorAdapter
|
||||
|
||||
@@ -24,6 +30,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
scrollBounce = cur.bounces
|
||||
scrollIndictorVisible = cur.showsVerticalScrollIndicator
|
||||
}
|
||||
scrollLocked = false
|
||||
} else {
|
||||
if let pre = oldValue {
|
||||
pre.isDirectionalLockEnabled = false
|
||||
@@ -37,8 +44,8 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
private(set) var state: FloatingPanelState = .hidden {
|
||||
didSet {
|
||||
os_log(msg, log: devLog, type: .debug, "state changed: \(oldValue) -> \(state)")
|
||||
if let vc = ownerVC {
|
||||
vc.delegate?.floatingPanelDidChangeState?(vc)
|
||||
if let fpc = ownerVC {
|
||||
fpc.delegate?.floatingPanelDidChangeState?(fpc)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -68,6 +75,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
private var scrollBounce = false
|
||||
private var scrollIndictorVisible = false
|
||||
private var scrollBounceThreshold: CGFloat = -30.0
|
||||
private var scrollLocked = false
|
||||
|
||||
// MARK: - Interface
|
||||
|
||||
@@ -120,7 +128,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
completion?()
|
||||
return
|
||||
}
|
||||
if state != layoutAdapter.mostExpandedState {
|
||||
if !isScrollable(state: state) {
|
||||
lockScrollView()
|
||||
}
|
||||
tearDownActiveInteraction()
|
||||
@@ -130,7 +138,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
if animated {
|
||||
let updateScrollView: () -> Void = { [weak self] in
|
||||
guard let self = self else { return }
|
||||
if self.state == self.layoutAdapter.mostExpandedState, 0 == self.layoutAdapter.offsetFromMostExpandedAnchor {
|
||||
if self.isScrollable(state: self.state), 0 == self.layoutAdapter.offset(from: self.state) {
|
||||
self.unlockScrollView()
|
||||
} else {
|
||||
self.lockScrollView()
|
||||
@@ -184,7 +192,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
} else {
|
||||
self.state = to
|
||||
self.updateLayout(to: to)
|
||||
if self.state == self.layoutAdapter.mostExpandedState {
|
||||
if isScrollable(state: state) {
|
||||
self.unlockScrollView()
|
||||
} else {
|
||||
self.lockScrollView()
|
||||
@@ -209,6 +217,9 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
contentOffset = scrollView?.contentOffset
|
||||
}
|
||||
|
||||
if layoutAdapter.validStates.contains(state) == false {
|
||||
state = layoutAdapter.initialState
|
||||
}
|
||||
layoutAdapter.updateStaticConstraint()
|
||||
layoutAdapter.activateLayout(for: state, forceLayout: forceLayout)
|
||||
|
||||
@@ -221,11 +232,14 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
if let contentOffset = contentOffset {
|
||||
scrollView?.contentOffset = contentOffset
|
||||
}
|
||||
|
||||
adjustScrollContentInsetIfNeeded()
|
||||
}
|
||||
|
||||
private func updateLayout(to target: FloatingPanelState) {
|
||||
self.layoutAdapter.activateLayout(for: target, forceLayout: true)
|
||||
self.backdropView.alpha = self.getBackdropAlpha(for: target)
|
||||
layoutAdapter.activateLayout(for: target, forceLayout: true)
|
||||
backdropView.alpha = getBackdropAlpha(for: target)
|
||||
adjustScrollContentInsetIfNeeded()
|
||||
}
|
||||
|
||||
private func getBackdropAlpha(for target: FloatingPanelState) -> CGFloat {
|
||||
@@ -294,8 +308,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if #available(iOS 11.0, *),
|
||||
otherGestureRecognizer.name == "_UISheetInteractionBackgroundDismissRecognizer" {
|
||||
if otherGestureRecognizer.name == "_UISheetInteractionBackgroundDismissRecognizer" {
|
||||
// The dismiss gesture of a sheet modal should not begin until the pan gesture fails.
|
||||
return true
|
||||
}
|
||||
@@ -327,7 +340,9 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
if surfaceView.grabberAreaContains(gestureRecognizer.location(in: surfaceView)) {
|
||||
return false
|
||||
}
|
||||
guard state == layoutAdapter.mostExpandedState else { return false }
|
||||
|
||||
guard isScrollable(state: state) else { return false }
|
||||
|
||||
// The condition where offset > 0 must not be included here. Because it will stop recognizing
|
||||
// the panel pan gesture if a user starts scrolling content from an offset greater than 0.
|
||||
return allowScrollPanGesture(of: scrollView) { offset in offset <= scrollBounceThreshold }
|
||||
@@ -350,8 +365,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
is UIRotationGestureRecognizer,
|
||||
is UIScreenEdgePanGestureRecognizer,
|
||||
is UIPinchGestureRecognizer:
|
||||
if #available(iOS 11.0, *),
|
||||
otherGestureRecognizer.name == "_UISheetInteractionBackgroundDismissRecognizer" {
|
||||
if otherGestureRecognizer.name == "_UISheetInteractionBackgroundDismissRecognizer" {
|
||||
// Should begin the pan gesture without waiting the dismiss gesture of a sheet modal.
|
||||
return false
|
||||
}
|
||||
@@ -381,7 +395,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
let velocity = value(of: panGesture.velocity(in: panGesture.view))
|
||||
let location = panGesture.location(in: surfaceView)
|
||||
|
||||
let insideMostExpandedAnchor = 0 > layoutAdapter.offsetFromMostExpandedAnchor
|
||||
let insideMostExpandedAnchor = 0 < layoutAdapter.offsetFromMostExpandedAnchor
|
||||
|
||||
os_log(msg, log: devLog, type: .debug, """
|
||||
scroll gesture(\(state):\(panGesture.state)) -- \
|
||||
@@ -392,21 +406,31 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
"""
|
||||
)
|
||||
|
||||
let offsetDiff = value(of: scrollView.contentOffset - contentOffsetForPinning(of: scrollView))
|
||||
let baseOffset = contentOffsetForPinning(of: scrollView)
|
||||
let offsetDiff = value(of: scrollView.contentOffset - baseOffset)
|
||||
|
||||
if insideMostExpandedAnchor {
|
||||
// Scroll offset pinning
|
||||
if state == layoutAdapter.mostExpandedState {
|
||||
// Prevent scrolling if needed
|
||||
if isScrollable(state: state) {
|
||||
if interactionInProgress {
|
||||
os_log(msg, log: devLog, type: .debug, "settle offset -- \(value(of: initialScrollOffset))")
|
||||
// Return content offset to initial offset to prevent scrolling
|
||||
stopScrolling(at: initialScrollOffset)
|
||||
} else {
|
||||
if surfaceView.grabberAreaContains(location) {
|
||||
if surfaceView.grabberAreaContains(initialLocation) {
|
||||
// Preserve the current content offset in moving from full.
|
||||
stopScrolling(at: initialScrollOffset)
|
||||
}
|
||||
/// When the scroll offset is at the pinned offset and a panel is moved, the content
|
||||
/// must be fixed at the pinned position without scrolling. According to the scroll
|
||||
/// pan gesture behavior, the content might have already scrolled a bit by the time
|
||||
/// this handler is called. Thus `initialScrollOffset` property is used here.
|
||||
if value(of: initialScrollOffset - baseOffset) == 0.0 {
|
||||
stopScrolling(at: initialScrollOffset)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Return content offset to initial offset to prevent scrolling
|
||||
stopScrolling(at: initialScrollOffset)
|
||||
}
|
||||
|
||||
@@ -414,7 +438,9 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
if interactionInProgress {
|
||||
lockScrollView()
|
||||
} else {
|
||||
if state == layoutAdapter.mostExpandedState, self.transitionAnimator == nil {
|
||||
// Put back the scroll indicator and bounce of tracking scroll view
|
||||
// for scrollable states, not most expanded state.
|
||||
if isScrollable(state: state), self.transitionAnimator == nil {
|
||||
switch layoutAdapter.position {
|
||||
case .top, .left:
|
||||
if offsetDiff < 0 && velocity > 0 {
|
||||
@@ -428,6 +454,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Here handles seamless scrolling at the most expanded position
|
||||
if interactionInProgress {
|
||||
// Show a scroll indicator at the top in dragging.
|
||||
switch layoutAdapter.position {
|
||||
@@ -442,14 +469,14 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
return
|
||||
}
|
||||
}
|
||||
if state == layoutAdapter.mostExpandedState {
|
||||
if isScrollable(state: state) {
|
||||
// Adjust a small gap of the scroll offset just after swiping down starts in the grabber area.
|
||||
if surfaceView.grabberAreaContains(location), surfaceView.grabberAreaContains(initialLocation) {
|
||||
stopScrolling(at: initialScrollOffset)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if state == layoutAdapter.mostExpandedState {
|
||||
if isScrollable(state: state) {
|
||||
let allowScroll = allowScrollPanGesture(of: scrollView) { offset in
|
||||
offset <= scrollBounceThreshold || 0 < offset
|
||||
}
|
||||
@@ -480,7 +507,9 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
}
|
||||
case panGestureRecognizer:
|
||||
let translation = panGesture.translation(in: panGestureRecognizer.view!.superview)
|
||||
// The touch velocity in the surface view
|
||||
let velocity = panGesture.velocity(in: panGesture.view)
|
||||
// The touch location in the surface view
|
||||
let location = panGesture.location(in: panGesture.view)
|
||||
|
||||
os_log(msg, log: devLog, type: .debug, """
|
||||
@@ -538,7 +567,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
endAttraction(false)
|
||||
}
|
||||
if let animator = self.transitionAnimator {
|
||||
guard 0 >= layoutAdapter.offsetFromMostExpandedAnchor else { return }
|
||||
guard 0 <= layoutAdapter.offsetFromMostExpandedAnchor else { return }
|
||||
os_log(msg, log: devLog, type: .debug, "a panel animation(interruptible: \(animator.isInterruptible)) interrupted!!!")
|
||||
if animator.isInterruptible {
|
||||
animator.stopAnimation(false)
|
||||
@@ -557,13 +586,18 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
|
||||
private func shouldScrollViewHandleTouch(_ scrollView: UIScrollView?, point: CGPoint, velocity: CGFloat) -> Bool {
|
||||
// When no scrollView, nothing to handle.
|
||||
guard let scrollView = scrollView else { return false }
|
||||
guard let scrollView = scrollView, scrollView.frame.contains(initialLocation) else { return false }
|
||||
|
||||
// For _UISwipeActionPanGestureRecognizer
|
||||
if let scrollGestureRecognizers = scrollView.gestureRecognizers {
|
||||
// Prevents moving a panel on swipe actions using _UISwipeActionPanGestureRecognizer.
|
||||
// [Warning] Do not apply this to WKWebView. Since iOS 17.4, WKWebView has an additional pan
|
||||
// gesture recognizer besides UIScrollViewPanGestureRecognizer. Applying this to WKWebView
|
||||
// will block panel movements because another pan gesture isn't `scrollView.panGestureRecognizer`.
|
||||
if let scrollGestureRecognizers = scrollView.gestureRecognizers,
|
||||
scrollView is UITableView || scrollView is UICollectionView {
|
||||
for gesture in scrollGestureRecognizers {
|
||||
guard gesture.state == .began || gesture.state == .changed
|
||||
else { continue }
|
||||
guard gesture.state == .began || gesture.state == .changed else {
|
||||
continue
|
||||
}
|
||||
|
||||
if gesture != scrollView.panGestureRecognizer {
|
||||
return true
|
||||
@@ -572,27 +606,10 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
}
|
||||
|
||||
guard
|
||||
state == layoutAdapter.mostExpandedState, // When not top most(i.e. .full), don't scroll.
|
||||
interactionInProgress == false, // When interaction already in progress, don't scroll.
|
||||
0 == layoutAdapter.offsetFromMostExpandedAnchor
|
||||
else {
|
||||
return false
|
||||
}
|
||||
|
||||
// When the current point is within grabber area but the initial point is not, do scroll.
|
||||
if surfaceView.grabberAreaContains(point), !surfaceView.grabberAreaContains(initialLocation) {
|
||||
return true
|
||||
}
|
||||
|
||||
// When the initial point is within grabber area and the current point is out of surface, don't scroll.
|
||||
if surfaceView.grabberAreaContains(initialLocation), !surfaceView.frame.contains(point) {
|
||||
return false
|
||||
}
|
||||
|
||||
let scrollViewFrame = scrollView.convert(scrollView.bounds, to: surfaceView)
|
||||
guard
|
||||
scrollViewFrame.contains(initialLocation), // When the initial point not in scrollView, don't scroll.
|
||||
!surfaceView.grabberAreaContains(point) // When point within grabber area, don't scroll.
|
||||
isScrollable(state: state), // When not top most(i.e. .full), don't scroll.
|
||||
interactionInProgress == false, // When interaction already in progress, don't scroll.
|
||||
0 == layoutAdapter.offset(from: state),
|
||||
!surfaceView.grabberAreaContains(initialLocation) // When the initial point is within grabber area, don't scroll
|
||||
else {
|
||||
return false
|
||||
}
|
||||
@@ -605,14 +622,14 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
if offset < 0.0 {
|
||||
return true
|
||||
}
|
||||
if velocity >= 0 {
|
||||
if velocity >= 0, offset > 0.0 {
|
||||
return true
|
||||
}
|
||||
case .bottom, .right:
|
||||
if offset > 0.0 {
|
||||
return true
|
||||
}
|
||||
if velocity <= 0 {
|
||||
if velocity <= 0, offset < 0.0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -634,24 +651,24 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
os_log(msg, log: devLog, type: .debug, "panningBegan -- location = \(value(of: location))")
|
||||
|
||||
guard let scrollView = scrollView else { return }
|
||||
if state == layoutAdapter.mostExpandedState {
|
||||
if surfaceView.grabberAreaContains(location) {
|
||||
initialScrollOffset = scrollView.contentOffset
|
||||
}
|
||||
} else {
|
||||
initialScrollOffset = scrollView.contentOffset
|
||||
}
|
||||
|
||||
initialScrollOffset = scrollView.contentOffset
|
||||
}
|
||||
|
||||
private func panningChange(with translation: CGPoint) {
|
||||
os_log(msg, log: devLog, type: .debug, "panningChange -- translation = \(value(of: translation))")
|
||||
let pre = value(of: layoutAdapter.surfaceLocation)
|
||||
let diff = value(of: translation - initialTranslation)
|
||||
let next = pre + diff
|
||||
|
||||
layoutAdapter.updateInteractiveEdgeConstraint(diff: diff,
|
||||
scrollingContent: shouldScrollingContentInMoving(from: pre, to: next),
|
||||
allowsRubberBanding: behaviorAdapter.allowsRubberBanding(for:))
|
||||
os_log(msg, log: devLog, type: .debug, """
|
||||
panningChange -- translation = \(value(of: translation)), diff = \(diff), pre = \(pre), next = \(next)
|
||||
""")
|
||||
|
||||
layoutAdapter.updateInteractiveEdgeConstraint(
|
||||
diff: diff,
|
||||
scrollingContent: shouldScrollingContentInMoving(from: pre, to: next),
|
||||
allowsRubberBanding: behaviorAdapter.allowsRubberBanding(for:)
|
||||
)
|
||||
|
||||
let cur = value(of: layoutAdapter.surfaceLocation)
|
||||
|
||||
@@ -664,31 +681,36 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
private func shouldScrollingContentInMoving(from pre: CGFloat, to next: CGFloat) -> Bool {
|
||||
/// Determines if the content should scroll while the surface is moving from `cur` to `target`.
|
||||
///
|
||||
/// - Note: `cur` argument starts from an anchor location of surface view in a state. For example,
|
||||
/// it starts from zero if the state is full whose FloatingPanelLayoutAnchor.absoluteInset is zero
|
||||
/// and there is no additional safe area insets like a navigation bar. Therefore, `cur` argument
|
||||
/// can be minus if the absoluteInset is minus with such a condition.
|
||||
private func shouldScrollingContentInMoving(from cur: CGFloat, to target: CGFloat) -> Bool {
|
||||
// Don't allow scrolling if the initial panning location is in the grabber area.
|
||||
if surfaceView.grabberAreaContains(initialLocation) {
|
||||
return false
|
||||
}
|
||||
if let scrollView = scrollView, scrollView.panGestureRecognizer.state == .changed {
|
||||
if let sv = scrollView, sv.panGestureRecognizer.state == .changed {
|
||||
let (contentSize, bounds, alwaysBounceHorizontal, alwaysBounceVertical)
|
||||
= (sv.contentSize, sv.bounds, sv.alwaysBounceHorizontal, sv.alwaysBounceVertical)
|
||||
|
||||
switch layoutAdapter.position {
|
||||
case .top:
|
||||
if pre > .zero, pre < next,
|
||||
scrollView.contentSize.height > scrollView.bounds.height || scrollView.alwaysBounceVertical {
|
||||
if cur < target, contentSize.height > bounds.height || alwaysBounceVertical {
|
||||
return true
|
||||
}
|
||||
case .left:
|
||||
if pre > .zero, pre < next,
|
||||
scrollView.contentSize.width > scrollView.bounds.width || scrollView.alwaysBounceHorizontal {
|
||||
if cur < target, contentSize.width > bounds.width || alwaysBounceHorizontal {
|
||||
return true
|
||||
}
|
||||
case .bottom:
|
||||
if pre > .zero, pre > next,
|
||||
scrollView.contentSize.height > scrollView.bounds.height || scrollView.alwaysBounceVertical {
|
||||
if cur > target, contentSize.height > bounds.height || alwaysBounceVertical {
|
||||
return true
|
||||
}
|
||||
case .right:
|
||||
if pre > .zero, pre > next,
|
||||
scrollView.contentSize.width > scrollView.bounds.width || scrollView.alwaysBounceHorizontal {
|
||||
if cur > target, contentSize.width > bounds.width || alwaysBounceHorizontal {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -729,14 +751,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
}
|
||||
|
||||
guard shouldAttract(to: target) else {
|
||||
self.state = target
|
||||
self.updateLayout(to: target)
|
||||
self.unlockScrollView()
|
||||
// The `floatingPanelDidEndDragging(_:willAttract:)` must be called after the state property changes.
|
||||
// This allows library users to get the correct state in the delegate method.
|
||||
if let vc = ownerVC {
|
||||
vc.delegate?.floatingPanelDidEndDragging?(vc, willAttract: false)
|
||||
}
|
||||
self.endWithoutAttraction(target)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -777,13 +792,10 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
var offset: CGPoint = .zero
|
||||
|
||||
initialSurfaceLocation = layoutAdapter.surfaceLocation
|
||||
if state == layoutAdapter.mostExpandedState, let scrollView = scrollView {
|
||||
let scrollFrame = scrollView.convert(scrollView.bounds, to: nil)
|
||||
let touchStartingPoint = surfaceView.convert(initialLocation, to: nil)
|
||||
|
||||
ifLabel: if surfaceView.grabberAreaContains(location) {
|
||||
if isScrollable(state: state), let scrollView = scrollView {
|
||||
ifLabel: if surfaceView.grabberAreaContains(initialLocation) {
|
||||
initialScrollOffset = scrollView.contentOffset
|
||||
} else if scrollFrame.contains(touchStartingPoint) {
|
||||
} else if scrollView.frame.contains(initialLocation) {
|
||||
let pinningOffset = contentOffsetForPinning(of: scrollView)
|
||||
|
||||
// This code block handles the scenario where there's a navigation bar or toolbar
|
||||
@@ -861,7 +873,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
interactionInProgress = false
|
||||
|
||||
// Prevent to keep a scroll view indicator visible at the half/tip position
|
||||
if state != layoutAdapter.mostExpandedState {
|
||||
if !isScrollable(state: state) {
|
||||
lockScrollView()
|
||||
}
|
||||
|
||||
@@ -882,6 +894,17 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
return true
|
||||
}
|
||||
|
||||
func endWithoutAttraction(_ target: FloatingPanelState) {
|
||||
self.state = target
|
||||
self.updateLayout(to: target)
|
||||
self.unlockScrollView()
|
||||
// The `floatingPanelDidEndDragging(_:willAttract:)` must be called after the state property changes.
|
||||
// This allows library users to get the correct state in the delegate method.
|
||||
if let vc = ownerVC {
|
||||
vc.delegate?.floatingPanelDidEndDragging?(vc, willAttract: false)
|
||||
}
|
||||
}
|
||||
|
||||
private func startAttraction(to state: FloatingPanelState, with velocity: CGPoint, completion: @escaping (() -> Void)) {
|
||||
os_log(msg, log: devLog, type: .debug, "startAnimation to \(state) -- velocity = \(value(of: velocity))")
|
||||
guard let vc = ownerVC else { return }
|
||||
@@ -947,11 +970,11 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
os_log(msg, log: devLog, type: .debug, """
|
||||
finishAnimation -- state = \(state) \
|
||||
surface location = \(layoutAdapter.surfaceLocation) \
|
||||
edge most position = \(layoutAdapter.surfaceLocation(for: layoutAdapter.mostExpandedState))
|
||||
offset from state position = \(layoutAdapter.offset(from: state))
|
||||
""")
|
||||
|
||||
if tryUnlockScroll {
|
||||
if (state == layoutAdapter.mostExpandedState && 0 == layoutAdapter.offsetFromMostExpandedAnchor)
|
||||
if (isScrollable(state: state) && 0 == layoutAdapter.offset(from: state))
|
||||
|| shouldLooselyLockScrollView {
|
||||
unlockScrollView()
|
||||
}
|
||||
@@ -1042,7 +1065,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
return
|
||||
}
|
||||
let contentOffset = scrollView.contentOffset.y
|
||||
guard contentOffset < 0, layoutAdapter.position == .bottom, state == layoutAdapter.mostExpandedState else {
|
||||
guard contentOffset < 0, layoutAdapter.position == .bottom, isScrollable(state: state) else {
|
||||
if surfaceView.transform != .identity {
|
||||
surfaceView.transform = .identity
|
||||
scrollView.transform = .identity
|
||||
@@ -1056,14 +1079,11 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
private func lockScrollView(strict: Bool = false) {
|
||||
guard let scrollView = scrollView else { return }
|
||||
|
||||
if scrollView.isLocked {
|
||||
os_log(msg, log: devLog, type: .debug, "Already scroll locked.")
|
||||
if scrollLocked {
|
||||
os_log(msg, log: devLog, type: .debug, "Already scroll locked")
|
||||
return
|
||||
}
|
||||
os_log(msg, log: devLog, type: .debug, "lock scroll view")
|
||||
|
||||
scrollIndictorVisible = scrollView.showsVerticalScrollIndicator
|
||||
|
||||
scrollBounce = scrollView.bounces
|
||||
if !strict, shouldLooselyLockScrollView {
|
||||
// Don't change its `bounces` property. If it's changed, it will cause its scroll content offset jump at
|
||||
// the most expanded anchor position while seamlessly scrolling content. This problem only occurs where its
|
||||
@@ -1071,26 +1091,49 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
// The reason why is because `bounces` prop change leads to the "content frame" change on `.fitToBounds`.
|
||||
// See also https://github.com/scenee/FloatingPanel/issues/524.
|
||||
} else {
|
||||
scrollBounce = scrollView.bounces
|
||||
scrollView.bounces = false
|
||||
}
|
||||
os_log(msg, log: devLog, type: .debug, "lock scroll view")
|
||||
|
||||
|
||||
scrollLocked = true
|
||||
scrollView.isDirectionalLockEnabled = true
|
||||
scrollView.showsVerticalScrollIndicator = false
|
||||
switch layoutAdapter.position {
|
||||
case .top, .bottom:
|
||||
scrollIndictorVisible = scrollView.showsVerticalScrollIndicator
|
||||
scrollView.showsVerticalScrollIndicator = false
|
||||
case .left, .right:
|
||||
scrollIndictorVisible = scrollView.showsHorizontalScrollIndicator
|
||||
scrollView.showsHorizontalScrollIndicator = false
|
||||
}
|
||||
}
|
||||
|
||||
private func unlockScrollView() {
|
||||
guard let scrollView = scrollView, scrollView.isLocked else { return }
|
||||
guard let scrollView = scrollView else { return }
|
||||
if !scrollLocked {
|
||||
os_log(msg, log: devLog, type: .debug, "Already scroll unlocked.")
|
||||
return
|
||||
}
|
||||
os_log(msg, log: devLog, type: .debug, "unlock scroll view")
|
||||
|
||||
scrollLocked = false
|
||||
scrollView.bounces = scrollBounce
|
||||
scrollView.isDirectionalLockEnabled = false
|
||||
scrollView.showsVerticalScrollIndicator = scrollIndictorVisible
|
||||
switch layoutAdapter.position {
|
||||
case .top, .bottom:
|
||||
scrollView.showsVerticalScrollIndicator = scrollIndictorVisible
|
||||
case .left, .right:
|
||||
scrollView.showsHorizontalScrollIndicator = scrollIndictorVisible
|
||||
}
|
||||
}
|
||||
|
||||
private var shouldLooselyLockScrollView: Bool {
|
||||
if surfaceView.frame == .zero {
|
||||
return false
|
||||
}
|
||||
var isSmallScrollContentAndFitToBoundsMode: Bool {
|
||||
if ownerVC?.contentMode == .fitToBounds, let scrollView = scrollView,
|
||||
value(of: scrollView.contentSize) < value(of: scrollView.bounds.size) - min(layoutAdapter.offsetFromMostExpandedAnchor, 0) {
|
||||
value(of: scrollView.contentSize) < value(of: scrollView.bounds.size) + max(layoutAdapter.offsetFromMostExpandedAnchor, 0) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@@ -1116,9 +1159,9 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
case .left:
|
||||
return CGPoint(x: scrollView.fp_contentOffsetMax.x, y: 0.0)
|
||||
case .bottom:
|
||||
return CGPoint(x: 0.0, y: 0.0 - scrollView.fp_contentInset.top)
|
||||
return CGPoint(x: 0.0, y: 0.0 - scrollView.adjustedContentInset.top)
|
||||
case .right:
|
||||
return CGPoint(x: 0.0 - scrollView.fp_contentInset.left, y: 0.0)
|
||||
return CGPoint(x: 0.0 - scrollView.adjustedContentInset.left, y: 0.0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1132,10 +1175,55 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
}
|
||||
return condition(offset)
|
||||
}
|
||||
|
||||
func isScrollable(state: FloatingPanelState) -> Bool {
|
||||
guard let scrollView = scrollView else { return false }
|
||||
if let fpc = ownerVC,
|
||||
let scrollable = fpc.delegate?.floatingPanel?(fpc, shouldAllowToScroll: scrollView, in: state)
|
||||
{
|
||||
return scrollable
|
||||
}
|
||||
return state == layoutAdapter.mostExpandedState
|
||||
}
|
||||
|
||||
/// Adjust content inset of the tracking scroll view if the controller's
|
||||
/// `contentInsetAdjustmentBehavior` is `.always` and its `contentMode` is `.static`.
|
||||
/// if its content is scrollable, the content might not be fully visible on `.half`
|
||||
/// state, for example. Therefore the content inset needs to adjust to display the
|
||||
/// full content.
|
||||
func adjustScrollContentInsetIfNeeded() {
|
||||
guard
|
||||
let fpc = ownerVC,
|
||||
let scrollView = scrollView,
|
||||
fpc.contentInsetAdjustmentBehavior == .always
|
||||
else { return }
|
||||
|
||||
switch fpc.contentMode {
|
||||
case .static:
|
||||
var inset = scrollView.safeAreaInsets
|
||||
let offset = layoutAdapter.offsetFromMostExpandedAnchor
|
||||
if offset > 0 {
|
||||
switch layoutAdapter.position {
|
||||
case .top:
|
||||
inset.top = offset + scrollView.safeAreaInsets.top
|
||||
case .bottom:
|
||||
inset.bottom = offset + scrollView.safeAreaInsets.bottom
|
||||
case .left:
|
||||
inset.left = offset + scrollView.safeAreaInsets.left
|
||||
case .right:
|
||||
inset.left = offset + scrollView.safeAreaInsets.right
|
||||
}
|
||||
}
|
||||
scrollView.contentInset = inset
|
||||
case .fitToBounds:
|
||||
scrollView.contentInset = scrollView.safeAreaInsets
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A gesture recognizer that looks for panning (dragging) gestures in a panel.
|
||||
public final class FloatingPanelPanGestureRecognizer: UIPanGestureRecognizer {
|
||||
/// The gesture starting location in the surface view which it is attached to.
|
||||
fileprivate var initialLocation: CGPoint = .zero
|
||||
private weak var floatingPanel: Core! // Core has this gesture recognizer as non-optional
|
||||
fileprivate func set(floatingPanel: Core) {
|
||||
@@ -1144,9 +1232,7 @@ public final class FloatingPanelPanGestureRecognizer: UIPanGestureRecognizer {
|
||||
|
||||
init() {
|
||||
super.init(target: nil, action: nil)
|
||||
if #available(iOS 11.0, *) {
|
||||
name = "FloatingPanelPanGestureRecognizer"
|
||||
}
|
||||
name = "FloatingPanelPanGestureRecognizer"
|
||||
}
|
||||
|
||||
public override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
|
||||
+10
-67
@@ -7,7 +7,9 @@ import UIKit
|
||||
extension CGFloat {
|
||||
/// Returns this value rounded to an logical pixel value by a display scale
|
||||
func rounded(by displayScale: CGFloat) -> CGFloat {
|
||||
return (self * displayScale).rounded(.toNearestOrAwayFromZero) / displayScale
|
||||
let p = CGFloat(1.0e9)
|
||||
let v = (self * p).rounded(.towardZero) / p
|
||||
return (v * displayScale).rounded(.toNearestOrAwayFromZero) / displayScale
|
||||
}
|
||||
func isEqual(to: CGFloat, on displayScale: CGFloat) -> Bool {
|
||||
return rounded(by: displayScale) == to.rounded(by: displayScale)
|
||||
@@ -45,65 +47,16 @@ protocol LayoutGuideProvider {
|
||||
extension UILayoutGuide: LayoutGuideProvider {}
|
||||
extension UIView: LayoutGuideProvider {}
|
||||
|
||||
private class CustomLayoutGuide: LayoutGuideProvider {
|
||||
let topAnchor: NSLayoutYAxisAnchor
|
||||
let leftAnchor: NSLayoutXAxisAnchor
|
||||
let bottomAnchor: NSLayoutYAxisAnchor
|
||||
let rightAnchor: NSLayoutXAxisAnchor
|
||||
let widthAnchor: NSLayoutDimension
|
||||
let heightAnchor: NSLayoutDimension
|
||||
init(topAnchor: NSLayoutYAxisAnchor,
|
||||
leftAnchor: NSLayoutXAxisAnchor,
|
||||
bottomAnchor: NSLayoutYAxisAnchor,
|
||||
rightAnchor: NSLayoutXAxisAnchor,
|
||||
widthAnchor: NSLayoutDimension,
|
||||
heightAnchor: NSLayoutDimension) {
|
||||
self.topAnchor = topAnchor
|
||||
self.leftAnchor = leftAnchor
|
||||
self.bottomAnchor = bottomAnchor
|
||||
self.rightAnchor = rightAnchor
|
||||
self.widthAnchor = widthAnchor
|
||||
self.heightAnchor = heightAnchor
|
||||
}
|
||||
}
|
||||
|
||||
extension UIViewController {
|
||||
/// The proxy property to be used in `LayoutAdapter`
|
||||
///
|
||||
/// This property is to allow the safe area inset to change in unit testing
|
||||
@objc var fp_safeAreaInsets: UIEdgeInsets {
|
||||
if #available(iOS 11.0, *) {
|
||||
return view.safeAreaInsets
|
||||
} else {
|
||||
return UIEdgeInsets(top: topLayoutGuide.length,
|
||||
left: 0.0,
|
||||
bottom: bottomLayoutGuide.length,
|
||||
right: 0.0)
|
||||
}
|
||||
}
|
||||
|
||||
var fp_safeAreaLayoutGuide: LayoutGuideProvider {
|
||||
if #available(iOS 11.0, *) {
|
||||
return view!.safeAreaLayoutGuide
|
||||
} else {
|
||||
return CustomLayoutGuide(topAnchor: topLayoutGuide.bottomAnchor,
|
||||
leftAnchor: view.leftAnchor,
|
||||
bottomAnchor: bottomLayoutGuide.topAnchor,
|
||||
rightAnchor: view.rightAnchor,
|
||||
widthAnchor: view.widthAnchor,
|
||||
heightAnchor: topLayoutGuide.bottomAnchor.anchorWithOffset(to: bottomLayoutGuide.topAnchor))
|
||||
}
|
||||
return view.safeAreaInsets
|
||||
}
|
||||
}
|
||||
|
||||
// The reason why UIView has no extensions of safe area insets and top/bottom guides
|
||||
// is for iOS10 compatibility.
|
||||
extension UIView {
|
||||
var fp_safeAreaLayoutGuide: LayoutGuideProvider {
|
||||
if #available(iOS 11.0, *) {
|
||||
return safeAreaLayoutGuide
|
||||
} else {
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
var presentationFrame: CGRect {
|
||||
return layer.presentation()?.frame ?? frame
|
||||
}
|
||||
@@ -139,7 +92,7 @@ extension UIView {
|
||||
}
|
||||
}
|
||||
|
||||
#if __FP_LOG
|
||||
#if FP_LOG
|
||||
extension UIGestureRecognizer.State: CustomDebugStringConvertible {
|
||||
public var debugDescription: String {
|
||||
switch self {
|
||||
@@ -156,19 +109,9 @@ extension UIGestureRecognizer.State: CustomDebugStringConvertible {
|
||||
#endif
|
||||
|
||||
extension UIScrollView {
|
||||
var isLocked: Bool {
|
||||
return !showsVerticalScrollIndicator && !bounces && isDirectionalLockEnabled
|
||||
}
|
||||
var fp_contentInset: UIEdgeInsets {
|
||||
if #available(iOS 11.0, *) {
|
||||
return adjustedContentInset
|
||||
} else {
|
||||
return contentInset
|
||||
}
|
||||
}
|
||||
var fp_contentOffsetMax: CGPoint {
|
||||
return CGPoint(x: max((contentSize.width + fp_contentInset.right) - bounds.width, 0.0),
|
||||
y: max((contentSize.height + fp_contentInset.bottom) - bounds.height, 0.0))
|
||||
return CGPoint(x: max((contentSize.width + adjustedContentInset.right) - bounds.width, 0.0),
|
||||
y: max((contentSize.height + adjustedContentInset.bottom) - bounds.height, 0.0))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2.7.0</string>
|
||||
<string>2.8.6</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
</dict>
|
||||
|
||||
+20
-15
@@ -45,8 +45,8 @@ open class FloatingPanelBottomLayout: NSObject, FloatingPanelLayout {
|
||||
|
||||
open func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint] {
|
||||
return [
|
||||
surfaceView.leftAnchor.constraint(equalTo: view.fp_safeAreaLayoutGuide.leftAnchor, constant: 0.0),
|
||||
surfaceView.rightAnchor.constraint(equalTo: view.fp_safeAreaLayoutGuide.rightAnchor, constant: 0.0),
|
||||
surfaceView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 0.0),
|
||||
surfaceView.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor, constant: 0.0),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -266,12 +266,23 @@ class LayoutAdapter {
|
||||
}
|
||||
|
||||
var offsetFromMostExpandedAnchor: CGFloat {
|
||||
return offset(from: mostExpandedState)
|
||||
}
|
||||
|
||||
/// The distance from the given state position to the current surface location.
|
||||
///
|
||||
/// If the returned value is positive, it indicates that the surface is moving from
|
||||
/// the given state position to closer to the `hidden` state position. In other
|
||||
/// words, the surface is within the given state position. Otherwise, it indicates
|
||||
/// that the surface is outside this position and is moving away from the `hidden`
|
||||
/// state position.
|
||||
func offset(from state: FloatingPanelState) -> CGFloat {
|
||||
let offset: CGFloat
|
||||
switch position {
|
||||
case .top, .left:
|
||||
offset = edgePosition(surfaceView.presentationFrame) - position(for: mostExpandedState)
|
||||
offset = position(for: state) - edgePosition(surfaceView.frame)
|
||||
case .bottom, .right:
|
||||
offset = position(for: mostExpandedState) - edgePosition(surfaceView.presentationFrame)
|
||||
offset = edgePosition(surfaceView.frame) - position(for: state)
|
||||
}
|
||||
return offset.rounded(by: surfaceView.fp_displayScale)
|
||||
}
|
||||
@@ -424,13 +435,13 @@ class LayoutAdapter {
|
||||
switch position {
|
||||
case .top, .bottom:
|
||||
surfaceConstraints = [
|
||||
surfaceView.leftAnchor.constraint(equalTo: vc.fp_safeAreaLayoutGuide.leftAnchor, constant: 0.0),
|
||||
surfaceView.rightAnchor.constraint(equalTo: vc.fp_safeAreaLayoutGuide.rightAnchor, constant: 0.0),
|
||||
surfaceView.leftAnchor.constraint(equalTo: vc.view.safeAreaLayoutGuide.leftAnchor, constant: 0.0),
|
||||
surfaceView.rightAnchor.constraint(equalTo: vc.view.safeAreaLayoutGuide.rightAnchor, constant: 0.0),
|
||||
]
|
||||
case .left, .right:
|
||||
surfaceConstraints = [
|
||||
surfaceView.topAnchor.constraint(equalTo: vc.fp_safeAreaLayoutGuide.topAnchor, constant: 0.0),
|
||||
surfaceView.bottomAnchor.constraint(equalTo: vc.fp_safeAreaLayoutGuide.bottomAnchor, constant: 0.0),
|
||||
surfaceView.topAnchor.constraint(equalTo: vc.view.safeAreaLayoutGuide.topAnchor, constant: 0.0),
|
||||
surfaceView.bottomAnchor.constraint(equalTo: vc.view.safeAreaLayoutGuide.bottomAnchor, constant: 0.0),
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -538,7 +549,7 @@ class LayoutAdapter {
|
||||
let layoutGuideProvider: LayoutGuideProvider
|
||||
switch anchor.referenceGuide {
|
||||
case .safeArea:
|
||||
layoutGuideProvider = vc.fp_safeAreaLayoutGuide
|
||||
layoutGuideProvider = vc.view.safeAreaLayoutGuide
|
||||
case .superview:
|
||||
layoutGuideProvider = vc.view
|
||||
}
|
||||
@@ -771,12 +782,6 @@ class LayoutAdapter {
|
||||
NSLayoutConstraint.activate(constraint: self.fitToBoundsConstraint)
|
||||
}
|
||||
|
||||
var state = state
|
||||
|
||||
if validStates.contains(state) == false {
|
||||
state = layout.initialState
|
||||
}
|
||||
|
||||
// Recalculate the intrinsic size of a content view. This is because
|
||||
// UIView.systemLayoutSizeFitting() returns a different size between an
|
||||
// on-screen and off-screen view which includes
|
||||
|
||||
@@ -17,7 +17,7 @@ import UIKit
|
||||
/// positioning.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - absoluteOffset: An absolute offset to attach the panel from the edge.
|
||||
/// - absoluteInset: An absolute distance to attach the panel from the specified edge.
|
||||
/// - edge: Specify the edge of ``FloatingPanelController``'s view. This is the staring point of the offset.
|
||||
/// - referenceGuide: The rectangular area to lay out the content. If it's set to `.safeArea`, the panel content lays out inside the safe area of its ``FloatingPanelController``'s view.
|
||||
@objc public init(absoluteInset: CGFloat, edge: FloatingPanelReferenceEdge, referenceGuide: FloatingPanelLayoutReferenceGuide) {
|
||||
@@ -34,7 +34,7 @@ import UIKit
|
||||
/// 1.0 represents a distance to the opposite edge.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - fractionalOffset: A fractional value of the size of ``FloatingPanelController``'s view to attach the panel from the edge.
|
||||
/// - fractionalInset: A fractional value of the size of ``FloatingPanelController``'s view to attach the panel from the specified edge.
|
||||
/// - edge: Specify the edge of ``FloatingPanelController``'s view. This is the staring point of the offset.
|
||||
/// - referenceGuide: The rectangular area to lay out the content. If it's set to `.safeArea`, the panel content lays out inside the safe area of its ``FloatingPanelController``'s view.
|
||||
@objc public init(fractionalInset: CGFloat, edge: FloatingPanelReferenceEdge, referenceGuide: FloatingPanelLayoutReferenceGuide) {
|
||||
@@ -115,8 +115,8 @@ public extension FloatingPanelLayoutAnchor {
|
||||
/// - Parameters:
|
||||
/// - absoluteOffset: An absolute offset from the content size in the main dimension(i.e. y axis for a bottom panel) to attach the panel.
|
||||
/// - referenceGuide: The rectangular area to lay out the content. If it's set to `.safeArea`, the panel content lays out inside the safe area of its ``FloatingPanelController``'s view.
|
||||
@objc public init(absoluteOffset offset: CGFloat, referenceGuide: FloatingPanelLayoutReferenceGuide = .safeArea) {
|
||||
self.offset = offset
|
||||
@objc public init(absoluteOffset: CGFloat, referenceGuide: FloatingPanelLayoutReferenceGuide = .safeArea) {
|
||||
self.offset = absoluteOffset
|
||||
self.referenceGuide = referenceGuide
|
||||
self.isAbsolute = true
|
||||
}
|
||||
@@ -129,8 +129,8 @@ public extension FloatingPanelLayoutAnchor {
|
||||
/// - Parameters:
|
||||
/// - fractionalOffset: A fractional offset of the content size in the main dimension(i.e. y axis for a bottom panel) to attach the panel.
|
||||
/// - referenceGuide: The rectangular area to lay out the content. If it's set to `.safeArea`, the panel content lays out inside the safe area of its ``FloatingPanelController``'s view.
|
||||
@objc public init(fractionalOffset offset: CGFloat, referenceGuide: FloatingPanelLayoutReferenceGuide = .safeArea) {
|
||||
self.offset = offset
|
||||
@objc public init(fractionalOffset: CGFloat, referenceGuide: FloatingPanelLayoutReferenceGuide = .safeArea) {
|
||||
self.offset = fractionalOffset
|
||||
self.referenceGuide = referenceGuide
|
||||
self.isAbsolute = false
|
||||
}
|
||||
@@ -177,12 +177,12 @@ public extension FloatingPanelIntrinsicLayoutAnchor {
|
||||
///
|
||||
/// - Warning: If ``contentBoundingGuide`` is set to none, the panel may expand out of the screen size, depending on the intrinsic size of its content.
|
||||
@objc public init(
|
||||
absoluteOffset offset: CGFloat,
|
||||
absoluteOffset: CGFloat,
|
||||
contentLayout: UILayoutGuide,
|
||||
referenceGuide: FloatingPanelLayoutReferenceGuide,
|
||||
contentBoundingGuide: FloatingPanelLayoutContentBoundingGuide = .none
|
||||
) {
|
||||
self.offset = offset
|
||||
self.offset = absoluteOffset
|
||||
self.contentLayoutGuide = contentLayout
|
||||
self.referenceGuide = referenceGuide
|
||||
self.contentBoundingGuide = contentBoundingGuide
|
||||
@@ -204,12 +204,12 @@ public extension FloatingPanelIntrinsicLayoutAnchor {
|
||||
///
|
||||
/// - Warning: If ``contentBoundingGuide`` is set to none, the panel may expand out of the screen size, depending on the intrinsic size of its content.
|
||||
@objc public init(
|
||||
fractionalOffset offset: CGFloat,
|
||||
fractionalOffset: CGFloat,
|
||||
contentLayout: UILayoutGuide,
|
||||
referenceGuide: FloatingPanelLayoutReferenceGuide,
|
||||
contentBoundingGuide: FloatingPanelLayoutContentBoundingGuide = .none
|
||||
) {
|
||||
self.offset = offset
|
||||
self.offset = fractionalOffset
|
||||
self.contentLayoutGuide = contentLayout
|
||||
self.referenceGuide = referenceGuide
|
||||
self.contentBoundingGuide = contentBoundingGuide
|
||||
|
||||
@@ -37,7 +37,7 @@ extension FloatingPanelLayoutReferenceGuide {
|
||||
func layoutGuide(vc: UIViewController) -> LayoutGuideProvider {
|
||||
switch self {
|
||||
case .safeArea:
|
||||
return vc.fp_safeAreaLayoutGuide
|
||||
return vc.view.safeAreaLayoutGuide
|
||||
case .superview:
|
||||
return vc.view
|
||||
}
|
||||
@@ -57,7 +57,7 @@ extension FloatingPanelLayoutContentBoundingGuide {
|
||||
case .superview:
|
||||
return fpc.view
|
||||
case .safeArea:
|
||||
return fpc.fp_safeAreaLayoutGuide
|
||||
return fpc.view.safeAreaLayoutGuide
|
||||
case .none:
|
||||
return nil
|
||||
}
|
||||
|
||||
+39
-26
@@ -56,11 +56,11 @@ public class SurfaceAppearance: NSObject {
|
||||
/// Defaults to `.circular`.
|
||||
@available(iOS 13.0, *)
|
||||
public var cornerCurve: CALayerCornerCurve {
|
||||
get { _cornerCurve ?? .circular }
|
||||
get { _cornerCurve as? CALayerCornerCurve ?? .circular }
|
||||
set { _cornerCurve = newValue }
|
||||
}
|
||||
|
||||
private var _cornerCurve: CALayerCornerCurve?
|
||||
private var _cornerCurve: Any?
|
||||
|
||||
/// An array of shadows used to create drop shadows underneath a surface view.
|
||||
public var shadows: [Shadow] = [Shadow()]
|
||||
@@ -383,29 +383,24 @@ public class SurfaceView: UIView {
|
||||
}
|
||||
containerView.layer.masksToBounds = true
|
||||
if position.inset(containerMargins) != 0 {
|
||||
if #available(iOS 11, *) {
|
||||
containerView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner,
|
||||
.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
|
||||
}
|
||||
containerView.layer.maskedCorners = [
|
||||
.layerMinXMinYCorner, .layerMaxXMinYCorner,
|
||||
.layerMinXMaxYCorner, .layerMaxXMaxYCorner
|
||||
]
|
||||
return
|
||||
}
|
||||
if #available(iOS 11, *) {
|
||||
// Don't use `contentView.clipToBounds` because it prevents content view from expanding the height of a subview of it
|
||||
// for the bottom overflow like Auto Layout settings of UIVisualEffectView in Main.storyboard of Example/Maps.
|
||||
// Because the bottom of contentView must be fit to the bottom of a screen to work the `safeLayoutGuide` of a content VC.
|
||||
switch position {
|
||||
case .top:
|
||||
containerView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
|
||||
case .left:
|
||||
containerView.layer.maskedCorners = [.layerMaxXMinYCorner, .layerMaxXMaxYCorner]
|
||||
case .bottom:
|
||||
containerView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
|
||||
case .right:
|
||||
containerView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMinXMaxYCorner]
|
||||
}
|
||||
} else {
|
||||
// Can't use `containerView.layer.mask` because of a UIVisualEffectView issue in iOS 10, https://forums.developer.apple.com/thread/50854
|
||||
// Instead, a user should display rounding corners appropriately.
|
||||
// Don't use `contentView.clipToBounds` because it prevents content view from expanding the height of a subview of it
|
||||
// for the bottom overflow like Auto Layout settings of UIVisualEffectView in Main.storyboard of Example/Maps.
|
||||
// Because the bottom of contentView must be fit to the bottom of a screen to work the `safeLayoutGuide` of a content VC.
|
||||
switch position {
|
||||
case .top:
|
||||
containerView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
|
||||
case .left:
|
||||
containerView.layer.maskedCorners = [.layerMaxXMinYCorner, .layerMaxXMaxYCorner]
|
||||
case .bottom:
|
||||
containerView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
|
||||
case .right:
|
||||
containerView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMinXMaxYCorner]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -423,12 +418,30 @@ public class SurfaceView: UIView {
|
||||
let leftConstraint = contentView.leftAnchor.constraint(equalTo: leftAnchor, constant: containerMargins.left + contentPadding.left)
|
||||
let rightConstraint = rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: containerMargins.right + contentPadding.right)
|
||||
let bottomConstraint = bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: containerMargins.bottom + contentPadding.bottom)
|
||||
NSLayoutConstraint.activate([
|
||||
|
||||
var constraints = [
|
||||
topConstraint,
|
||||
leftConstraint,
|
||||
rightConstraint,
|
||||
bottomConstraint,
|
||||
].map {
|
||||
bottomConstraint
|
||||
]
|
||||
|
||||
// This constraint is for UICollectionView using UICollectionViewCompositionalLayout.
|
||||
// It's seemingly obvious, but the UICollectionView doesn't work without setting it. (#628)
|
||||
switch position {
|
||||
case .top, .bottom:
|
||||
constraints += [
|
||||
heightAnchor.constraint(greaterThanOrEqualToConstant: 1.0)
|
||||
]
|
||||
case .left, .right:
|
||||
constraints += [
|
||||
widthAnchor.constraint(greaterThanOrEqualToConstant: 1.0)
|
||||
]
|
||||
}
|
||||
|
||||
NSLayoutConstraint.activate(
|
||||
constraints
|
||||
.map {
|
||||
switch mode {
|
||||
case .static:
|
||||
$0.priority = .required
|
||||
|
||||
@@ -84,7 +84,7 @@ class ModalPresentTransition: NSObject, UIViewControllerAnimatedTransitioning {
|
||||
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
|
||||
guard
|
||||
let fpc = transitionContext?.viewController(forKey: .to) as? FloatingPanelController
|
||||
else { fatalError()}
|
||||
else { return 0.0 }
|
||||
|
||||
let animator = fpc.animatorForPresenting(to: fpc.layout.initialState)
|
||||
return TimeInterval(animator.duration)
|
||||
@@ -119,7 +119,7 @@ class ModalDismissTransition: NSObject, UIViewControllerAnimatedTransitioning {
|
||||
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
|
||||
guard
|
||||
let fpc = transitionContext?.viewController(forKey: .from) as? FloatingPanelController
|
||||
else { fatalError()}
|
||||
else { return 0.0 }
|
||||
|
||||
let animator = fpc.animatorForDismissing(with: .zero)
|
||||
return TimeInterval(animator.duration)
|
||||
|
||||
+43
-10
@@ -64,6 +64,7 @@ class ControllerTests: XCTestCase {
|
||||
}
|
||||
|
||||
func test_moveTo() {
|
||||
let timeout = 3.0
|
||||
let delegate = FloatingPanelTestDelegate()
|
||||
let fpc = FloatingPanelController(delegate: delegate)
|
||||
XCTAssertEqual(delegate.position, .hidden)
|
||||
@@ -102,7 +103,7 @@ class ControllerTests: XCTestCase {
|
||||
}
|
||||
XCTAssertEqual(fpc.state, .full)
|
||||
XCTAssertEqual(delegate.position, .full)
|
||||
wait(for: [exp], timeout: 1.0)
|
||||
wait(for: [exp], timeout: timeout)
|
||||
}
|
||||
|
||||
XCTContext.runActivity(named: "move to half(animated)") { act in
|
||||
@@ -113,7 +114,7 @@ class ControllerTests: XCTestCase {
|
||||
}
|
||||
XCTAssertEqual(fpc.state, .half)
|
||||
XCTAssertEqual(delegate.position, .half)
|
||||
wait(for: [exp], timeout: 1.0)
|
||||
wait(for: [exp], timeout: timeout)
|
||||
}
|
||||
|
||||
XCTContext.runActivity(named: "move to tip(animated)") { act in
|
||||
@@ -124,7 +125,7 @@ class ControllerTests: XCTestCase {
|
||||
}
|
||||
XCTAssertEqual(fpc.state, .tip)
|
||||
XCTAssertEqual(delegate.position, .tip)
|
||||
wait(for: [exp], timeout: 1.0)
|
||||
wait(for: [exp], timeout: timeout)
|
||||
}
|
||||
|
||||
fpc.move(to: .hidden, animated: true)
|
||||
@@ -137,6 +138,7 @@ class ControllerTests: XCTestCase {
|
||||
class MyFloatingPanelTop2BottomLayout: FloatingPanelTop2BottomTestLayout {
|
||||
override var initialState: FloatingPanelState { return .half }
|
||||
}
|
||||
let timeout = 3.0
|
||||
let delegate = FloatingPanelTestDelegate()
|
||||
let fpc = FloatingPanelController(delegate: delegate)
|
||||
fpc.layout = MyFloatingPanelTop2BottomLayout()
|
||||
@@ -175,7 +177,7 @@ class ControllerTests: XCTestCase {
|
||||
}
|
||||
XCTAssertEqual(fpc.state, .full)
|
||||
XCTAssertEqual(delegate.position, .full)
|
||||
wait(for: [exp], timeout: 1.0)
|
||||
wait(for: [exp], timeout: timeout)
|
||||
}
|
||||
|
||||
XCTContext.runActivity(named: "move to half(animated)") { act in
|
||||
@@ -186,7 +188,7 @@ class ControllerTests: XCTestCase {
|
||||
}
|
||||
XCTAssertEqual(fpc.state, .half)
|
||||
XCTAssertEqual(delegate.position, .half)
|
||||
wait(for: [exp], timeout: 1.0)
|
||||
wait(for: [exp], timeout: timeout)
|
||||
}
|
||||
|
||||
XCTContext.runActivity(named: "move to tip(animated)") { act in
|
||||
@@ -197,7 +199,7 @@ class ControllerTests: XCTestCase {
|
||||
}
|
||||
XCTAssertEqual(fpc.state, .tip)
|
||||
XCTAssertEqual(delegate.position, .tip)
|
||||
wait(for: [exp], timeout: 1.0)
|
||||
wait(for: [exp], timeout: timeout)
|
||||
}
|
||||
|
||||
fpc.move(to: .hidden, animated: true)
|
||||
@@ -223,6 +225,7 @@ class ControllerTests: XCTestCase {
|
||||
}
|
||||
|
||||
func test_moveTo_didMoveDelegate() {
|
||||
let timeout = 3.0
|
||||
let delegate = FloatingPanelTestDelegate()
|
||||
let fpc = FloatingPanelController(delegate: delegate)
|
||||
XCTAssertEqual(delegate.position, .hidden)
|
||||
@@ -237,7 +240,7 @@ class ControllerTests: XCTestCase {
|
||||
exp.fulfill()
|
||||
}
|
||||
fpc.move(to: .full, animated: false)
|
||||
wait(for: [exp], timeout: 1.0)
|
||||
wait(for: [exp], timeout: timeout)
|
||||
|
||||
XCTAssertEqual(count, 1)
|
||||
}
|
||||
@@ -253,7 +256,7 @@ class ControllerTests: XCTestCase {
|
||||
fpc.move(to: .half, animated: true) {
|
||||
exp.fulfill()
|
||||
}
|
||||
wait(for: [exp], timeout: 1.0)
|
||||
wait(for: [exp], timeout: timeout)
|
||||
|
||||
XCTAssertGreaterThan(count, 1)
|
||||
}
|
||||
@@ -270,7 +273,7 @@ class ControllerTests: XCTestCase {
|
||||
exp.fulfill()
|
||||
}
|
||||
}
|
||||
wait(for: [exp], timeout: 1.0)
|
||||
wait(for: [exp], timeout: timeout)
|
||||
|
||||
XCTAssertEqual(count, 1)
|
||||
}
|
||||
@@ -288,7 +291,7 @@ class ControllerTests: XCTestCase {
|
||||
exp.fulfill()
|
||||
}
|
||||
}
|
||||
wait(for: [exp], timeout: 1.0)
|
||||
wait(for: [exp], timeout: timeout)
|
||||
|
||||
XCTAssertGreaterThan(count, 1)
|
||||
}
|
||||
@@ -334,6 +337,36 @@ class ControllerTests: XCTestCase {
|
||||
fpc.move(to: .tip, animated: false)
|
||||
XCTAssertEqual(fpc.surfaceView.frame.height, fpc.view.bounds.height - fpc.surfaceLocation(for: .tip).y)
|
||||
}
|
||||
|
||||
func test_switching_layout() {
|
||||
final class FirstLayout: FloatingPanelLayout {
|
||||
let position: FloatingPanelPosition = .bottom
|
||||
let initialState: FloatingPanelState = .half
|
||||
let anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] = [
|
||||
.full: FloatingPanelLayoutAnchor(absoluteInset: 16.0, edge: .top, referenceGuide: .safeArea),
|
||||
.half: FloatingPanelLayoutAnchor(absoluteInset: 262, edge: .top, referenceGuide: .safeArea),
|
||||
.tip: FloatingPanelLayoutAnchor(absoluteInset: 44.0, edge: .bottom, referenceGuide: .safeArea)
|
||||
]
|
||||
}
|
||||
final class SecondLayout: FloatingPanelLayout {
|
||||
let position: FloatingPanelPosition = .bottom
|
||||
let initialState: FloatingPanelState = .half
|
||||
let anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] = [
|
||||
.half: FloatingPanelLayoutAnchor(absoluteInset: 262, edge: .top, referenceGuide: .safeArea)
|
||||
]
|
||||
}
|
||||
let fpc = FloatingPanelController()
|
||||
fpc.layout = FirstLayout()
|
||||
fpc.showForTest()
|
||||
|
||||
fpc.move(to: .tip, animated: false)
|
||||
|
||||
// Switch to another layout
|
||||
fpc.layout = SecondLayout()
|
||||
fpc.invalidateLayout()
|
||||
|
||||
XCTAssertEqual(fpc.state, .half)
|
||||
}
|
||||
}
|
||||
|
||||
private class MyZombieViewController: UIViewController, FloatingPanelLayout, FloatingPanelBehavior, FloatingPanelControllerDelegate {
|
||||
|
||||
+118
-3
@@ -209,6 +209,8 @@ class CoreTests: XCTestCase {
|
||||
return floor(fpc.backdropView.alpha * 1e+06) / 1e+06
|
||||
}
|
||||
|
||||
let timeout = 3.0
|
||||
|
||||
let delegate = TestDelegate()
|
||||
let fpc = FloatingPanelController(delegate: delegate)
|
||||
fpc.layout = BackdropTestLayout()
|
||||
@@ -228,14 +230,14 @@ class CoreTests: XCTestCase {
|
||||
fpc.move(to: .full, animated: true) {
|
||||
exp1.fulfill()
|
||||
}
|
||||
wait(for: [exp1], timeout: 1.0)
|
||||
wait(for: [exp1], timeout: timeout)
|
||||
XCTAssertEqual(_floor(fpc.backdropView.alpha), 0.3)
|
||||
|
||||
let exp2 = expectation(description: "move to half with animation")
|
||||
fpc.move(to: .half, animated: true) {
|
||||
exp2.fulfill()
|
||||
}
|
||||
wait(for: [exp2], timeout: 1.0)
|
||||
wait(for: [exp2], timeout: timeout)
|
||||
XCTAssertEqual(fpc.backdropView.alpha, 0.0)
|
||||
|
||||
// Test a content mode change of FloatingPanelController
|
||||
@@ -246,7 +248,7 @@ class CoreTests: XCTestCase {
|
||||
}
|
||||
fpc.contentMode = .fitToBounds
|
||||
XCTAssertEqual(fpc.backdropView.alpha, 0.0) // Must not affect the backdrop alpha by changing the content mode
|
||||
wait(for: [exp3], timeout: 1.0)
|
||||
wait(for: [exp3], timeout: timeout)
|
||||
XCTAssertEqual(_floor(fpc.backdropView.alpha), 0.3)
|
||||
|
||||
// Test a size class change of FloatingPanelController.view
|
||||
@@ -820,6 +822,119 @@ class CoreTests: XCTestCase {
|
||||
fpc.showForTest()
|
||||
XCTAssertFalse(fpc.panGestureRecognizer.isEnabled)
|
||||
}
|
||||
|
||||
func test_is_scrollable() {
|
||||
class Delegate: FloatingPanelControllerDelegate {
|
||||
var shouldScroll = false
|
||||
func floatingPanel(
|
||||
_ fpc: FloatingPanelController,
|
||||
shouldAllowToScroll scrollView: UIScrollView,
|
||||
in state: FloatingPanelState
|
||||
) -> Bool {
|
||||
return shouldScroll
|
||||
}
|
||||
}
|
||||
|
||||
let fpc = FloatingPanelController()
|
||||
let scrollView = UIScrollView()
|
||||
let delegate = Delegate()
|
||||
fpc.layout = FloatingPanelBottomLayout()
|
||||
fpc.track(scrollView: scrollView)
|
||||
fpc.showForTest()
|
||||
|
||||
XCTAssertTrue(fpc.floatingPanel.isScrollable(state: .full))
|
||||
XCTAssertFalse(fpc.floatingPanel.isScrollable(state: .half))
|
||||
|
||||
fpc.delegate = delegate
|
||||
|
||||
XCTAssertFalse(fpc.floatingPanel.isScrollable(state: .full))
|
||||
XCTAssertFalse(fpc.floatingPanel.isScrollable(state: .half))
|
||||
|
||||
delegate.shouldScroll = true
|
||||
|
||||
XCTAssertTrue(fpc.floatingPanel.isScrollable(state: .full))
|
||||
XCTAssertTrue(fpc.floatingPanel.isScrollable(state: .half))
|
||||
}
|
||||
|
||||
func test_adjustScrollContentInsetIfNeeded() {
|
||||
class CustomScrollView: UIScrollView {
|
||||
var customSafeAreaInsets: UIEdgeInsets = .zero
|
||||
override var safeAreaInsets: UIEdgeInsets {
|
||||
customSafeAreaInsets
|
||||
}
|
||||
}
|
||||
|
||||
do {
|
||||
let scrollView = CustomScrollView()
|
||||
scrollView.customSafeAreaInsets = UIEdgeInsets(top: 0, left: 0, bottom: 34, right: 0)
|
||||
let fpc = FloatingPanelController()
|
||||
fpc.track(scrollView: scrollView)
|
||||
fpc.layout = FloatingPanelBottomLayout()
|
||||
fpc.contentInsetAdjustmentBehavior = .always
|
||||
fpc.contentMode = .static
|
||||
fpc.showForTest()
|
||||
|
||||
fpc.move(to: .half, animated: false)
|
||||
fpc.floatingPanel.adjustScrollContentInsetIfNeeded()
|
||||
|
||||
let expect = 34 + (fpc.surfaceLocation(for: .half).y - fpc.surfaceLocation(for: .full).y)
|
||||
XCTAssertEqual(
|
||||
scrollView.contentInset,
|
||||
UIEdgeInsets(top: 0, left: 0, bottom: expect, right: 0)
|
||||
)
|
||||
|
||||
fpc.contentMode = .fitToBounds
|
||||
XCTAssertEqual(
|
||||
scrollView.contentInset,
|
||||
scrollView.customSafeAreaInsets
|
||||
)
|
||||
}
|
||||
|
||||
do {
|
||||
let scrollView = CustomScrollView()
|
||||
scrollView.customSafeAreaInsets = UIEdgeInsets(top: 91, left: 0, bottom: 0, right: 0)
|
||||
let fpc = FloatingPanelController()
|
||||
fpc.track(scrollView: scrollView)
|
||||
fpc.layout = FloatingPanelTopPositionedLayout()
|
||||
fpc.contentInsetAdjustmentBehavior = .always
|
||||
fpc.contentMode = .static
|
||||
fpc.showForTest()
|
||||
|
||||
fpc.move(to: .half, animated: false)
|
||||
fpc.floatingPanel.adjustScrollContentInsetIfNeeded()
|
||||
|
||||
let expect = 91 + (fpc.surfaceLocation(for: .full).y - fpc.surfaceLocation(for: .half).y)
|
||||
XCTAssertEqual(
|
||||
scrollView.contentInset,
|
||||
UIEdgeInsets(top: expect, left: 0, bottom: 0, right: 0)
|
||||
)
|
||||
fpc.contentMode = .fitToBounds
|
||||
XCTAssertEqual(
|
||||
scrollView.contentInset,
|
||||
scrollView.customSafeAreaInsets
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func test_handleGesture_endWithoutAttraction() throws {
|
||||
class Delegate: FloatingPanelControllerDelegate {
|
||||
var willAttract: Bool?
|
||||
func floatingPanelDidEndDragging(_ fpc: FloatingPanelController, willAttract attract: Bool) {
|
||||
willAttract = attract
|
||||
}
|
||||
}
|
||||
let fpc = FloatingPanelController()
|
||||
let delegate = Delegate()
|
||||
fpc.showForTest()
|
||||
fpc.delegate = delegate
|
||||
XCTAssertEqual(fpc.state, .half)
|
||||
|
||||
fpc.floatingPanel.endWithoutAttraction(.full)
|
||||
|
||||
XCTAssertEqual(fpc.state, .full)
|
||||
XCTAssertEqual(fpc.surfaceLocation(for: .full).y, fpc.surfaceLocation.y)
|
||||
XCTAssertEqual(delegate.willAttract, false)
|
||||
}
|
||||
}
|
||||
|
||||
private class FloatingPanelLayout3Positions: FloatingPanelTestLayout {
|
||||
|
||||
@@ -9,4 +9,20 @@ class ExtensionTests: XCTestCase {
|
||||
XCTAssertNotEqual(CGFloat(333.5).rounded(by: 3), 333.66666666666674)
|
||||
XCTAssertTrue(CGFloat(333.5).isEqual(to: 333.66666666666674, on: 3.0))
|
||||
}
|
||||
|
||||
func test_roundedByDisplayScale_2() {
|
||||
XCTAssertEqual(CGFloat(-0.16666666666674246).rounded(by: 3), 0.0)
|
||||
XCTAssertEqual(CGFloat(0.16666666666674246).rounded(by: 3), 0.0)
|
||||
|
||||
XCTAssertEqual(CGFloat(-0.3333333333374246).rounded(by: 3), -0.3333333333333333)
|
||||
XCTAssertEqual(CGFloat(-0.3333333333074246).rounded(by: 3), -0.3333333333333333)
|
||||
XCTAssertEqual(CGFloat(0.33333333333374246).rounded(by: 3), 0.3333333333333333)
|
||||
XCTAssertEqual(CGFloat(0.33333333333074246).rounded(by: 3), 0.3333333333333333)
|
||||
|
||||
XCTAssertEqual(CGFloat(-0.16666666666674246).rounded(by: 2), 0.0)
|
||||
XCTAssertEqual(CGFloat(0.16666666666674246).rounded(by: 2), 0.0)
|
||||
|
||||
XCTAssertEqual(CGFloat(-0.16666666666674246).rounded(by: 6), -0.16666666666666666)
|
||||
XCTAssertEqual(CGFloat(0.16666666666674246).rounded(by: 6), 0.16666666666666666)
|
||||
}
|
||||
}
|
||||
|
||||
+59
-33
@@ -411,18 +411,18 @@ class LayoutTests: XCTestCase {
|
||||
for prop in [
|
||||
// from top edge
|
||||
(anchor: FloatingPanelLayoutAnchor(absoluteInset: 0.0, edge: .top, referenceGuide: .safeArea),
|
||||
result: (#line, constant: 0.0, firstAnchor: fpc.surfaceView.bottomAnchor, secondAnchor: fpc.fp_safeAreaLayoutGuide.topAnchor)),
|
||||
result: (#line, constant: 0.0, firstAnchor: fpc.surfaceView.bottomAnchor, secondAnchor: fpc.view.safeAreaLayoutGuide.topAnchor)),
|
||||
(anchor: FloatingPanelLayoutAnchor(absoluteInset: 100.0, edge: .top, referenceGuide: .safeArea),
|
||||
result: (#line, constant: 100.0, firstAnchor: fpc.surfaceView.bottomAnchor, secondAnchor: fpc.fp_safeAreaLayoutGuide.topAnchor)),
|
||||
result: (#line, constant: 100.0, firstAnchor: fpc.surfaceView.bottomAnchor, secondAnchor: fpc.view.safeAreaLayoutGuide.topAnchor)),
|
||||
(anchor: FloatingPanelLayoutAnchor(absoluteInset: 0.0, edge: .top, referenceGuide: .superview),
|
||||
result: (#line, constant: 0.0, firstAnchor: fpc.surfaceView.bottomAnchor, secondAnchor: fpc.view.topAnchor)),
|
||||
(anchor: FloatingPanelLayoutAnchor(absoluteInset: 100.0, edge: .top, referenceGuide: .superview),
|
||||
result: (#line, constant: 100.0, firstAnchor: fpc.surfaceView.bottomAnchor, secondAnchor: fpc.view.topAnchor)),
|
||||
// from bottom edge
|
||||
(anchor: FloatingPanelLayoutAnchor(absoluteInset: 0.0, edge: .bottom, referenceGuide: .safeArea),
|
||||
result: (#line, constant: 0.0, firstAnchor: fpc.fp_safeAreaLayoutGuide.bottomAnchor, secondAnchor: fpc.surfaceView.bottomAnchor)),
|
||||
result: (#line, constant: 0.0, firstAnchor: fpc.view.safeAreaLayoutGuide.bottomAnchor, secondAnchor: fpc.surfaceView.bottomAnchor)),
|
||||
(anchor: FloatingPanelLayoutAnchor(absoluteInset: 100.0, edge: .bottom, referenceGuide: .safeArea),
|
||||
result: (#line, constant: 100.0, firstAnchor: fpc.fp_safeAreaLayoutGuide.bottomAnchor, secondAnchor: fpc.surfaceView.bottomAnchor)),
|
||||
result: (#line, constant: 100.0, firstAnchor: fpc.view.safeAreaLayoutGuide.bottomAnchor, secondAnchor: fpc.surfaceView.bottomAnchor)),
|
||||
(anchor: FloatingPanelLayoutAnchor(absoluteInset: 0.0, edge: .bottom, referenceGuide: .superview),
|
||||
result: (#line, constant: 0.0, firstAnchor: fpc.view.bottomAnchor, secondAnchor: fpc.surfaceView.bottomAnchor)),
|
||||
(anchor: FloatingPanelLayoutAnchor(absoluteInset: 100.0, edge: .bottom, referenceGuide: .superview),
|
||||
@@ -440,7 +440,7 @@ class LayoutTests: XCTestCase {
|
||||
(anchor: FloatingPanelLayoutAnchor(fractionalInset: 0.0, edge: .top, referenceGuide: .safeArea),
|
||||
result: (#line, multiplier: 1.0, secondAnchor: nil)),
|
||||
(anchor: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .top, referenceGuide: .safeArea),
|
||||
result: (#line, multiplier: 0.5, secondAnchor: fpc.fp_safeAreaLayoutGuide.heightAnchor)),
|
||||
result: (#line, multiplier: 0.5, secondAnchor: fpc.view.safeAreaLayoutGuide.heightAnchor)),
|
||||
(anchor: FloatingPanelLayoutAnchor(fractionalInset: 0.0, edge: .top, referenceGuide: .superview),
|
||||
result: (#line, multiplier: 1.0, secondAnchor: nil)),
|
||||
(anchor: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .top, referenceGuide: .superview),
|
||||
@@ -450,7 +450,7 @@ class LayoutTests: XCTestCase {
|
||||
(anchor: FloatingPanelLayoutAnchor(fractionalInset: 0.0, edge: .bottom, referenceGuide: .safeArea),
|
||||
result: (#line, multiplier: 1.0, secondAnchor: nil)),
|
||||
(anchor: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .safeArea),
|
||||
result: (#line, multiplier: 0.5, secondAnchor: fpc.fp_safeAreaLayoutGuide.heightAnchor)),
|
||||
result: (#line, multiplier: 0.5, secondAnchor: fpc.view.safeAreaLayoutGuide.heightAnchor)),
|
||||
(anchor: FloatingPanelLayoutAnchor(fractionalInset: 0.0, edge: .bottom, referenceGuide: .superview),
|
||||
result: (#line, multiplier: 1.0, secondAnchor: nil)),
|
||||
(anchor: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .superview),
|
||||
@@ -459,11 +459,7 @@ class LayoutTests: XCTestCase {
|
||||
let c = prop.anchor.layoutConstraints(fpc, for: position)[0]
|
||||
XCTAssertEqual(c.multiplier, CGFloat(prop.result.multiplier), line: UInt(prop.result.0))
|
||||
XCTAssertTrue(c.firstAnchor is NSLayoutAnchor<NSLayoutDimension>, line: UInt(prop.result.0))
|
||||
// On iOS 10, `c.secondAnchor` can't be equal object to `prop.result.secondAnchor`
|
||||
// because there is no safe area on iOS 10 and `fp_safeAreaLayoutGuide` emulates it.
|
||||
if #available(iOS 11, *) {
|
||||
XCTAssertEqual(c.secondAnchor, prop.result.secondAnchor, line: UInt(prop.result.0))
|
||||
}
|
||||
XCTAssertEqual(c.secondAnchor, prop.result.secondAnchor, line: UInt(prop.result.0))
|
||||
}
|
||||
}
|
||||
func test_layoutAnchor_bottomPosition() {
|
||||
@@ -476,9 +472,9 @@ class LayoutTests: XCTestCase {
|
||||
for prop in [
|
||||
// from top edge
|
||||
(anchor: FloatingPanelLayoutAnchor(absoluteInset: 0.0, edge: .top, referenceGuide: .safeArea),
|
||||
result: (#line, constant: 0.0, firstAnchor: fpc.surfaceView.topAnchor, secondAnchor: fpc.fp_safeAreaLayoutGuide.topAnchor)),
|
||||
result: (#line, constant: 0.0, firstAnchor: fpc.surfaceView.topAnchor, secondAnchor: fpc.view.safeAreaLayoutGuide.topAnchor)),
|
||||
(anchor: FloatingPanelLayoutAnchor(absoluteInset: 100.0, edge: .top, referenceGuide: .safeArea),
|
||||
result: (#line, constant: 100.0, firstAnchor: fpc.surfaceView.topAnchor, secondAnchor: fpc.fp_safeAreaLayoutGuide.topAnchor)),
|
||||
result: (#line, constant: 100.0, firstAnchor: fpc.surfaceView.topAnchor, secondAnchor: fpc.view.safeAreaLayoutGuide.topAnchor)),
|
||||
(anchor: FloatingPanelLayoutAnchor(absoluteInset: 0.0, edge: .top, referenceGuide: .superview),
|
||||
result: (#line, constant: 0.0, firstAnchor: fpc.surfaceView.topAnchor, secondAnchor: fpc.view.topAnchor)),
|
||||
(anchor: FloatingPanelLayoutAnchor(absoluteInset: 100.0, edge: .top, referenceGuide: .superview),
|
||||
@@ -486,9 +482,9 @@ class LayoutTests: XCTestCase {
|
||||
|
||||
// from bottom edge
|
||||
(anchor: FloatingPanelLayoutAnchor(absoluteInset: 0.0, edge: .bottom, referenceGuide: .safeArea),
|
||||
result: (#line, constant: 0.0, firstAnchor: fpc.fp_safeAreaLayoutGuide.bottomAnchor, secondAnchor: fpc.surfaceView.topAnchor)),
|
||||
result: (#line, constant: 0.0, firstAnchor: fpc.view.safeAreaLayoutGuide.bottomAnchor, secondAnchor: fpc.surfaceView.topAnchor)),
|
||||
(anchor: FloatingPanelLayoutAnchor(absoluteInset: 100.0, edge: .bottom, referenceGuide: .safeArea),
|
||||
result: (#line, constant: 100.0, firstAnchor: fpc.fp_safeAreaLayoutGuide.bottomAnchor, secondAnchor: fpc.surfaceView.topAnchor)),
|
||||
result: (#line, constant: 100.0, firstAnchor: fpc.view.safeAreaLayoutGuide.bottomAnchor, secondAnchor: fpc.surfaceView.topAnchor)),
|
||||
(anchor: FloatingPanelLayoutAnchor(absoluteInset: 0.0, edge: .bottom, referenceGuide: .superview),
|
||||
result: (#line, constant: 0.0, firstAnchor: fpc.view.bottomAnchor, secondAnchor: fpc.surfaceView.topAnchor)),
|
||||
(anchor: FloatingPanelLayoutAnchor(absoluteInset: 100.0, edge: .bottom, referenceGuide: .superview),
|
||||
@@ -506,7 +502,7 @@ class LayoutTests: XCTestCase {
|
||||
(anchor: FloatingPanelLayoutAnchor(fractionalInset: 0.0, edge: .top, referenceGuide: .safeArea),
|
||||
result: (#line, multiplier: 1.0, secondAnchor: nil)),
|
||||
(anchor: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .top, referenceGuide: .safeArea),
|
||||
result: (#line, multiplier: 0.5, secondAnchor: fpc.fp_safeAreaLayoutGuide.heightAnchor)),
|
||||
result: (#line, multiplier: 0.5, secondAnchor: fpc.view.safeAreaLayoutGuide.heightAnchor)),
|
||||
(anchor: FloatingPanelLayoutAnchor(fractionalInset: 0.0, edge: .top, referenceGuide: .superview),
|
||||
result: (#line, multiplier: 1.0, secondAnchor: nil)),
|
||||
(anchor: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .top, referenceGuide: .superview),
|
||||
@@ -516,7 +512,7 @@ class LayoutTests: XCTestCase {
|
||||
(anchor: FloatingPanelLayoutAnchor(fractionalInset: 0.0, edge: .bottom, referenceGuide: .safeArea),
|
||||
result: (#line, multiplier: 1.0, secondAnchor: nil)),
|
||||
(anchor: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .safeArea),
|
||||
result: (#line, multiplier: 0.5, secondAnchor: fpc.fp_safeAreaLayoutGuide.heightAnchor)),
|
||||
result: (#line, multiplier: 0.5, secondAnchor: fpc.view.safeAreaLayoutGuide.heightAnchor)),
|
||||
(anchor: FloatingPanelLayoutAnchor(fractionalInset: 0.0, edge: .bottom, referenceGuide: .superview),
|
||||
result: (#line, multiplier: 1.0, secondAnchor: nil)),
|
||||
(anchor: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .superview),
|
||||
@@ -525,12 +521,7 @@ class LayoutTests: XCTestCase {
|
||||
let c = prop.anchor.layoutConstraints(fpc, for: position)[0]
|
||||
XCTAssertEqual(c.multiplier, CGFloat(prop.result.multiplier), line: UInt(prop.result.0))
|
||||
XCTAssertTrue(c.firstAnchor is NSLayoutAnchor<NSLayoutDimension>, line: UInt(prop.result.0))
|
||||
// On iOS 10, `c.secondAnchor` can't be equal object to `prop.result.secondAnchor`
|
||||
// because there is no safe area on iOS 10 and `fp_safeAreaLayoutGuide` emulates it.
|
||||
if #available(iOS 11, *) {
|
||||
XCTAssertEqual(c.secondAnchor, prop.result.secondAnchor, line: UInt(prop.result.0))
|
||||
}
|
||||
print(c)
|
||||
XCTAssertEqual(c.secondAnchor, prop.result.secondAnchor, line: UInt(prop.result.0))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -556,20 +547,20 @@ class LayoutTests: XCTestCase {
|
||||
|
||||
for prop in [
|
||||
(anchor: FloatingPanelIntrinsicLayoutAnchor(absoluteOffset: 0.0, referenceGuide: .safeArea),
|
||||
result: (#line, constant: 420, firstAnchor: fpc.surfaceView.bottomAnchor, secondAnchor: fpc.fp_safeAreaLayoutGuide.topAnchor)),
|
||||
result: (#line, constant: 420, firstAnchor: fpc.surfaceView.bottomAnchor, secondAnchor: fpc.view.safeAreaLayoutGuide.topAnchor)),
|
||||
(anchor: FloatingPanelIntrinsicLayoutAnchor(absoluteOffset: 42.0, referenceGuide: .safeArea),
|
||||
result: (#line, constant: 420 - 42, firstAnchor: fpc.surfaceView.bottomAnchor, secondAnchor: fpc.fp_safeAreaLayoutGuide.topAnchor)),
|
||||
result: (#line, constant: 420 - 42, firstAnchor: fpc.surfaceView.bottomAnchor, secondAnchor: fpc.view.safeAreaLayoutGuide.topAnchor)),
|
||||
(anchor: FloatingPanelIntrinsicLayoutAnchor(absoluteOffset: 0.0, referenceGuide: .superview),
|
||||
result: (#line, constant: 420, firstAnchor: fpc.surfaceView.bottomAnchor, secondAnchor: fpc.view.topAnchor)),
|
||||
(anchor: FloatingPanelIntrinsicLayoutAnchor(absoluteOffset: 42.0, referenceGuide: .superview),
|
||||
result: (#line, constant: 420 - 42, firstAnchor: fpc.surfaceView.bottomAnchor, secondAnchor: fpc.view.topAnchor)),
|
||||
|
||||
(anchor: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 0.0, referenceGuide: .safeArea),
|
||||
result: (#line, constant: 420, firstAnchor: fpc.surfaceView.bottomAnchor, secondAnchor: fpc.fp_safeAreaLayoutGuide.topAnchor)),
|
||||
result: (#line, constant: 420, firstAnchor: fpc.surfaceView.bottomAnchor, secondAnchor: fpc.view.safeAreaLayoutGuide.topAnchor)),
|
||||
(anchor: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 0.5, referenceGuide: .safeArea),
|
||||
result: (#line, constant: 210, firstAnchor: fpc.surfaceView.bottomAnchor, secondAnchor: fpc.fp_safeAreaLayoutGuide.topAnchor)),
|
||||
result: (#line, constant: 210, firstAnchor: fpc.surfaceView.bottomAnchor, secondAnchor: fpc.view.safeAreaLayoutGuide.topAnchor)),
|
||||
(anchor: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 1.0, referenceGuide: .safeArea),
|
||||
result: (#line, constant: 0, firstAnchor: fpc.surfaceView.bottomAnchor, secondAnchor: fpc.fp_safeAreaLayoutGuide.topAnchor)),
|
||||
result: (#line, constant: 0, firstAnchor: fpc.surfaceView.bottomAnchor, secondAnchor: fpc.view.safeAreaLayoutGuide.topAnchor)),
|
||||
(anchor: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 0.0, referenceGuide: .superview),
|
||||
result: (#line, constant: 420, firstAnchor: fpc.surfaceView.bottomAnchor, secondAnchor: fpc.view.topAnchor)),
|
||||
(anchor: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 0.5, referenceGuide: .superview),
|
||||
@@ -606,20 +597,20 @@ class LayoutTests: XCTestCase {
|
||||
|
||||
for prop in [
|
||||
(anchor: FloatingPanelIntrinsicLayoutAnchor(absoluteOffset: 0.0, referenceGuide: .safeArea),
|
||||
result: (#line, constant: -420, firstAnchor: fpc.surfaceView.topAnchor, secondAnchor: fpc.fp_safeAreaLayoutGuide.bottomAnchor)),
|
||||
result: (#line, constant: -420, firstAnchor: fpc.surfaceView.topAnchor, secondAnchor: fpc.view.safeAreaLayoutGuide.bottomAnchor)),
|
||||
(anchor: FloatingPanelIntrinsicLayoutAnchor(absoluteOffset: 42.0, referenceGuide: .safeArea),
|
||||
result: (#line, constant: -420 + 42, firstAnchor: fpc.surfaceView.topAnchor, secondAnchor: fpc.fp_safeAreaLayoutGuide.bottomAnchor)),
|
||||
result: (#line, constant: -420 + 42, firstAnchor: fpc.surfaceView.topAnchor, secondAnchor: fpc.view.safeAreaLayoutGuide.bottomAnchor)),
|
||||
(anchor: FloatingPanelIntrinsicLayoutAnchor(absoluteOffset: 0.0, referenceGuide: .superview),
|
||||
result: (#line, constant: -420, firstAnchor: fpc.surfaceView.topAnchor, secondAnchor: fpc.view.bottomAnchor)),
|
||||
(anchor: FloatingPanelIntrinsicLayoutAnchor(absoluteOffset: 42.0, referenceGuide: .superview),
|
||||
result: (#line, constant: -420 + 42, firstAnchor: fpc.surfaceView.topAnchor, secondAnchor: fpc.view.bottomAnchor)),
|
||||
|
||||
(anchor: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 0.0, referenceGuide: .safeArea),
|
||||
result: (#line, constant: -420, firstAnchor: fpc.surfaceView.topAnchor, secondAnchor: fpc.fp_safeAreaLayoutGuide.bottomAnchor)),
|
||||
result: (#line, constant: -420, firstAnchor: fpc.surfaceView.topAnchor, secondAnchor: fpc.view.safeAreaLayoutGuide.bottomAnchor)),
|
||||
(anchor: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 0.5, referenceGuide: .safeArea),
|
||||
result: (#line, constant: -210, firstAnchor: fpc.surfaceView.topAnchor, secondAnchor: fpc.fp_safeAreaLayoutGuide.bottomAnchor)),
|
||||
result: (#line, constant: -210, firstAnchor: fpc.surfaceView.topAnchor, secondAnchor: fpc.view.safeAreaLayoutGuide.bottomAnchor)),
|
||||
(anchor: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 1.0, referenceGuide: .safeArea),
|
||||
result: (#line, constant: 0, firstAnchor: fpc.surfaceView.topAnchor, secondAnchor: fpc.fp_safeAreaLayoutGuide.bottomAnchor)),
|
||||
result: (#line, constant: 0, firstAnchor: fpc.surfaceView.topAnchor, secondAnchor: fpc.view.safeAreaLayoutGuide.bottomAnchor)),
|
||||
(anchor: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 0.0, referenceGuide: .superview),
|
||||
result: (#line, constant: -420, firstAnchor: fpc.surfaceView.topAnchor, secondAnchor: fpc.view.bottomAnchor)),
|
||||
(anchor: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 0.5, referenceGuide: .superview),
|
||||
@@ -633,6 +624,41 @@ class LayoutTests: XCTestCase {
|
||||
XCTAssertEqual(c.secondAnchor, prop.result.secondAnchor, line: UInt(prop.result.0))
|
||||
}
|
||||
}
|
||||
|
||||
func test_offsetFromMostExpandedAnchor() {
|
||||
do {
|
||||
let fpc = FloatingPanelController()
|
||||
fpc.layout = FloatingPanelBottomLayout()
|
||||
fpc.showForTest()
|
||||
|
||||
fpc.move(to: .full, animated: false)
|
||||
|
||||
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.offsetFromMostExpandedAnchor, 0.0)
|
||||
|
||||
fpc.move(to: .half, animated: false)
|
||||
|
||||
XCTAssertEqual(
|
||||
fpc.floatingPanel.layoutAdapter.offsetFromMostExpandedAnchor,
|
||||
(fpc.surfaceLocation(for: .half) - fpc.surfaceLocation(for: .full)).y
|
||||
)
|
||||
}
|
||||
do {
|
||||
let fpc = FloatingPanelController()
|
||||
fpc.layout = FloatingPanelTopPositionedLayout()
|
||||
fpc.showForTest()
|
||||
|
||||
fpc.move(to: .full, animated: false)
|
||||
|
||||
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.offsetFromMostExpandedAnchor, 0.0)
|
||||
|
||||
fpc.move(to: .half, animated: false)
|
||||
|
||||
XCTAssertEqual(
|
||||
fpc.floatingPanel.layoutAdapter.offsetFromMostExpandedAnchor,
|
||||
-(fpc.surfaceLocation(for: .half) - fpc.surfaceLocation(for: .full)).y
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private typealias LayoutSegmentTestParameter = (UInt, pos: CGFloat, forwardY: Bool, lower: FloatingPanelState?, upper: FloatingPanelState?)
|
||||
|
||||
@@ -72,6 +72,16 @@ class FloatingPanelTop2BottomTestLayout: FloatingPanelLayout {
|
||||
}
|
||||
}
|
||||
|
||||
class FloatingPanelTopPositionedLayout: FloatingPanelLayout {
|
||||
let position: FloatingPanelPosition = .top
|
||||
let initialState: FloatingPanelState = .full
|
||||
let anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] = [
|
||||
.full: FloatingPanelLayoutAnchor(absoluteInset: 88.0, edge: .bottom, referenceGuide: .safeArea),
|
||||
.half: FloatingPanelLayoutAnchor(absoluteInset: 216.0, edge: .top, referenceGuide: .safeArea),
|
||||
.tip: FloatingPanelLayoutAnchor(absoluteInset: 44.0, edge: .top, referenceGuide: .safeArea)
|
||||
]
|
||||
}
|
||||
|
||||
class FloatingPanelProjectableBehavior: FloatingPanelBehavior {
|
||||
func shouldProjectMomentum(_ fpc: FloatingPanelController, to proposedState: FloatingPanelState) -> Bool {
|
||||
return true
|
||||
|
||||
Reference in New Issue
Block a user