Compare commits

..

3 Commits

Author SHA1 Message Date
Shin Yamamoto ac675d505b Add build-tools package for the command and build tool plugins
For backward compat, build-tools package isn't added to FloatingPanel
project file as a local package. Because it breaks builds on Xcode 14.2
or earlier according to the swift version.

I confirmed this behavior was improved on Xcode 15 beta.

But so far we need to add the build-tools package into the project
file manually as a local package to work the build tool plugin.
2023-06-10 10:09:32 +09:00
Shin Yamamoto 53bb530537 Turn NeverForceUnwrap & NeverUseForceTry on 2023-06-10 10:02:02 +09:00
Shin Yamamoto d6d8834423 Fix swift-format's errors and warnings 2023-06-10 10:02:02 +09:00
51 changed files with 2392 additions and 2636 deletions
+53 -87
View File
@@ -4,90 +4,99 @@ on:
push:
branches:
- master
- next
pull_request:
branches:
- '*'
jobs:
lint:
runs-on: ${{ matrix.runsOn }}
env:
DEVELOPER_DIR: /Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer
SWIFT_FORMAT: build-tools/.build/release/swift-format
strategy:
fail-fast: false
matrix:
include:
- xcode: "14.3"
runsOn: macos-13
steps:
- uses: actions/checkout@v3
- name: Cache swift-format
id: cache_swift-format
uses: actions/cache@v3
env:
cache-name: cache_swift-format
with:
path: ${{ env.SWIFT_FORMAT }}
key: ${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('build-tools/Package.resolved') }}
restore-keys: |
${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('build-tools/Package.resolved') }}
- name: Run swift-format
run: |
[[ -f $SWIFT_FORMAT ]] || xcrun swift build --package-path "build-tools" -c release
$SWIFT_FORMAT lint --configuration .swift-format -s -r Sources Tests
build:
runs-on: ${{ matrix.runs-on }}
runs-on: ${{ matrix.runsOn }}
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.1"
runs-on: macos-13
xcode: "14.3"
runsOn: macos-13
- swift: "5.7"
xcode: "14.1"
runs-on: macos-12
runsOn: macos-12
- swift: "5.6"
xcode: "13.4.1"
runs-on: macos-12
runsOn: macos-12
- swift: "5.5"
xcode: "13.2.1"
runs-on: macos-12
runsOn: macos-11
- swift: "5.4"
xcode: "12.5.1"
runs-on: macos-11
runsOn: macos-11
- swift: "5.3"
xcode: "12.4"
runs-on: macos-11
runsOn: macos-11
- swift: "5.2"
xcode: "11.7"
runs-on: macos-11
runsOn: macos-11
steps:
- uses: actions/checkout@v3
- name: Building in Swift ${{ matrix.swift }}
run: xcodebuild -scheme FloatingPanel SWIFT_VERSION=${{ matrix.swift }} clean build
test:
runs-on: ${{ matrix.runs-on }}
runs-on: ${{ matrix.runsOn }}
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"
- os: "16.1"
xcode: "14.1"
sim: "iPhone 14 Pro"
parallel: NO # Stop random test job failures
runs-on: macos-13
runsOn: macos-12
- os: "15.5"
xcode: "13.4.1"
sim: "iPhone 13 Pro"
parallel: NO # Stop random test job failures
runs-on: macos-12
runsOn: macos-12
steps:
- uses: actions/checkout@v3
- name: Testing in iOS ${{ matrix.os }}
run: |
xcodebuild clean test \
-workspace FloatingPanel.xcworkspace \
-scheme FloatingPanel \
-destination 'platform=iOS Simulator,OS=${{ matrix.os }},name=${{ matrix.sim }}' \
-parallel-testing-enabled '${{ matrix.parallel }}'
run: xcodebuild clean test -scheme FloatingPanel -workspace FloatingPanel.xcworkspace -destination 'platform=iOS Simulator,OS=${{ matrix.os }},name=${{ matrix.sim }}'
timeout-minutes: 20
example:
runs-on: macos-14
runs-on: macos-12
env:
DEVELOPER_DIR: /Applications/Xcode_15.4.app/Contents/Developer
DEVELOPER_DIR: /Applications/Xcode_14.1.app/Contents/Developer
strategy:
fail-fast: false
matrix:
@@ -99,67 +108,26 @@ jobs:
steps:
- uses: actions/checkout@v3
- name: Building ${{ matrix.example }}
run: |
xcodebuild clean build \
-workspace FloatingPanel.xcworkspace \
-scheme ${{ matrix.example }} \
-sdk iphonesimulator
run: xcodebuild -workspace FloatingPanel.xcworkspace -scheme ${{ matrix.example }} -sdk iphonesimulator clean build
swiftpm:
runs-on: macos-14
runs-on: macos-12
env:
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@v3
- 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
DEVELOPER_DIR: /Applications/Xcode_14.1.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"
xcode: "14.1"
runs-on: macos-12
# 16.1
- target: "x86_64-apple-ios16.1-simulator"
- target: "arm64-apple-ios16.1-simulator"
steps:
- uses: actions/checkout@v3
- name: "Swift Package Manager build"
run: |
swift build \
-Xswiftc "-sdk" -Xswiftc "`xcrun --sdk iphonesimulator --show-sdk-path`" \
-Xswiftc "-target" -Xswiftc "${{ matrix.target }}"
run: swift build -Xswiftc "-sdk" -Xswiftc "`xcrun --sdk iphonesimulator --show-sdk-path`" -Xswiftc "-target" -Xswiftc "${{ matrix.target }}"
carthage:
runs-on: macos-11
@@ -169,9 +137,7 @@ jobs:
run: carthage build --use-xcframeworks --no-skip-current
cocoapods:
runs-on: macos-14
env:
DEVELOPER_DIR: /Applications/Xcode_15.4.app/Contents/Developer
runs-on: macos-12
steps:
- uses: actions/checkout@v3
- name: "CocoaPods: pod lib lint"
-5
View File
@@ -1,5 +0,0 @@
version: 1
builder:
configs:
- documentation_targets: [FloatingPanel]
platform: ios
+56
View File
@@ -0,0 +1,56 @@
{
"fileScopedDeclarationPrivacy" : {
"accessLevel" : "private"
},
"indentation" : {
"spaces" : 4
},
"indentConditionalCompilationBlocks" : false,
"indentSwitchCaseLabels" : false,
"lineBreakAroundMultilineExpressionChainComponents" : false,
"lineBreakBeforeControlFlowKeywords" : false,
"lineBreakBeforeEachArgument" : false,
"lineBreakBeforeEachGenericRequirement" : false,
"lineLength" : 240,
"maximumBlankLines" : 1,
"prioritizeKeepingFunctionOutputTogether" : false,
"respectsExistingLineBreaks" : true,
"rules" : {
"AllPublicDeclarationsHaveDocumentation" : false,
"AlwaysUseLowerCamelCase" : false,
"AmbiguousTrailingClosureOverload" : true,
"BeginDocumentationCommentWithOneLineSummary" : false,
"DoNotUseSemicolons" : true,
"DontRepeatTypeInStaticProperties" : true,
"FileScopedDeclarationPrivacy" : true,
"FullyIndirectEnum" : true,
"GroupNumericLiterals" : true,
"IdentifiersMustBeASCII" : true,
"NeverForceUnwrap" : true,
"NeverUseForceTry" : true,
"NeverUseImplicitlyUnwrappedOptionals" : false,
"NoAccessLevelOnExtensionDeclaration" : true,
"NoBlockComments" : true,
"NoCasesWithOnlyFallthrough" : true,
"NoEmptyTrailingClosureParentheses" : true,
"NoLabelsInCasePatterns" : true,
"NoLeadingUnderscores" : false,
"NoParensAroundConditions" : true,
"NoVoidReturnOnFunctionSignature" : true,
"OneCasePerLine" : true,
"OneVariableDeclarationPerLine" : true,
"OnlyOneTrailingClosureArgument" : true,
"OrderedImports" : true,
"ReturnVoidInsteadOfEmptyTuple" : true,
"UseEarlyExits" : false,
"UseLetInEveryBoundCaseVariable" : true,
"UseShorthandTypeNames" : true,
"UseSingleLinePropertyGetter" : true,
"UseSynthesizedInitializer" : true,
"UseTripleSlashForDocumentationComments" : true,
"UseWhereClausesInForLoops" : false,
"ValidateDocumentationComments" : false
},
"tabWidth" : 8,
"version" : 1
}
+31 -33
View File
@@ -1,9 +1,7 @@
<?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="BYZ-38-t0r">
<device id="retina6_12" orientation="portrait" appearance="light"/>
<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">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21679"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="19454"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
@@ -13,16 +11,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="393" height="852"/>
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<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="393" height="852"/>
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
</mapView>
<visualEffectView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="d9i-3g-8Ja">
<rect key="frame" x="0.0" y="0.0" width="393" height="59"/>
<rect key="frame" x="0.0" y="0.0" width="600" height="0.0"/>
<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="393" height="59"/>
<rect key="frame" x="0.0" y="0.0" width="600" height="0.0"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
</view>
<blurEffect style="prominent"/>
@@ -54,31 +52,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="393" height="852"/>
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<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="393" height="852"/>
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<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="393" height="852"/>
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<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="393" height="56"/>
<rect key="frame" x="0.0" y="6" width="600" height="51"/>
<textInputTraits key="textInputTraits"/>
</searchBar>
<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"/>
<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"/>
<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="393" height="116"/>
<rect key="frame" x="0.0" y="0.0" width="600" 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.666666666666664" width="345" height="97.333333333333343"/>
<rect key="frame" x="24" y="10.5" width="552" height="97.5"/>
<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.333333333333329"/>
<rect key="frame" x="0.0" y="0.0" width="60" height="97.5"/>
<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"/>
@@ -88,7 +86,7 @@
</constraints>
</imageView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Food &amp; 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.333333333333329"/>
<rect key="frame" x="0.0" y="66" width="60" height="31.5"/>
<fontDescription key="fontDescription" type="system" pointSize="13"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
@@ -96,7 +94,7 @@
</subviews>
</stackView>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="6" translatesAutoresizingMaskIntoConstraints="NO" id="0vd-sD-XKv">
<rect key="frame" x="95" y="0.0" width="60" height="97.333333333333329"/>
<rect key="frame" x="164" y="0.0" width="60" height="97.5"/>
<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"/>
@@ -106,7 +104,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.333333333333329"/>
<rect key="frame" x="0.0" y="66" width="60" height="31.5"/>
<string key="text">Shopping
</string>
<fontDescription key="fontDescription" type="system" pointSize="13"/>
@@ -116,7 +114,7 @@
</subviews>
</stackView>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="6" translatesAutoresizingMaskIntoConstraints="NO" id="Jd8-YL-b5s">
<rect key="frame" x="190" y="0.0" width="60" height="97.333333333333329"/>
<rect key="frame" x="328" y="0.0" width="60" height="97.5"/>
<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"/>
@@ -126,7 +124,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.333333333333329"/>
<rect key="frame" x="0.0" y="66" width="60" height="31.5"/>
<string key="text">Fun
</string>
<fontDescription key="fontDescription" type="system" pointSize="13"/>
@@ -136,7 +134,7 @@
</subviews>
</stackView>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="6" translatesAutoresizingMaskIntoConstraints="NO" id="dTL-e1-Arz">
<rect key="frame" x="285" y="0.0" width="60" height="97.333333333333329"/>
<rect key="frame" x="492" y="0.0" width="60" height="97.5"/>
<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"/>
@@ -146,7 +144,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.333333333333329"/>
<rect key="frame" x="0.0" y="66" width="60" height="31.5"/>
<string key="text">Travel
</string>
<fontDescription key="fontDescription" type="system" pointSize="13"/>
@@ -168,10 +166,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="166" width="393" height="70"/>
<rect key="frame" x="0.0" y="160.5" width="600" 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="393" height="70"/>
<rect key="frame" x="0.0" y="0.0" width="600" height="70"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="like" translatesAutoresizingMaskIntoConstraints="NO" id="GEk-yE-lLq">
@@ -183,16 +181,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="321" height="46"/>
<rect key="frame" x="57" y="12" width="528" 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="321" height="22"/>
<rect key="frame" x="0.0" y="0.0" width="528" 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="321" height="22"/>
<rect key="frame" x="0.0" y="24" width="528" 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"/>
@@ -257,18 +255,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="393" height="852"/>
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<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="-1" y="-1" width="393" height="852"/>
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<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="393" height="852"/>
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<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="393" height="852"/>
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</tableView>
</subviews>
+12 -13
View File
@@ -156,14 +156,6 @@ 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:
@@ -223,10 +215,17 @@ class SearchPanelLandscapeLayout: FloatingPanelLayout {
.tip: FloatingPanelLayoutAnchor(absoluteInset: 69.0, edge: .bottom, referenceGuide: .safeArea),
]
func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint] {
return [
surfaceView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 8.0),
surfaceView.widthAnchor.constraint(equalToConstant: 291),
]
if #available(iOS 11.0, *) {
return [
surfaceView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 8.0),
surfaceView.widthAnchor.constraint(equalToConstant: 291),
]
} else {
return [
surfaceView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 8.0),
surfaceView.widthAnchor.constraint(equalToConstant: 291),
]
}
}
func backdropAlpha(for state: FloatingPanelState) -> CGFloat {
return 0.0
@@ -320,7 +319,7 @@ class SearchPaneliPadBehavior: FloatingPanelBehavior {
var momentumProjectionRate: CGFloat {
return UIScrollView.DecelerationRate.fast.rawValue
}
func shouldProjectMomentum(_ fpc: FloatingPanelController, to proposedState: FloatingPanelState) -> Bool {
func shouldProjectMomentum(_ fpc: FloatingPanelController, to proposedTargetPosition: FloatingPanelState) -> Bool {
return true
}
}
+30 -4
View File
@@ -81,6 +81,9 @@ 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
@@ -89,6 +92,29 @@ 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
}
@@ -104,16 +130,16 @@ class SearchViewController: UIViewController, UITableViewDataSource {
}
func showHeader(animated: Bool) {
changeHeader(height: 116.0, animated: animated)
changeHeader(height: 116.0, aniamted: animated)
}
func hideHeader(animated: Bool) {
changeHeader(height: 0.0, animated: animated)
changeHeader(height: 0.0, aniamted: animated)
}
private func changeHeader(height: CGFloat, animated: Bool) {
private func changeHeader(height: CGFloat, aniamted: Bool) {
guard let headerView = tableView.tableHeaderView, headerView.bounds.height != height else { return }
if animated == false {
if aniamted == 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 /* TableViewControllerForAdaptiveLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 544BC56726CC918200D0A436 /* TableViewControllerForAdaptiveLayout.swift */; };
544BC56826CC918200D0A436 /* AdaptiveLayoutTestViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 544BC56726CC918200D0A436 /* AdaptiveLayoutTestViewController.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,11 +30,8 @@
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 */
@@ -64,7 +61,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 /* TableViewControllerForAdaptiveLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewControllerForAdaptiveLayout.swift; sourceTree = "<group>"; };
544BC56726CC918200D0A436 /* AdaptiveLayoutTestViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdaptiveLayoutTestViewController.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>"; };
@@ -77,11 +74,8 @@
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 */
@@ -101,18 +95,17 @@
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 */,
54F185832BF4B82E00916F57 /* UnavailableViewController.swift */,
544BC56726CC918200D0A436 /* AdaptiveLayoutTestViewController.swift */,
);
path = ContentViewControllers;
sourceTree = "<group>";
@@ -164,16 +157,6 @@
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 = (
@@ -255,17 +238,15 @@
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 /* TableViewControllerForAdaptiveLayout.swift in Sources */,
544BC56826CC918200D0A436 /* AdaptiveLayoutTestViewController.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 */,
@@ -276,7 +257,6 @@
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="32700.99.1234" 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="21507" 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="22685"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21505"/>
<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"/>
@@ -383,22 +383,22 @@
<objects>
<viewController storyboardIdentifier="ModalViewController" id="bYI-y3-Rzb" customClass="ModalViewController" customModule="Samples" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="qwo-GK-p1U">
<rect key="frame" x="0.0" y="0.0" width="375" height="768"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="720"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="vut-mK-Y4t" customClass="SafeAreaView" customModule="Samples" customModuleProvider="target">
<rect key="frame" x="0.0" y="768" width="375" height="0.0"/>
<rect key="frame" x="0.0" y="720" width="375" height="0.0"/>
<color key="backgroundColor" white="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</view>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="sbF-Az-7sy">
<rect key="frame" x="20" y="0.0" width="39" height="30"/>
<rect key="frame" x="20" y="48" width="39" height="30"/>
<state key="normal" title="Close"/>
<connections>
<action selector="closeWithSender:" destination="bYI-y3-Rzb" eventType="touchUpInside" id="MSC-ch-YJK"/>
</connections>
</button>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="top" spacing="44" translatesAutoresizingMaskIntoConstraints="NO" id="9p4-06-y2T">
<rect key="frame" x="134.5" y="88" width="106" height="326"/>
<rect key="frame" x="134.5" y="136" width="106" height="326"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="i9x-x5-n1q">
<rect key="frame" x="0.0" y="0.0" width="80" height="30"/>
@@ -598,7 +598,7 @@
</constraints>
</view>
<view alpha="0.5" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Kva-Z7-0qY" customClass="OnSafeAreaView" customModule="Samples" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="375" height="744"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="778"/>
<color key="backgroundColor" red="0.0078431372550000003" green="0.72156862749999995" blue="0.45882352939999999" alpha="1" colorSpace="calibratedRGB"/>
</view>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="noi-1a-5bZ" customClass="CloseButton" customModule="Samples" customModuleProvider="target">
@@ -761,33 +761,14 @@ Section 1.10.33 of "de Finibus Bonorum et Malorum", written by Cicero in 45 BC
<fontDescription key="fontDescription" name="CourierNewPSMT" family="Courier New" pointSize="14"/>
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
</textView>
<stackView opaque="NO" contentMode="scaleToFill" distribution="fillProportionally" alignment="center" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="wUo-kb-NIn">
<rect key="frame" x="159" y="16" width="200" height="31"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Expand top margin" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="OC3-od-ldC">
<rect key="frame" x="0.0" y="5.5" width="143" height="20.5"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="XFC-sq-pWj">
<rect key="frame" x="151" y="0.0" width="51" height="31"/>
<connections>
<action selector="toggleTopMargin:" destination="tvD-nO-QUb" eventType="valueChanged" id="XWo-eX-0Jn"/>
</connections>
</switch>
</subviews>
</stackView>
</subviews>
<viewLayoutGuide key="safeArea" id="5ET-zC-lCb"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="rN1-HL-YHv" firstAttribute="leading" secondItem="5ET-zC-lCb" secondAttribute="leading" id="7V3-KL-vXd"/>
<constraint firstItem="5ET-zC-lCb" firstAttribute="trailing" secondItem="wUo-kb-NIn" secondAttribute="trailing" constant="16" id="CtG-H5-tAI"/>
<constraint firstAttribute="bottom" secondItem="rN1-HL-YHv" secondAttribute="bottom" id="efD-U5-Tet"/>
<constraint firstItem="rN1-HL-YHv" firstAttribute="top" secondItem="9YG-0j-Zzg" secondAttribute="top" constant="17" id="fiO-LL-nSC"/>
<constraint firstItem="rN1-HL-YHv" firstAttribute="trailing" secondItem="5ET-zC-lCb" secondAttribute="trailing" id="lfg-EE-euw"/>
<constraint firstItem="wUo-kb-NIn" firstAttribute="top" secondItem="9YG-0j-Zzg" secondAttribute="top" constant="16" id="ogC-1W-upw"/>
</constraints>
</view>
<size key="freeformSize" width="375" height="778"/>
@@ -799,12 +780,12 @@ Section 1.10.33 of "de Finibus Bonorum et Malorum", written by Cicero in 45 BC
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="x1h-y1-h8q" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-2.1739130434782612" y="733.92857142857144"/>
<point key="canvasLocation" x="-1" y="734"/>
</scene>
<!--Table View Controller For Adaptive Layout-->
<!--Adaptive Layout Test View Controller-->
<scene sceneID="rDI-lU-wEx">
<objects>
<viewController storyboardIdentifier="TableViewControllerForAdaptiveLayout" id="5nC-6E-bXf" customClass="TableViewControllerForAdaptiveLayout" customModule="Samples" customModuleProvider="target" sceneMemberID="viewController">
<viewController storyboardIdentifier="AdaptiveLayoutTestViewController" id="5nC-6E-bXf" customClass="AdaptiveLayoutTestViewController" 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 +842,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.58431372550000005" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color red="1" green="0.58431372549019611" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor>
<systemColor name="systemPurpleColor">
<color red="0.68627450980000004" green="0.32156862749999998" blue="0.87058823529999996" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color red="0.68627450980392157" green="0.32156862745098042" blue="0.87058823529411766" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor>
<systemColor name="systemTealColor">
<color red="0.18823529410000001" green="0.69019607839999997" blue="0.78039215689999997" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color red="0.18823529411764706" green="0.69019607843137254" blue="0.7803921568627451" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor>
</resources>
</document>
@@ -1,175 +0,0 @@
// 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)
}
}
}
@@ -3,7 +3,7 @@
import UIKit
import FloatingPanel
final class TableViewControllerForAdaptiveLayout: UIViewController, UITableViewDataSource, UITableViewDelegate {
final class AdaptiveLayoutTestViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
class PanelLayout: FloatingPanelLayout {
let position: FloatingPanelPosition = .bottom
let initialState: FloatingPanelState = .full
@@ -75,6 +75,7 @@ final class TableViewControllerForAdaptiveLayout: UIViewController, UITableViewD
}
class IntrinsicTableView: UITableView {
override var contentSize:CGSize {
didSet {
invalidateIntrinsicContentSize()
@@ -1,77 +0,0 @@
// 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,7 +11,9 @@ final class DebugTextViewController: UIViewController, UITextViewDelegate {
textView.delegate = self
print("viewDidLoad: TextView --- ", textView.contentOffset, textView.contentInset)
textView.contentInsetAdjustmentBehavior = .never
if #available(iOS 11.0, *) {
textView.contentInsetAdjustmentBehavior = .never
}
}
override func viewWillLayoutSubviews() {
@@ -30,14 +32,8 @@ final class DebugTextViewController: UIViewController, UITextViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
print("TextView --- ", scrollView.contentOffset, scrollView.contentInset)
print("TextView --- ", scrollView.adjustedContentInset)
}
@IBAction func toggleTopMargin(_ sender: UISwitch) {
if sender.isOn {
textViewTopConstraint.constant = 160
} else {
textViewTopConstraint.constant = 16
if #available(iOS 11.0, *) {
print("TextView --- ", scrollView.adjustedContentInset)
}
}
@@ -37,6 +37,7 @@ final class ImageViewController: UIViewController {
case withHeaderFooter
}
@available(iOS 11.0, *)
func layoutGuideFor(mode: Mode) -> UILayoutGuide {
switch mode {
case .onlyImage:
@@ -14,17 +14,21 @@ final class SettingsViewController: InspectableViewController {
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let prefersLargeTitles = navigationController!.navigationBar.prefersLargeTitles
largeTitlesSwitch.setOn(prefersLargeTitles, animated: false)
if #available(iOS 11.0, *) {
let prefersLargeTitles = navigationController!.navigationBar.prefersLargeTitles
largeTitlesSwitch.setOn(prefersLargeTitles, animated: false)
} else {
largeTitlesSwitch.isEnabled = false
}
let isTranslucent = navigationController!.navigationBar.isTranslucent
translucentSwitch.setOn(isTranslucent, animated: false)
}
@IBAction func toggleLargeTitle(_ sender: UISwitch) {
navigationController?.navigationBar.prefersLargeTitles = sender.isOn
if #available(iOS 11.0, *) {
navigationController?.navigationBar.prefersLargeTitles = sender.isOn
}
}
@IBAction func toggleTranslucent(_ sender: UISwitch) {
// White non-translucent navigation bar, supports dark appearance
if #available(iOS 15, *) {
@@ -238,8 +238,13 @@ class ThreeTabBarPanelLayout: FloatingPanelLayout {
}
func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint] {
leftConstraint = surfaceView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 0.0)
rightConstraint = surfaceView.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor, constant: 0.0)
if #available(iOS 11.0, *) {
leftConstraint = surfaceView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 0.0)
rightConstraint = surfaceView.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor, constant: 0.0)
} else {
leftConstraint = surfaceView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 0.0)
rightConstraint = surfaceView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 0.0)
}
return [ leftConstraint, rightConstraint ]
}
}
@@ -1,24 +0,0 @@
// 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
}
}
+14 -2
View File
@@ -33,10 +33,22 @@ class CustomLayoutGuide: LayoutGuideProvider {
extension UIViewController {
var layoutInsets: UIEdgeInsets {
return view.safeAreaInsets
if #available(iOS 11.0, *) {
return view.safeAreaInsets
} else {
return UIEdgeInsets(top: topLayoutGuide.length,
left: 0.0,
bottom: bottomLayoutGuide.length,
right: 0.0)
}
}
var layoutGuide: LayoutGuideProvider {
return view.safeAreaLayoutGuide
if #available(iOS 11.0, *) {
return view!.safeAreaLayoutGuide
} else {
return CustomLayoutGuide(topAnchor: topLayoutGuide.bottomAnchor,
bottomAnchor: bottomLayoutGuide.topAnchor)
}
}
}
@@ -15,11 +15,14 @@ extension MainViewController {
tableView.dataSource = self
tableView.delegate = self
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
automaticallyAdjustsScrollViewInsets = false
let searchController = UISearchController(searchResultsController: nil)
navigationItem.searchController = searchController
navigationItem.hidesSearchBarWhenScrolling = false
navigationItem.largeTitleDisplayMode = .automatic
if #available(iOS 11.0, *) {
navigationItem.searchController = searchController
navigationItem.hidesSearchBarWhenScrolling = false
navigationItem.largeTitleDisplayMode = .automatic
}
var insets = UIEdgeInsets.zero
insets.bottom += 69.0
tableView.contentInset = insets
@@ -30,10 +33,12 @@ extension MainViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if let observation = navigationController?.navigationBar.observe(\.prefersLargeTitles, changeHandler: { (bar, _) in
self.tableView.reloadData()
}) {
observations.append(observation)
if #available(iOS 11.0, *) {
if let observation = navigationController?.navigationBar.observe(\.prefersLargeTitles, changeHandler: { (bar, _) in
self.tableView.reloadData()
}) {
observations.append(observation)
}
}
}
@@ -51,8 +56,12 @@ extension MainViewController {
extension MainViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if navigationController?.navigationBar.prefersLargeTitles == true {
return UseCase.allCases.count + 30
if #available(iOS 11.0, *) {
if navigationController?.navigationBar.prefersLargeTitles == true {
return UseCase.allCases.count + 30
} else {
return UseCase.allCases.count
}
} else {
return UseCase.allCases.count
}
@@ -5,13 +5,11 @@ import UIKit
enum UseCase: Int, CaseIterable {
case trackingTableView
case trackingTextView
case trackingCollectionViewList
case showDetail
case showModal
case showPanelModal
case showMultiPanelModal
case showPanelInSheetModal
case showOnWindow
case showTabBar
case showPageView
case showPageContentView
@@ -23,24 +21,19 @@ enum UseCase: Int, CaseIterable {
case showNavigationController
case showTopPositionedPanel
case showAdaptivePanel
case showAdaptivePanelWithTableView
case showAdaptivePanelWithCollectionView
case showAdaptivePanelWithCompositionalCollectionView
case showAdaptivePanelWithCustomGuide
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"
case .showPanelModal: return "Show Panel Modal"
case .showMultiPanelModal: return "Show Multi Panel Modal"
case .showOnWindow: return "Show Panel over Window"
case .showPanelInSheetModal: return "Show Panel in Sheet Modal"
case .showTabBar: return "Show Tab Bar"
case .showPageView: return "Show Page View"
@@ -53,11 +46,8 @@ extension UseCase {
case .showNavigationController: return "Show Navigation Controller"
case .showTopPositionedPanel: return "Show Top Positioned Panel"
case .showAdaptivePanel: return "Show Adaptive Panel"
case .showAdaptivePanelWithTableView: return "Show Adaptive Panel (TableView)"
case .showAdaptivePanelWithCollectionView: return "Show Adaptive Panel (CollectionView)"
case .showAdaptivePanelWithCompositionalCollectionView: return "Show Adaptive Panel (Compositional CollectionView)"
case .showAdaptivePanelWithCustomGuide: return "Show Adaptive Panel (Custom Layout Guide)"
case .showCustomStatePanel: return "Show Panel with Custom state"
case .showCustomBackdrop: return "Show Panel with Custom Backdrop"
}
}
}
@@ -71,18 +61,10 @@ 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))
case .showMultiPanelModal: return .viewController(DebugTableViewController())
case .showOnWindow: return .viewController(DebugTableViewController())
case .showPanelInSheetModal: return .viewController(DebugTableViewController())
case .showPanelModal: return .viewController(DebugTableViewController())
case .showTabBar: return .storyboard(String(describing: TabBarViewController.self))
@@ -96,19 +78,8 @@ extension UseCase {
case .showNavigationController: return .storyboard("RootNavigationController") // Storyboard only
case .showTopPositionedPanel: return .viewController(DebugTableViewController())
case .showAdaptivePanel: return .storyboard(String(describing: ImageViewController.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 .showAdaptivePanelWithCustomGuide: return .storyboard(String(describing: AdaptiveLayoutTestViewController.self))
case .showCustomStatePanel: return .viewController(DebugTableViewController())
case .showCustomBackdrop: return .viewController(UIViewController())
}
}
@@ -121,11 +92,4 @@ extension UseCase {
return vc
}
}
private func makeUnavailableViewContent(message: String) -> Content {
let vc = UnavailableViewController()
vc.loadViewIfNeeded()
vc.label.text = message
return .viewController(vc)
}
}
@@ -11,7 +11,6 @@ final class UseCaseController: NSObject {
private var detailPanelVC: FloatingPanelController!
private var settingsPanelVC: FloatingPanelController!
private lazy var pagePanelController = PagePanelController()
private lazy var overWindowPanelVC = FloatingPanelController()
init(mainVC: MainViewController) {
self.mainVC = mainVC
@@ -52,30 +51,6 @@ 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
@@ -182,20 +157,6 @@ extension UseCaseController {
let fpc = MultiPanelController()
mainVC.present(fpc, animated: true, completion: nil)
case .showOnWindow:
let fpc = overWindowPanelVC
fpc.backdropView.dismissalTapGestureRecognizer.isEnabled = true
fpc.isRemovalInteractionEnabled = true
fpc.set(contentViewController: contentVC)
fpc.ext_trackScrollView(in: contentVC)
guard let window = UIApplication.shared.windows.first else { fatalError("Any window not found") }
window.addSubview(fpc.view)
fpc.view.frame = window.bounds
fpc.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
fpc.show(animated: true)
case .showPanelInSheetModal:
let fpc = FloatingPanelController()
let contentVC = UIViewController()
@@ -263,13 +224,13 @@ extension UseCaseController {
fpc.set(contentViewController: contentVC)
fpc.ext_trackScrollView(in: contentVC)
if case let contentVC as ImageViewController = contentVC {
let mode: ImageViewController.Mode = (useCase == .showAdaptivePanelWithTableView) ? .withHeaderFooter : .onlyImage
let mode: ImageViewController.Mode = (useCase == .showAdaptivePanelWithCustomGuide) ? .withHeaderFooter : .onlyImage
let layoutGuide = contentVC.layoutGuideFor(mode: mode)
fpc.layout = ImageViewController.PanelLayout(targetGuide: layoutGuide)
}
addMain(panel: fpc)
case .showAdaptivePanelWithTableView:
case .showAdaptivePanelWithCustomGuide:
let fpc = FloatingPanelController()
fpc.isRemovalInteractionEnabled = true
fpc.contentInsetAdjustmentBehavior = .always
@@ -279,25 +240,10 @@ extension UseCaseController {
return appearance
}()
fpc.set(contentViewController: contentVC)
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())
}
fpc.ext_trackScrollView(in: contentVC)
fpc.layout = AdaptiveLayoutTestViewController.PanelLayout(targetGuide: contentVC.view.makeBoundsLayoutGuide())
addMain(panel: fpc)
case .showCustomStatePanel:
@@ -312,52 +258,6 @@ 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)
}
}
@@ -407,16 +307,8 @@ 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 {
if useCase == .showNavigationController, #available(iOS 11.0, *) {
// 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)
}
@@ -497,7 +389,7 @@ private extension FloatingPanelController {
case let contentVC as ImageViewController:
track(scrollView: contentVC.scrollView)
case let contentVC as TableViewControllerForAdaptiveLayout:
case let contentVC as AdaptiveLayoutTestViewController:
track(scrollView: contentVC.tableView)
default:
@@ -15,6 +15,7 @@
545BA71421BA3217007F7846 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 545BA71321BA3217007F7846 /* main.m */; };
545BA72621BA3BAF007F7846 /* FloatingPanel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 545BA72521BA3BAF007F7846 /* FloatingPanel.framework */; };
545BA72721BA3BAF007F7846 /* FloatingPanel.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 545BA72521BA3BAF007F7846 /* FloatingPanel.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
5D82A6B028D18447006A44BA /* libswiftCoreGraphics.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 5D82A6AF28D18443006A44BA /* libswiftCoreGraphics.tbd */; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
@@ -52,6 +53,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
5D82A6B028D18447006A44BA /* libswiftCoreGraphics.tbd in Frameworks */,
545BA72621BA3BAF007F7846 /* FloatingPanel.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -81,7 +81,7 @@ class MainViewController: UIViewController, FloatingPanelControllerDelegate {
}
private func hideStockTickerBanner() {
// Dismiss top bar with dissolve animation
// Dimiss top bar with dissolve animation
UIView.animate(withDuration: 0.25) {
self.topBannerView.alpha = 0.0
self.labelStackView.alpha = 1.0
+4 -4
View File
@@ -1,18 +1,18 @@
Pod::Spec.new do |s|
s.name = "FloatingPanel"
s.version = "2.8.3"
s.version = "2.6.2"
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, "11.0"
s.source = { :git => "https://github.com/scenee/FloatingPanel.git", :tag => s.version.to_s }
s.platform = :ios, "10.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'
+25 -21
View File
@@ -21,12 +21,11 @@
5469F4B024B30E1500537F8A /* LayoutAnchoring.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5469F4AF24B30E1500537F8A /* LayoutAnchoring.swift */; };
5469F4B224B30F1100537F8A /* Position.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5469F4B124B30F1100537F8A /* Position.swift */; };
5469F4B424B30F3500537F8A /* LayoutProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5469F4B324B30F3500537F8A /* LayoutProperties.swift */; };
547F7A9C2A6E946000303905 /* GestureTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 547F7A9B2A6E946000303905 /* GestureTests.swift */; };
549C371F2361E15E007D8058 /* ExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 549C371E2361E15D007D8058 /* ExtensionTests.swift */; };
549E944522CF295D0050AECF /* StateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 549E944422CF295D0050AECF /* StateTests.swift */; };
54A6B6B122968B530077F348 /* CoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54A6B6B022968B530077F348 /* CoreTests.swift */; };
54A6B6B82296A8520077F348 /* SurfaceViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54A6B6B72296A8520077F348 /* SurfaceViewTests.swift */; };
54ABD7AF216CCFF7002E6C13 /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54ABD7AE216CCFF7002E6C13 /* Logging.swift */; };
54ABD7AF216CCFF7002E6C13 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54ABD7AE216CCFF7002E6C13 /* Logger.swift */; };
54CDC5D3215B6D5A007D205C /* SurfaceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CDC5D2215B6D5A007D205C /* SurfaceView.swift */; };
54CDC5D5215B6D8D007D205C /* BackdropView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CDC5D4215B6D8D007D205C /* BackdropView.swift */; };
54CFBFC3215CD045006B5735 /* Layout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CFBFC2215CD045006B5735 /* Layout.swift */; };
@@ -64,12 +63,11 @@
5469F4AF24B30E1500537F8A /* LayoutAnchoring.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutAnchoring.swift; sourceTree = "<group>"; };
5469F4B124B30F1100537F8A /* Position.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Position.swift; sourceTree = "<group>"; };
5469F4B324B30F3500537F8A /* LayoutProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutProperties.swift; sourceTree = "<group>"; };
547F7A9B2A6E946000303905 /* GestureTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GestureTests.swift; sourceTree = "<group>"; };
549C371E2361E15D007D8058 /* ExtensionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionTests.swift; sourceTree = "<group>"; };
549E944422CF295D0050AECF /* StateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateTests.swift; sourceTree = "<group>"; };
54A6B6B022968B530077F348 /* CoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreTests.swift; sourceTree = "<group>"; };
54A6B6B72296A8520077F348 /* SurfaceViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SurfaceViewTests.swift; sourceTree = "<group>"; };
54ABD7AE216CCFF7002E6C13 /* Logging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logging.swift; sourceTree = "<group>"; };
54ABD7AE216CCFF7002E6C13 /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; };
54CDC5D2215B6D5A007D205C /* SurfaceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SurfaceView.swift; sourceTree = "<group>"; };
54CDC5D4215B6D8D007D205C /* BackdropView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackdropView.swift; sourceTree = "<group>"; };
54CFBFC2215CD045006B5735 /* Layout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Layout.swift; sourceTree = "<group>"; };
@@ -135,7 +133,7 @@
545DBA2A2152383100CA77B8 /* GrabberView.swift */,
54352E9521A51A2500CBCA08 /* Transitioning.swift */,
54DBA3DB262E938500D75969 /* Extensions.swift */,
54ABD7AE216CCFF7002E6C13 /* Logging.swift */,
54ABD7AE216CCFF7002E6C13 /* Logger.swift */,
545DB9C42151169500CA77B8 /* FloatingPanel.h */,
545DB9C52151169500CA77B8 /* Info.plist */,
54E3992627141F5100A8F9ED /* FloatingPanel.docc */,
@@ -148,7 +146,6 @@
children = (
54A6B6B022968B530077F348 /* CoreTests.swift */,
545DB9CF2151169500CA77B8 /* ControllerTests.swift */,
547F7A9B2A6E946000303905 /* GestureTests.swift */,
542753C522C49A6E00D17955 /* LayoutTests.swift */,
54A6B6B72296A8520077F348 /* SurfaceViewTests.swift */,
549E944422CF295D0050AECF /* StateTests.swift */,
@@ -193,6 +190,7 @@
buildRules = (
);
dependencies = (
54B58FC929EB95880009567E /* PBXTargetDependency */,
);
name = FloatingPanel;
productName = FloatingModalController;
@@ -245,6 +243,8 @@
Base,
);
mainGroup = 545DB9B72151169500CA77B8;
packageReferences = (
);
productRefGroup = 545DB9C22151169500CA77B8 /* Products */;
projectDirPath = "";
projectRoot = "";
@@ -285,7 +285,7 @@
54CDC5D5215B6D8D007D205C /* BackdropView.swift in Sources */,
54352E9821A521CA00CBCA08 /* PassthroughView.swift in Sources */,
54CFBFC5215CD09C006B5735 /* Core.swift in Sources */,
54ABD7AF216CCFF7002E6C13 /* Logging.swift in Sources */,
54ABD7AF216CCFF7002E6C13 /* Logger.swift in Sources */,
545DB9E021511AC100CA77B8 /* Controller.swift in Sources */,
54DBA3DC262E938500D75969 /* Extensions.swift in Sources */,
5450EEE421646DF500135936 /* Behavior.swift in Sources */,
@@ -307,7 +307,6 @@
542753C622C49A6E00D17955 /* LayoutTests.swift in Sources */,
54A6B6B82296A8520077F348 /* SurfaceViewTests.swift in Sources */,
546055BF2333C4740069F400 /* TestSupports.swift in Sources */,
547F7A9C2A6E946000303905 /* GestureTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -319,6 +318,10 @@
target = 545DB9C02151169500CA77B8 /* FloatingPanel */;
targetProxy = 545DB9CC2151169500CA77B8 /* PBXContainerItemProxy */;
};
54B58FC929EB95880009567E /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
productRef = 54B58FC829EB95880009567E /* SwiftFormatBuildTool */;
};
/* End PBXTargetDependency section */
/* Begin XCBuildConfiguration section */
@@ -462,13 +465,12 @@
DYLIB_INSTALL_NAME_BASE = "@rpath";
INFOPLIST_FILE = Sources/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
IPHONEOS_DEPLOYMENT_TARGET = 10.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;
@@ -494,13 +496,12 @@
DYLIB_INSTALL_NAME_BASE = "@rpath";
INFOPLIST_FILE = Sources/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
IPHONEOS_DEPLOYMENT_TARGET = 10.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;
@@ -517,13 +518,12 @@
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = "";
INFOPLIST_FILE = Tests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@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;
@@ -538,13 +538,12 @@
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = "";
INFOPLIST_FILE = Tests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@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;
@@ -631,17 +630,16 @@
DYLIB_INSTALL_NAME_BASE = "@rpath";
INFOPLIST_FILE = Sources/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
IPHONEOS_DEPLOYMENT_TARGET = 10.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;
@@ -656,13 +654,12 @@
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = "";
INFOPLIST_FILE = Tests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@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;
@@ -704,6 +701,13 @@
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
/* Begin XCSwiftPackageProductDependency section */
54B58FC829EB95880009567E /* SwiftFormatBuildTool */ = {
isa = XCSwiftPackageProductDependency;
productName = "plugin:SwiftFormatBuildTool";
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 545DB9B82151169500CA77B8 /* Project object */;
}
+1 -1
View File
@@ -6,7 +6,7 @@ import PackageDescription
let package = Package(
name: "FloatingPanel",
platforms: [
.iOS(.v11)
.iOS(.v10)
],
products: [
// Products define the executables and libraries produced by a package, and make them visible to other packages.
+68 -72
View File
@@ -9,7 +9,7 @@
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@SPI](https://swiftpackageindex.com/scenee/FloatingPanel/2.8.3/documentation/floatingpanel) for more details.
Please see also [the API reference](https://floatingpanel.github.io/2.6.2/documentation/floatingpanel/) for more details, powered by [DocC](https://developer.apple.com/documentation/docc).
![Maps](https://github.com/SCENEE/FloatingPanel/blob/master/assets/maps.gif)
![Stocks](https://github.com/SCENEE/FloatingPanel/blob/master/assets/stocks.gif)
@@ -21,47 +21,47 @@ Please see also [the API reference@SPI](https://swiftpackageindex.com/scenee/Flo
- [Features](#features)
- [Requirements](#requirements)
- [Installation](#installation)
- [CocoaPods](#cocoapods)
- [Carthage](#carthage)
- [Swift Package Manager](#swift-package-manager)
- [CocoaPods](#cocoapods)
- [Carthage](#carthage)
- [Swift Package Manager](#swift-package-manager)
- [Getting Started](#getting-started)
- [Add a floating panel as a child view controller](#add-a-floating-panel-as-a-child-view-controller)
- [Present a floating panel as a modality](#present-a-floating-panel-as-a-modality)
- [Add a floating panel as a child view controller](#add-a-floating-panel-as-a-child-view-controller)
- [Present a floating panel as a modality](#present-a-floating-panel-as-a-modality)
- [View hierarchy](#view-hierarchy)
- [Usage](#usage)
- [Show/Hide a floating panel in a view with your view hierarchy](#showhide-a-floating-panel-in-a-view-with-your-view-hierarchy)
- [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)
- [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)
- [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)
- ['Show' or 'Show Detail' Segues from `FloatingPanelController`'s content view controller](#show-or-show-detail-segues-from-floatingpanelcontrollers-content-view-controller)
- [UISearchController issue](#uisearchcontroller-issue)
- [FloatingPanelSurfaceView's issue on iOS 10](#floatingpanelsurfaceviews-issue-on-ios-10)
- [Maintainer](#maintainer)
- [License](#license)
@@ -93,6 +93,10 @@ 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
@@ -104,6 +108,8 @@ 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`:
@@ -162,8 +168,7 @@ self.present(fpc, animated: true, completion: nil)
You can show a floating panel over UINavigationController from the container view controllers as a modality of `.overCurrentContext` style.
> [!NOTE]
> FloatingPanelController has the custom presentation controller. If you would like to customize the presentation/dismissal, please see [Transitioning](https://github.com/SCENEE/FloatingPanel/blob/master/Sources/Transitioning.swift).
: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).
## View hierarchy
@@ -247,8 +252,7 @@ fpc.contentMode = .fitToBounds
Otherwise, `FloatingPanelController` fixes the content by the height of the top most position.
> [!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.
:pencil2: In `.fitToBounds` mode, the surface height changes as following a user interaction so that you have a responsibility to configure Auto Layout constrains not to break the layout of a content view by the elastic surface height.
### Customize the layout with `FloatingPanelLayout` protocol
@@ -346,8 +350,7 @@ class IntrinsicPanelLayout: FloatingPanelLayout {
}
```
> [!WARNING]
> `FloatingPanelIntrinsicLayout` is deprecated on v1.
:pencil2: `FloatingPanelIntrinsicLayout` is deprecated on v1.
#### Specify an anchor for each state by an inset of the `FloatingPanelController.view` frame
@@ -364,8 +367,7 @@ class MyFullScreenLayout: FloatingPanelLayout {
}
```
> [!WARNING]
> `FloatingPanelFullScreenLayout` is deprecated on v1.
:pencil2: `FloatingPanelFullScreenLayout` is deprecated on v1.
#### Change the backdrop alpha
@@ -423,14 +425,13 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate {
class CustomPanelBehavior: FloatingPanelBehavior {
let springDecelerationRate = UIScrollView.DecelerationRate.fast.rawValue + 0.02
let springResponseTime = 0.4
func shouldProjectMomentum(_ fpc: FloatingPanelController, to proposedState: FloatingPanelState) -> Bool {
func shouldProjectMomentum(_ fpc: FloatingPanelController, to proposedTargetPosition: FloatingPanelState) -> Bool {
return true
}
}
```
> [!WARNING]
> `floatingPanel(_ vc:behaviorFor:)` is deprecated on v1.
:pencil2: `floatingPanel(_ vc:behaviorFor:)` is deprecated on v1.
#### Activate the rubber-band effect on panel edges
@@ -450,7 +451,7 @@ This allows full projectional panel behavior. For example, a user can swipe up a
```swift
class MyPanelBehavior: FloatingPanelBehavior {
...
func shouldProjectMomentum(_ fpc: FloatingPanelController, to proposedState: FloatingPanelPosition) -> Bool {
func shouldProjectMomentum(_ fpc: FloatingPanelController, to proposedTargetPosition: FloatingPanelPosition) -> Bool {
return true
}
}
@@ -472,8 +473,7 @@ func floatingPanelDidMove(_ vc: FloatingPanelController) {
}
```
> [!WARNING]
> `{top,bottom}InteractionBuffer` property is removed from `FloatingPanelLayout` since v2.
:pencil2: `{top,bottom}InteractionBuffer` property is removed from `FloatingPanelLayout` since v2.
### Customize the surface design
@@ -514,8 +514,7 @@ fpc.surfaceView.grabberHandlePadding = 10.0
fpc.surfaceView.grabberHandleSize = .init(width: 44.0, height: 12.0)
```
> [!NOTE]
> `grabberHandleSize` width and height are reversed in the left/right position.
:pencil2: Note that `grabberHandleSize` width and height are reversed in the left/right position.
#### Customize content padding from surface edges
@@ -664,24 +663,6 @@ 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
@@ -722,6 +703,21 @@ 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)
+3 -3
View File
@@ -4,7 +4,7 @@ import UIKit
/// A view that presents a backdrop interface behind a panel.
@objc(FloatingPanelBackdropView)
open class BackdropView: UIView {
public class BackdropView: UIView {
/// The gesture recognizer for tap gestures to dismiss a panel.
///
@@ -12,14 +12,14 @@ open class BackdropView: UIView {
/// To dismiss a panel by tap gestures on the backdrop, `dismissalTapGestureRecognizer.isEnabled` is set to true.
@objc public var dismissalTapGestureRecognizer: UITapGestureRecognizer
public init() {
init() {
dismissalTapGestureRecognizer = UITapGestureRecognizer()
dismissalTapGestureRecognizer.isEnabled = false
super.init(frame: .zero)
addGestureRecognizer(dismissalTapGestureRecognizer)
}
required public init?(coder: NSCoder) {
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
+22 -26
View File
@@ -12,47 +12,43 @@ public protocol FloatingPanelBehavior {
/// When this value is between 0.978 and 1.0, it uses a underdamped spring system with a damping ratio computed by
/// this value. You shouldn't return less than 0.979 because the system is overdamped. If the pan gesture's velocity
/// is less than 300, this value is ignored and a panel applies a critically damped system.
@objc optional
var springDecelerationRate: CGFloat { get }
@objc
optional var springDecelerationRate: CGFloat { get }
/// A floating-point value that determines the approximate time until a panel stops to an anchor after the user lifts their finger.
@objc optional
var springResponseTime: CGFloat { get }
@objc
optional var springResponseTime: CGFloat { get }
/// Returns a deceleration rate to calculate a target position projected a dragging momentum.
///
/// The default implementation of this method returns the normal deceleration rate of UIScrollView.
@objc optional
var momentumProjectionRate: CGFloat { get }
@objc
optional var momentumProjectionRate: CGFloat { get }
/// Asks the behavior if a panel should project a momentum of a user interaction to move the
/// proposed state.
/// Asks the behavior if a panel should project a momentum of a user interaction to move the proposed position.
///
/// The default implementation of this method returns `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
/// The default implementation of this method returns true. This method is called for a layout to support all positions(tip, half and full).
/// Therefore, `proposedTargetPosition` can only be `FloatingPanelState.tip` or `FloatingPanelState.full`.
@objc
optional func shouldProjectMomentum(_ fpc: FloatingPanelController, to proposedTargetPosition: FloatingPanelState) -> Bool
/// Returns the progress to redirect to the previous position.
///
/// The progress is represented by a floating-point value between 0.0 and 1.0, inclusive, where 1.0 indicates a panel is impossible to move to the next position. The default value is 0.5. Values less than 0.0 and greater than 1.0 are pinned to those limits.
@objc optional
func redirectionalProgress(_ fpc: FloatingPanelController, from: FloatingPanelState, to: FloatingPanelState) -> CGFloat
@objc
optional func redirectionalProgress(_ fpc: FloatingPanelController, from: FloatingPanelState, to: FloatingPanelState) -> CGFloat
/// Asks the behavior whether the rubber band effect is enabled in moving over a given edge of the surface view.
///
/// This method allows a panel to activate the rubber band effect to a given edge of the surface view. By default, the effect is disabled.
@objc optional
func allowsRubberBanding(for edge: UIRectEdge) -> Bool
@objc
optional func allowsRubberBanding(for edge: UIRectEdge) -> Bool
/// Returns the velocity threshold for the default interactive removal gesture.
///
/// In case ``FloatingPanel/FloatingPanelControllerDelegate/floatingPanel(_:shouldRemoveAt:with:)`` is implemented, this value will not be used. The default value of ``FloatingPanelDefaultBehavior`` is 5.5
@objc optional
var removalInteractionVelocityThreshold: CGFloat { get }
@objc
optional var removalInteractionVelocityThreshold: CGFloat { get }
}
/// The default behavior object for a panel
@@ -80,7 +76,7 @@ open class FloatingPanelDefaultBehavior: FloatingPanelBehavior {
open func allowsRubberBanding(for edge: UIRectEdge) -> Bool {
return false
}
open var removalInteractionVelocityThreshold: CGFloat = 5.5
}
@@ -104,13 +100,13 @@ class BehaviorAdapter {
var momentumProjectionRate: CGFloat {
behavior.momentumProjectionRate ?? FloatingPanelDefaultBehavior().momentumProjectionRate
}
var removalInteractionVelocityThreshold: CGFloat {
behavior.removalInteractionVelocityThreshold ?? FloatingPanelDefaultBehavior().removalInteractionVelocityThreshold
}
func redirectionalProgress(from: FloatingPanelState, to: FloatingPanelState) -> CGFloat {
behavior.redirectionalProgress?(vc, from: from, to: to) ?? FloatingPanelDefaultBehavior().redirectionalProgress(vc,from: from, to: to)
behavior.redirectionalProgress?(vc, from: from, to: to) ?? FloatingPanelDefaultBehavior().redirectionalProgress(vc, from: from, to: to)
}
func shouldProjectMomentum(to: FloatingPanelState) -> Bool {
@@ -125,6 +121,6 @@ class BehaviorAdapter {
extension FloatingPanelController {
var _behavior: FloatingPanelBehavior {
get { floatingPanel.behaviorAdapter.behavior }
set { floatingPanel.behaviorAdapter.behavior = newValue}
set { floatingPanel.behaviorAdapter.behavior = newValue }
}
}
+114 -142
View File
@@ -1,90 +1,82 @@
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
import UIKit
import os.log
/// A set of methods implemented by the delegate of a panel controller allows the adopting delegate to respond to
/// messages from the FloatingPanelController class and thus respond to, and in some affect, operations such as
/// dragging, attracting a panel, layout of a panel and the content, and transition animations.
@objc public protocol FloatingPanelControllerDelegate {
/// Returns a FloatingPanelLayout object. If you use the default one, you can use a `FloatingPanelBottomLayout` object.
@objc(floatingPanel:layoutForTraitCollection:) optional
func floatingPanel(_ fpc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout
@objc(floatingPanel:layoutForTraitCollection:)
optional func floatingPanel(_ fpc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout
/// Returns a FloatingPanelLayout object. If you use the default one, you can use a `FloatingPanelBottomLayout` object.
@objc(floatingPanel:layoutForSize:) optional
func floatingPanel(_ fpc: FloatingPanelController, layoutFor size: CGSize) -> FloatingPanelLayout
@objc(floatingPanel:layoutForSize:)
optional func floatingPanel(_ fpc: FloatingPanelController, layoutFor size: CGSize) -> FloatingPanelLayout
/// Returns a UIViewPropertyAnimator object to add/present the panel to a position.
///
/// Default is the spring animation with 0.25 secs.
@objc(floatingPanel:animatorForPresentingToState:) optional
func floatingPanel(_ fpc: FloatingPanelController, animatorForPresentingTo state: FloatingPanelState) -> UIViewPropertyAnimator
@objc(floatingPanel:animatorForPresentingToState:)
optional func floatingPanel(_ fpc: FloatingPanelController, animatorForPresentingTo state: FloatingPanelState) -> UIViewPropertyAnimator
/// Returns a UIViewPropertyAnimator object to remove/dismiss a panel from a position.
///
/// Default is the spring animator with 0.25 secs.
@objc(floatingPanel:animatorForDismissingWithVelocity:) optional
func floatingPanel(_ fpc: FloatingPanelController, animatorForDismissingWith velocity: CGVector) -> UIViewPropertyAnimator
@objc(floatingPanel:animatorForDismissingWithVelocity:)
optional func floatingPanel(_ fpc: FloatingPanelController, animatorForDismissingWith velocity: CGVector) -> UIViewPropertyAnimator
/// Called when a panel has changed to a new state.
///
/// This can be called inside an animation block for presenting, dismissing a panel or moving a panel with your
/// animation. So any view properties set inside this function will be automatically animated alongside a panel.
@objc optional
func floatingPanelDidChangeState(_ fpc: FloatingPanelController)
@objc
optional func floatingPanelDidChangeState(_ fpc: FloatingPanelController)
/// Asks the delegate if dragging should begin by the pan gesture recognizer.
@objc optional
func floatingPanelShouldBeginDragging(_ fpc: FloatingPanelController) -> Bool
@objc
optional func floatingPanelShouldBeginDragging(_ fpc: FloatingPanelController) -> Bool
/// Called while the user drags the surface or the surface moves to a state anchor.
@objc optional
func floatingPanelDidMove(_ fpc: FloatingPanelController)
@objc
optional func floatingPanelDidMove(_ fpc: FloatingPanelController)
/// Called on start of dragging (may require some time and or distance to move)
@objc optional
func floatingPanelWillBeginDragging(_ fpc: FloatingPanelController)
@objc
optional func floatingPanelWillBeginDragging(_ fpc: FloatingPanelController)
/// Called on finger up if the user dragged. velocity is in points/second.
@objc optional
func floatingPanelWillEndDragging(_ fpc: FloatingPanelController, withVelocity velocity: CGPoint, targetState: UnsafeMutablePointer<FloatingPanelState>)
@objc
optional func floatingPanelWillEndDragging(_ fpc: FloatingPanelController, withVelocity velocity: CGPoint, targetState: UnsafeMutablePointer<FloatingPanelState>)
/// Called on finger up if the user dragged.
///
/// If `attract` is true, the panel continues moving towards the nearby state
/// anchor. Otherwise, it stops at the closest state anchor.
///
/// - Note: If `attract` is false, ``FloatingPanelController.state`` property has
/// already changed to the closest anchor's state by the time this delegate method
/// is called.
@objc optional
func floatingPanelDidEndDragging(_ fpc: FloatingPanelController, willAttract attract: Bool)
/// If `attract` is true, it will continue moving afterwards to a nearby state anchor.
@objc
optional func floatingPanelDidEndDragging(_ fpc: FloatingPanelController, willAttract attract: Bool)
/// Called when it is about to be attracted to a state anchor.
@objc optional
func floatingPanelWillBeginAttracting(_ fpc: FloatingPanelController, to state: FloatingPanelState) // called on finger up as a panel are moving
@objc
optional func floatingPanelWillBeginAttracting(_ fpc: FloatingPanelController, to state: FloatingPanelState) // called on finger up as a panel are moving
/// Called when attracting it is completed.
@objc optional
func floatingPanelDidEndAttracting(_ fpc: FloatingPanelController) // called when a panel stops
@objc
optional func floatingPanelDidEndAttracting(_ fpc: FloatingPanelController) // called when a panel stops
/// Asks the delegate whether a panel should be removed when dragging ended at the specified location
///
/// This delegate method is called only where ``FloatingPanel/FloatingPanelController/isRemovalInteractionEnabled`` is `true`.
/// The velocity vector is calculated from the distance to a point of the hidden state and the pan gesture's velocity.
@objc(floatingPanel:shouldRemoveAtLocation:withVelocity:)
optional
func floatingPanel(_ fpc: FloatingPanelController, shouldRemoveAt location: CGPoint, with velocity: CGVector) -> Bool
optional func floatingPanel(_ fpc: FloatingPanelController, shouldRemoveAt location: CGPoint, with velocity: CGVector) -> Bool
/// Called on start to remove its view controller from the parent view controller.
@objc(floatingPanelWillRemove:)
optional
func floatingPanelWillRemove(_ fpc: FloatingPanelController)
optional func floatingPanelWillRemove(_ fpc: FloatingPanelController)
/// Called when a panel is removed from the parent view controller.
@objc optional
func floatingPanelDidRemove(_ fpc: FloatingPanelController)
@objc
optional func floatingPanelDidRemove(_ fpc: FloatingPanelController)
/// Asks the delegate for a content offset of the tracking scroll view to be pinned when a panel moves
///
@@ -94,37 +86,7 @@ import os.log
///
/// This method will not be called if the controller doesn't track any scroll view.
@objc(floatingPanel:contentOffsetForPinningScrollView:)
optional
func floatingPanel(_ fpc: FloatingPanelController, contentOffsetForPinning trackingScrollView: UIScrollView) -> CGPoint
/// 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 doesnt 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
optional func floatingPanel(_ fpc: FloatingPanelController, contentOffsetForPinning trackingScrollView: UIScrollView) -> CGPoint
}
///
@@ -150,23 +112,22 @@ open class FloatingPanelController: UIViewController {
/// The delegate of a panel controller object.
@objc
public weak var delegate: FloatingPanelControllerDelegate?{
didSet{
public weak var delegate: FloatingPanelControllerDelegate? {
didSet {
didUpdateDelegate()
}
}
/// Returns the surface view managed by the controller object. It's the same as `self.view`.
@objc
public var surfaceView: SurfaceView {
public var surfaceView: SurfaceView! {
return floatingPanel.surfaceView
}
/// Returns the backdrop view managed by the controller object.
@objc
public var backdropView: BackdropView {
set { floatingPanel.backdropView = newValue }
get { return floatingPanel.backdropView }
public var backdropView: BackdropView! {
return floatingPanel.backdropView
}
/// Returns the scroll view that the controller tracks.
@@ -203,8 +164,7 @@ open class FloatingPanelController: UIViewController {
set {
_layout = newValue
if let parent = parent, let layout = newValue as? UIViewController, layout == parent {
let log = "Warning: A memory leak occurs due to a retain cycle, as \(self) owns the parent view controller(\(parent)) as its layout object. Don't allow the parent to adopt FloatingPanelLayout."
os_log(msg, log: sysLog, type: .error, log)
log.warning("A memory leak will occur by a retain cycle because \(self) owns the parent view controller(\(parent)) as the layout object. Don't let the parent adopt FloatingPanelLayout.")
}
}
}
@@ -216,8 +176,7 @@ open class FloatingPanelController: UIViewController {
set {
_behavior = newValue
if let parent = parent, let behavior = newValue as? UIViewController, behavior == parent {
let log = "Warning: A memory leak occurs due to a retain cycle, as \(self) owns the parent view controller(\(parent)) as its behavior object. Don't allow the parent to adopt FloatingPanelBehavior."
os_log(msg, log: sysLog, type: .error, log)
log.warning("A memory leak will occur by a retain cycle because \(self) owns the parent view controller(\(parent)) as the behavior object. Don't let the parent adopt FloatingPanelBehavior.")
}
}
}
@@ -251,7 +210,7 @@ open class FloatingPanelController: UIViewController {
/// The NearbyState determines that finger's nearby state.
public var nearbyState: FloatingPanelState {
let currentY = surfaceLocation.y
return floatingPanel.targetState(from: currentY, with: .zero)
return floatingPanel.targetPosition(from: currentY, with: .zero)
}
/// Constants that define how a panel content fills in the surface.
@@ -266,7 +225,7 @@ open class FloatingPanelController: UIViewController {
private var _contentViewController: UIViewController?
private(set) var floatingPanel: Core!
private var preSafeAreaInsets: UIEdgeInsets = .zero // Capture the latest one
private var preSafeAreaInsets: UIEdgeInsets = .zero // Capture the latest one
private var safeAreaInsetsObservation: NSKeyValueObservation?
private let modalTransition = ModalTransition()
@@ -305,7 +264,7 @@ open class FloatingPanelController: UIViewController {
floatingPanel = Core(self, layout: initialLayout, behavior: initialBehavior)
}
private func didUpdateDelegate(){
private func didUpdateDelegate() {
if let layout = delegate?.floatingPanel?(self, layoutFor: traitCollection) {
_layout = layout
}
@@ -331,19 +290,19 @@ open class FloatingPanelController: UIViewController {
open override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// Ensure to update the static constraint of a panel after rotating a device in static mode
if contentMode == .static {
floatingPanel.layoutAdapter.updateStaticConstraint()
if #available(iOS 11.0, *) {
// Ensure to update the static constraint of a panel after rotating a device in static mode
if contentMode == .static {
floatingPanel.layoutAdapter.updateStaticConstraint()
}
} else {
// Because {top,bottom}LayoutGuide is managed as a view
if floatingPanel.isAttracting == false {
self.update(safeAreaInsets: fp_safeAreaInsets)
}
}
}
open override func 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)
@@ -411,9 +370,9 @@ open class FloatingPanelController: UIViewController {
private func update(safeAreaInsets: UIEdgeInsets) {
guard
preSafeAreaInsets != safeAreaInsets
else { return }
else { return }
os_log(msg, log: devLog, type: .debug, "Update safeAreaInsets = \(safeAreaInsets)")
log.debug("Update safeAreaInsets", safeAreaInsets)
// Prevent an infinite loop on iOS 10: setUpLayout() -> viewDidLayoutSubviews() -> setUpLayout()
preSafeAreaInsets = safeAreaInsets
@@ -425,7 +384,6 @@ open class FloatingPanelController: UIViewController {
}
floatingPanel.layoutAdapter.updateStaticConstraint()
floatingPanel.adjustScrollContentInsetIfNeeded()
if let contentOffset = contentOffset {
trackingScrollView?.contentOffset = contentOffset
@@ -453,15 +411,8 @@ open class FloatingPanelController: UIViewController {
guard let self = self else { return }
self.delegate?.floatingPanelDidRemove?(self)
}
} else if parent != nil {
removePanelFromParent(animated: true)
} else {
delegate?.floatingPanelWillRemove?(self)
hide(animated: true) { [weak self] in
guard let self = self else { return }
self.view.removeFromSuperview()
self.delegate?.floatingPanelDidRemove?(self)
}
removePanelFromParent(animated: true)
}
}
@@ -476,36 +427,45 @@ open class FloatingPanelController: UIViewController {
// Must apply the current layout here
activateLayout(forceLayout: true)
// 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 }
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 }
// 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)
self.update(safeAreaInsets: self.view.safeAreaInsets)
}
} else {
// KVOs for topLayoutGuide & bottomLayoutGuide are not effective.
// Instead, update(safeAreaInsets:) is called at `viewDidLayoutSubviews()`
}
move(to: floatingPanel.layoutAdapter.initialState,
animated: animated,
completion: completion)
move(
to: floatingPanel.layoutAdapter.initialState,
animated: animated,
completion: completion
)
}
/// Hides the surface view to the hidden position
@objc(hide:completion:)
public func hide(animated: Bool = false, completion: (() -> Void)? = nil) {
move(to: .hidden,
animated: animated,
completion: completion)
move(
to: .hidden,
animated: animated,
completion: completion
)
}
/// Adds the view managed by the controller as a child of the specified view controller.
@@ -517,7 +477,7 @@ open class FloatingPanelController: UIViewController {
@objc(addPanelToParent:at:animated:completion:)
public func addPanel(toParent parent: UIViewController, at viewIndex: Int = -1, animated: Bool = false, completion: (() -> Void)? = nil) {
guard self.parent == nil else {
os_log(msg, log: sysLog, type: .error, "Warning: already added to a parent(\(parent))")
log.warning("Already added to a parent(\(parent))")
return
}
assert((parent is UINavigationController) == false, "UINavigationController displays only one child view controller at a time.")
@@ -534,14 +494,14 @@ open class FloatingPanelController: UIViewController {
parent.addChild(self)
view.frame = parent.view.bounds // Needed for a correct safe area configuration
view.frame = parent.view.bounds // Needed for a correct safe area configuration
view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
self.view.topAnchor.constraint(equalTo: parent.view.topAnchor, constant: 0.0),
self.view.leftAnchor.constraint(equalTo: parent.view.leftAnchor, constant: 0.0),
self.view.rightAnchor.constraint(equalTo: parent.view.rightAnchor, constant: 0.0),
self.view.bottomAnchor.constraint(equalTo: parent.view.bottomAnchor, constant: 0.0),
])
])
show(animated: animated) { [weak self] in
guard let self = self else { return }
@@ -624,7 +584,13 @@ open class FloatingPanelController: UIViewController {
switch contentInsetAdjustmentBehavior {
case .always:
scrollView.contentInsetAdjustmentBehavior = .never
if #available(iOS 11.0, *) {
scrollView.contentInsetAdjustmentBehavior = .never
} else {
children.forEach { (vc) in
vc.automaticallyAdjustsScrollViewInsets = false
}
}
default:
break
}
@@ -709,35 +675,41 @@ extension FloatingPanelController {
if let animator = delegate?.floatingPanel?(self, animatorForPresentingTo: to) {
return animator
}
let timingParameters = UISpringTimingParameters(decelerationRate: UIScrollView.DecelerationRate.fast.rawValue,
frequencyResponse: 0.25)
return UIViewPropertyAnimator(duration: 0.0,
timingParameters: timingParameters)
let timingParameters = UISpringTimingParameters(
decelerationRate: UIScrollView.DecelerationRate.fast.rawValue,
frequencyResponse: 0.25)
return UIViewPropertyAnimator(
duration: 0.0,
timingParameters: timingParameters
)
}
func animatorForDismissing(with velocity: CGVector) -> UIViewPropertyAnimator {
if let animator = delegate?.floatingPanel?(self, animatorForDismissingWith: velocity) {
return animator
}
let timingParameters = UISpringTimingParameters(decelerationRate: UIScrollView.DecelerationRate.fast.rawValue,
frequencyResponse: 0.25,
initialVelocity: velocity)
return UIViewPropertyAnimator(duration: 0.0,
timingParameters: timingParameters)
let timingParameters = UISpringTimingParameters(
decelerationRate: UIScrollView.DecelerationRate.fast.rawValue,
frequencyResponse: 0.25,
initialVelocity: velocity)
return UIViewPropertyAnimator(
duration: 0.0,
timingParameters: timingParameters
)
}
}
// MARK: - Swizzling
private var originalDismissImp: IMP?
private typealias DismissFunction = @convention(c) (AnyObject, Selector, Bool, (() -> Void)?) -> Void
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)
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:))) {
originalDismissImp = method_setImplementation(originalMethod, swizzledImp)
let swizzledImp = class_getMethodImplementation(aClass, #selector(__swizzled_dismiss(animated:completion:)))
{
originalDismissImp = method_setImplementation(originalMethod, swizzledImp)
}
}()
}
@@ -745,7 +717,7 @@ extension FloatingPanelController {
extension UIViewController {
@objc
fileprivate func __swizzled_dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
let dismissImp = unsafeBitCast(originalDismissImp, to: DismissFunction.self)
let dismissImp = unsafeBitCast(originalDismissImp, to: __dismissFunction.self)
let sel = #selector(UIViewController.dismiss(animated:completion:))
// Call dismiss(animated:completion:) to a content view controller
+296 -437
View File
File diff suppressed because it is too large Load Diff
+135 -45
View File
@@ -7,12 +7,10 @@ import UIKit
extension CGFloat {
/// Returns this value rounded to an logical pixel value by a display scale
func rounded(by displayScale: CGFloat) -> CGFloat {
let p = CGFloat(1.0e9)
let v = (self * p).rounded(.towardZero) / p
return (v * displayScale).rounded(.toNearestOrAwayFromZero) / displayScale
return (self * displayScale).rounded(.toNearestOrAwayFromZero) / displayScale
}
func isEqual(to: CGFloat, on displayScale: CGFloat) -> Bool {
return rounded(by: displayScale) == to.rounded(by: displayScale)
return self.rounded(by: displayScale) == to.rounded(by: displayScale)
}
}
@@ -47,16 +45,71 @@ protocol LayoutGuideProvider {
extension UILayoutGuide: LayoutGuideProvider {}
extension UIView: LayoutGuideProvider {}
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 {
return view.safeAreaInsets
private class CustomLayoutGuide: LayoutGuideProvider {
let topAnchor: NSLayoutYAxisAnchor
let leftAnchor: NSLayoutXAxisAnchor
let bottomAnchor: NSLayoutYAxisAnchor
let rightAnchor: NSLayoutXAxisAnchor
let widthAnchor: NSLayoutDimension
let heightAnchor: NSLayoutDimension
init(
topAnchor: NSLayoutYAxisAnchor,
leftAnchor: NSLayoutXAxisAnchor,
bottomAnchor: NSLayoutYAxisAnchor,
rightAnchor: NSLayoutXAxisAnchor,
widthAnchor: NSLayoutDimension,
heightAnchor: NSLayoutDimension
) {
self.topAnchor = topAnchor
self.leftAnchor = leftAnchor
self.bottomAnchor = bottomAnchor
self.rightAnchor = rightAnchor
self.widthAnchor = widthAnchor
self.heightAnchor = heightAnchor
}
}
extension UIViewController {
@objc var fp_safeAreaInsets: UIEdgeInsets {
if #available(iOS 11.0, *) {
return view.safeAreaInsets
} else {
return UIEdgeInsets(
top: topLayoutGuide.length,
left: 0.0,
bottom: bottomLayoutGuide.length,
right: 0.0
)
}
}
var fp_safeAreaLayoutGuide: LayoutGuideProvider {
if #available(iOS 11.0, *) {
return view?.safeAreaLayoutGuide ?? view
} else {
return CustomLayoutGuide(
topAnchor: topLayoutGuide.bottomAnchor,
leftAnchor: view.leftAnchor,
bottomAnchor: bottomLayoutGuide.topAnchor,
rightAnchor: view.rightAnchor,
widthAnchor: view.widthAnchor,
heightAnchor: topLayoutGuide.bottomAnchor.anchorWithOffset(to: bottomLayoutGuide.topAnchor)
)
}
}
}
// The reason why UIView has no extensions of safe area insets and top/bottom guides
// is for iOS10 compatibility.
extension UIView {
var fp_safeAreaLayoutGuide: LayoutGuideProvider {
if #available(iOS 11.0, *) {
return safeAreaLayoutGuide
} else {
return self
}
}
var presentationFrame: CGRect {
return layer.presentation()?.frame ?? frame
}
@@ -86,13 +139,18 @@ extension UIView {
}
static func performWithLinear(startTime: Double = 0.0, relativeDuration: Double = 1.0, _ animations: @escaping (() -> Void)) {
UIView.animateKeyframes(withDuration: 0.0, delay: 0.0, options: [.calculationModeCubic], animations: {
UIView.addKeyframe(withRelativeStartTime: startTime, relativeDuration: relativeDuration, animations: animations)
}, completion: nil)
UIView.animateKeyframes(
withDuration: 0.0,
delay: 0.0,
options: [.calculationModeCubic],
animations: {
UIView.addKeyframe(withRelativeStartTime: startTime, relativeDuration: relativeDuration, animations: animations)
},
completion: nil)
}
}
#if FP_LOG
#if __FP_LOG
extension UIGestureRecognizer.State: CustomDebugStringConvertible {
public var debugDescription: String {
switch self {
@@ -109,9 +167,21 @@ extension UIGestureRecognizer.State: CustomDebugStringConvertible {
#endif
extension UIScrollView {
var isLocked: Bool {
return !showsVerticalScrollIndicator && 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 + adjustedContentInset.right) - bounds.width, 0.0),
y: max((contentSize.height + adjustedContentInset.bottom) - bounds.height, 0.0))
return CGPoint(
x: max((contentSize.width + fp_contentInset.right) - bounds.width, 0.0),
y: max((contentSize.height + fp_contentInset.bottom) - bounds.height, 0.0)
)
}
}
@@ -151,7 +221,7 @@ extension UIEdgeInsets {
extension UIBezierPath {
static func path(roundedRect rect: CGRect, appearance: SurfaceAppearance) -> UIBezierPath {
let cornerRadius = appearance.cornerRadius;
let cornerRadius = appearance.cornerRadius
if #available(iOS 13.0, *) {
if appearance.cornerCurve == .circular {
let path = UIBezierPath()
@@ -161,45 +231,61 @@ extension UIBezierPath {
path.addLine(to: CGPoint(x: rect.maxX - cornerRadius, y: rect.minY))
if cornerRadius > 0 {
path .addArc(withCenter: CGPoint(x: rect.maxX - cornerRadius,
y: rect.minY + cornerRadius),
radius: cornerRadius,
startAngle: -0.5 * .pi,
endAngle: 0,
clockwise: true)
path.addArc(
withCenter: CGPoint(
x: rect.maxX - cornerRadius,
y: rect.minY + cornerRadius
),
radius: cornerRadius,
startAngle: -0.5 * .pi,
endAngle: 0,
clockwise: true
)
}
path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY - cornerRadius))
if cornerRadius > 0 {
path.addArc(withCenter: CGPoint(x: rect.maxX - cornerRadius,
y: rect.maxY - cornerRadius),
radius: cornerRadius,
startAngle: 0,
endAngle: .pi * 0.5,
clockwise: true)
path.addArc(
withCenter: CGPoint(
x: rect.maxX - cornerRadius,
y: rect.maxY - cornerRadius
),
radius: cornerRadius,
startAngle: 0,
endAngle: .pi * 0.5,
clockwise: true
)
}
path.addLine(to: CGPoint(x: rect.minX + cornerRadius, y: rect.maxY))
if cornerRadius > 0 {
path.addArc(withCenter: CGPoint(x: rect.minX + cornerRadius,
y: rect.maxY - cornerRadius),
radius: cornerRadius,
startAngle: .pi * 0.5,
endAngle: .pi,
clockwise: true)
path.addArc(
withCenter: CGPoint(
x: rect.minX + cornerRadius,
y: rect.maxY - cornerRadius
),
radius: cornerRadius,
startAngle: .pi * 0.5,
endAngle: .pi,
clockwise: true
)
}
path.addLine(to: CGPoint(x: rect.minX, y: rect.minY + cornerRadius))
if cornerRadius > 0 {
path.addArc(withCenter: CGPoint(x: rect.minX + cornerRadius,
y: rect.minY + cornerRadius),
radius: cornerRadius,
startAngle: .pi,
endAngle: .pi * 1.5,
clockwise: true)
path.addArc(
withCenter: CGPoint(
x: rect.minX + cornerRadius,
y: rect.minY + cornerRadius
),
radius: cornerRadius,
startAngle: .pi,
endAngle: .pi * 1.5,
clockwise: true
)
}
path.addLine(to: start)
@@ -209,9 +295,13 @@ extension UIBezierPath {
return path
}
}
return UIBezierPath(roundedRect: rect,
byRoundingCorners: [.allCorners],
cornerRadii: CGSize(width: cornerRadius,
height: cornerRadius))
return UIBezierPath(
roundedRect: rect,
byRoundingCorners: [.allCorners],
cornerRadii: CGSize(
width: cornerRadius,
height: cornerRadius
)
)
}
}
+1 -1
View File
@@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>2.8.3</string>
<string>2.6.2</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
</dict>
+112 -83
View File
@@ -1,7 +1,6 @@
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
import UIKit
import os.log
/// An interface for generating layout information for a panel.
@objc public protocol FloatingPanelLayout {
@@ -31,7 +30,7 @@ open class FloatingPanelBottomLayout: NSObject, FloatingPanelLayout {
return .half
}
open var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] {
open var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] {
return [
.full: FloatingPanelLayoutAnchor(absoluteInset: 18.0, edge: .top, referenceGuide: .safeArea),
.half: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .safeArea),
@@ -45,8 +44,8 @@ open class FloatingPanelBottomLayout: NSObject, FloatingPanelLayout {
open func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint] {
return [
surfaceView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 0.0),
surfaceView.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor, constant: 0.0),
surfaceView.leftAnchor.constraint(equalTo: view.fp_safeAreaLayoutGuide.leftAnchor, constant: 0.0),
surfaceView.rightAnchor.constraint(equalTo: view.fp_safeAreaLayoutGuide.rightAnchor, constant: 0.0),
]
}
@@ -154,36 +153,42 @@ class LayoutAdapter {
var adjustedContentInsets: UIEdgeInsets {
switch position {
case .top:
return UIEdgeInsets(top: safeAreaInsets.top,
left: 0.0,
bottom: 0.0,
right: 0.0)
return UIEdgeInsets(
top: safeAreaInsets.top,
left: 0.0,
bottom: 0.0,
right: 0.0
)
case .left:
return UIEdgeInsets(top: 0.0,
left: safeAreaInsets.left,
bottom: 0.0,
right: 0.0)
return UIEdgeInsets(
top: 0.0,
left: safeAreaInsets.left,
bottom: 0.0,
right: 0.0
)
case .bottom:
return UIEdgeInsets(top: 0.0,
left: 0.0,
bottom: safeAreaInsets.bottom,
right: 0.0)
return UIEdgeInsets(
top: 0.0,
left: 0.0,
bottom: safeAreaInsets.bottom,
right: 0.0
)
case .right:
return UIEdgeInsets(top: 0.0,
left: 0.0,
bottom: 0.0,
right: safeAreaInsets.right)
return UIEdgeInsets(
top: 0.0,
left: 0.0,
bottom: 0.0,
right: safeAreaInsets.right
)
}
}
/*
Returns a constraint based value in the interaction and animation.
So that it doesn't need to call `surfaceView.layoutIfNeeded()`
after every interaction and animation update. It has an effect on
the smooth interaction because the content view doesn't need to update
its layout frequently.
*/
/// Returns a constraint based value in the interaction and animation.
///
/// So that it doesn't need to call `surfaceView.layoutIfNeeded()`
/// after every interaction and animation update. It has an effect on
/// the smooth interaction because the content view doesn't need to update
/// its layout frequently.
var surfaceLocation: CGPoint {
get {
var pos: CGFloat
@@ -266,25 +271,12 @@ 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 = position(for: state) - edgePosition(surfaceView.frame)
return edgePosition(surfaceView.presentationFrame) - position(for: mostExpandedState)
case .bottom, .right:
offset = edgePosition(surfaceView.frame) - position(for: state)
return position(for: mostExpandedState) - edgePosition(surfaceView.presentationFrame)
}
return offset.rounded(by: surfaceView.fp_displayScale)
}
private var hiddenAnchor: FloatingPanelLayoutAnchoring {
@@ -435,22 +427,22 @@ class LayoutAdapter {
switch position {
case .top, .bottom:
surfaceConstraints = [
surfaceView.leftAnchor.constraint(equalTo: vc.view.safeAreaLayoutGuide.leftAnchor, constant: 0.0),
surfaceView.rightAnchor.constraint(equalTo: vc.view.safeAreaLayoutGuide.rightAnchor, constant: 0.0),
surfaceView.leftAnchor.constraint(equalTo: vc.fp_safeAreaLayoutGuide.leftAnchor, constant: 0.0),
surfaceView.rightAnchor.constraint(equalTo: vc.fp_safeAreaLayoutGuide.rightAnchor, constant: 0.0),
]
case .left, .right:
surfaceConstraints = [
surfaceView.topAnchor.constraint(equalTo: vc.view.safeAreaLayoutGuide.topAnchor, constant: 0.0),
surfaceView.bottomAnchor.constraint(equalTo: vc.view.safeAreaLayoutGuide.bottomAnchor, constant: 0.0),
surfaceView.topAnchor.constraint(equalTo: vc.fp_safeAreaLayoutGuide.topAnchor, constant: 0.0),
surfaceView.bottomAnchor.constraint(equalTo: vc.fp_safeAreaLayoutGuide.bottomAnchor, constant: 0.0),
]
}
}
let backdropConstraints = [
backdropView.topAnchor.constraint(equalTo: vc.view.topAnchor, constant: 0.0),
backdropView.leftAnchor.constraint(equalTo: vc.view.leftAnchor,constant: 0.0),
backdropView.leftAnchor.constraint(equalTo: vc.view.leftAnchor, constant: 0.0),
backdropView.rightAnchor.constraint(equalTo: vc.view.rightAnchor, constant: 0.0),
backdropView.bottomAnchor.constraint(equalTo: vc.view.bottomAnchor, constant: 0.0),
]
]
fixedConstraints = surfaceConstraints + backdropConstraints
@@ -485,7 +477,10 @@ class LayoutAdapter {
for state in layout.anchors.keys {
stateConstraints[state] = layout.anchors[state]?
.layoutConstraints(vc, for: position)
.map{ $0.identifier = "FloatingPanel-\(state)-constraint"; return $0 }
.map {
$0.identifier = "FloatingPanel-\(state)-constraint"
return $0
}
}
let hiddenAnchor = layout.anchors[.hidden] ?? self.hiddenAnchor
offConstraints = hiddenAnchor.layoutConstraints(vc, for: position)
@@ -549,7 +544,7 @@ class LayoutAdapter {
let layoutGuideProvider: LayoutGuideProvider
switch anchor.referenceGuide {
case .safeArea:
layoutGuideProvider = vc.view.safeAreaLayoutGuide
layoutGuideProvider = vc.fp_safeAreaLayoutGuide
case .superview:
layoutGuideProvider = vc.view
}
@@ -563,8 +558,10 @@ class LayoutAdapter {
case .top:
switch referenceEdge(of: anchor) {
case .top:
animationConstraint = surfaceView.bottomAnchor.constraint(equalTo: layoutGuideProvider.topAnchor,
constant: currentY)
animationConstraint = surfaceView.bottomAnchor.constraint(
equalTo: layoutGuideProvider.topAnchor,
constant: currentY
)
if anchor.referenceGuide == .safeArea {
animationConstraint.constant -= safeAreaInsets.top
targetY -= safeAreaInsets.top
@@ -572,8 +569,10 @@ class LayoutAdapter {
case .bottom:
let baseHeight = vc.view.bounds.height
targetY = -(baseHeight - targetY)
animationConstraint = surfaceView.bottomAnchor.constraint(equalTo: layoutGuideProvider.bottomAnchor,
constant: -(baseHeight - currentY))
animationConstraint = surfaceView.bottomAnchor.constraint(
equalTo: layoutGuideProvider.bottomAnchor,
constant: -(baseHeight - currentY)
)
if anchor.referenceGuide == .safeArea {
animationConstraint.constant += safeAreaInsets.bottom
targetY += safeAreaInsets.bottom
@@ -585,16 +584,20 @@ class LayoutAdapter {
case .left:
switch referenceEdge(of: anchor) {
case .left:
animationConstraint = surfaceView.rightAnchor.constraint(equalTo: layoutGuideProvider.leftAnchor,
constant: currentY)
animationConstraint = surfaceView.rightAnchor.constraint(
equalTo: layoutGuideProvider.leftAnchor,
constant: currentY
)
if anchor.referenceGuide == .safeArea {
animationConstraint.constant -= safeAreaInsets.right
targetY -= safeAreaInsets.right
}
case .right:
targetY = -(baseHeight - targetY)
animationConstraint = surfaceView.rightAnchor.constraint(equalTo: layoutGuideProvider.rightAnchor,
constant: -(baseHeight - currentY))
animationConstraint = surfaceView.rightAnchor.constraint(
equalTo: layoutGuideProvider.rightAnchor,
constant: -(baseHeight - currentY)
)
if anchor.referenceGuide == .safeArea {
animationConstraint.constant += safeAreaInsets.left
targetY += safeAreaInsets.left
@@ -605,16 +608,20 @@ class LayoutAdapter {
case .bottom:
switch referenceEdge(of: anchor) {
case .top:
animationConstraint = surfaceView.topAnchor.constraint(equalTo: layoutGuideProvider.topAnchor,
constant: currentY)
animationConstraint = surfaceView.topAnchor.constraint(
equalTo: layoutGuideProvider.topAnchor,
constant: currentY
)
if anchor.referenceGuide == .safeArea {
animationConstraint.constant -= safeAreaInsets.top
targetY -= safeAreaInsets.top
}
case .bottom:
targetY = -(baseHeight - targetY)
animationConstraint = surfaceView.topAnchor.constraint(equalTo: layoutGuideProvider.bottomAnchor,
constant: -(baseHeight - currentY))
animationConstraint = surfaceView.topAnchor.constraint(
equalTo: layoutGuideProvider.bottomAnchor,
constant: -(baseHeight - currentY)
)
if anchor.referenceGuide == .safeArea {
animationConstraint.constant += safeAreaInsets.bottom
targetY += safeAreaInsets.bottom
@@ -626,16 +633,20 @@ class LayoutAdapter {
case .right:
switch referenceEdge(of: anchor) {
case .left:
animationConstraint = surfaceView.leftAnchor.constraint(equalTo: layoutGuideProvider.leftAnchor,
constant: currentY)
animationConstraint = surfaceView.leftAnchor.constraint(
equalTo: layoutGuideProvider.leftAnchor,
constant: currentY
)
if anchor.referenceGuide == .safeArea {
animationConstraint.constant -= safeAreaInsets.left
targetY -= safeAreaInsets.left
}
case .right:
targetY = -(baseHeight - targetY)
animationConstraint = surfaceView.leftAnchor.constraint(equalTo: layoutGuideProvider.rightAnchor,
constant: -(baseHeight - currentY))
animationConstraint = surfaceView.leftAnchor.constraint(
equalTo: layoutGuideProvider.rightAnchor,
constant: -(baseHeight - currentY)
)
if anchor.referenceGuide == .safeArea {
animationConstraint.constant += safeAreaInsets.right
targetY += safeAreaInsets.right
@@ -661,7 +672,7 @@ class LayoutAdapter {
// The method is separated from prepareLayout(to:) for the rotation support
// It must be called in FloatingPanelController.traitCollectionDidChange(_:)
func updateStaticConstraint() {
NSLayoutConstraint.deactivate([staticConstraint, contentBoundingConstraint].compactMap{ $0 })
NSLayoutConstraint.deactivate([staticConstraint, contentBoundingConstraint].compactMap { $0 })
staticConstraint = nil
contentBoundingConstraint = nil
@@ -670,7 +681,10 @@ class LayoutAdapter {
return
}
let anchor = layout.anchors[self.mostExpandedState]!
guard let anchor = layout.anchors[mostExpandedState] else {
return
}
let surfaceAnchor = position.mainDimensionAnchor(surfaceView)
switch anchor {
case let anchor as FloatingPanelIntrinsicLayoutAnchor:
@@ -689,11 +703,15 @@ class LayoutAdapter {
let baseAnchor = position.mainDimensionAnchor(anchor.contentLayoutGuide)
if let boundingLayoutGuide = anchor.contentBoundingGuide.layoutGuide(vc) {
if anchor.isAbsolute {
contentBoundingConstraint = baseAnchor.constraint(lessThanOrEqualTo: position.mainDimensionAnchor(boundingLayoutGuide),
constant: anchor.offset)
contentBoundingConstraint = baseAnchor.constraint(
lessThanOrEqualTo: position.mainDimensionAnchor(boundingLayoutGuide),
constant: anchor.offset
)
} else {
contentBoundingConstraint = baseAnchor.constraint(lessThanOrEqualTo: position.mainDimensionAnchor(boundingLayoutGuide),
multiplier: anchor.offset)
contentBoundingConstraint = baseAnchor.constraint(
lessThanOrEqualTo: position.mainDimensionAnchor(boundingLayoutGuide),
multiplier: anchor.offset
)
}
staticConstraint = surfaceAnchor.constraint(lessThanOrEqualTo: baseAnchor, constant: constant)
} else {
@@ -705,8 +723,10 @@ class LayoutAdapter {
staticConstraint = surfaceAnchor.constraint(equalToConstant: position(for: self.mostCoordinateState))
case .bottom, .right:
let rootViewAnchor = position.mainDimensionAnchor(vc.view)
staticConstraint = rootViewAnchor.constraint(equalTo: surfaceAnchor,
constant: position(for: self.leastCoordinateState))
staticConstraint = rootViewAnchor.constraint(
equalTo: surfaceAnchor,
constant: position(for: self.leastCoordinateState)
)
}
}
@@ -717,14 +737,14 @@ class LayoutAdapter {
staticConstraint?.identifier = "FloatingPanel-static-width"
}
NSLayoutConstraint.activate([staticConstraint, contentBoundingConstraint].compactMap{ $0 })
NSLayoutConstraint.activate([staticConstraint, contentBoundingConstraint].compactMap { $0 })
surfaceView.containerOverflow = position.mainDimension(vc.view.bounds.size)
}
func updateInteractiveEdgeConstraint(diff: CGFloat, scrollingContent: Bool, allowsRubberBanding: (UIRectEdge) -> Bool) {
defer {
os_log(msg, log: devLog, type: .debug, "update surface location = \(surfaceLocation)")
log.debug("update surface location = \(surfaceLocation)")
}
let minConst: CGFloat = position(for: leastCoordinateState)
@@ -764,9 +784,9 @@ class LayoutAdapter {
defer {
if forceLayout {
layoutSurfaceIfNeeded()
os_log(msg, log: devLog, type: .debug, "activateLayout for \(state) -- surface.presentation = \(self.surfaceView.presentationFrame) surface.frame = \(self.surfaceView.frame)")
log.debug("activateLayout for \(state) -- surface.presentation = \(self.surfaceView.presentationFrame) surface.frame = \(self.surfaceView.frame)")
} else {
os_log(msg, log: devLog, type: .debug, "activateLayout for \(state)")
log.debug("activateLayout for \(state)")
}
}
@@ -782,6 +802,12 @@ 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
@@ -795,7 +821,7 @@ class LayoutAdapter {
if let constraints = stateConstraints[state] {
NSLayoutConstraint.activate(constraints)
} else {
os_log(msg, log: sysLog, type: .fault, "Error: can not find any constraints for \(state)")
log.error("Couldn't find any constraints for \(state)")
}
}
}
@@ -814,9 +840,12 @@ class LayoutAdapter {
fileprivate func checkLayout() {
// Verify layout configurations
assert(anchorStates.count > 0)
assert(validStates.contains(layout.initialState),
"Does not include an initial state (\(layout.initialState)) in (\(validStates))")
/* This assertion does not work in a device rotating
assert(
validStates.contains(layout.initialState),
"Does not include an initial state (\(layout.initialState)) in (\(validStates))"
)
/**
// This assertion does not work in a device rotating
let statePosOrder = activeStates.sorted(by: { position(for: $0) < position(for: $1) })
assert(sortedDirectionalStates == statePosOrder,
"Check your layout anchors because the state order(\(statePosOrder)) must be (\(sortedDirectionalStates))).")
+15 -11
View File
@@ -3,13 +3,15 @@
import UIKit
/// An interface for implementing custom layout anchor objects.
@objc public protocol FloatingPanelLayoutAnchoring {
@objc
public protocol FloatingPanelLayoutAnchoring {
var referenceGuide: FloatingPanelLayoutReferenceGuide { get }
func layoutConstraints(_ fpc: FloatingPanelController, for position: FloatingPanelPosition) -> [NSLayoutConstraint]
}
/// An object that defines how to settles a panel with insets from an edge of a reference rectangle.
@objc final public class FloatingPanelLayoutAnchor: NSObject, FloatingPanelLayoutAnchoring /*, NSCopying */ {
@objc
public final class FloatingPanelLayoutAnchor: NSObject, FloatingPanelLayoutAnchoring {
/// Returns a layout anchor with the specified inset by an absolute value, edge and reference guide for a panel.
///
@@ -50,8 +52,8 @@ import UIKit
@objc let referenceEdge: FloatingPanelReferenceEdge
}
public extension FloatingPanelLayoutAnchor {
func layoutConstraints(_ vc: FloatingPanelController, for position: FloatingPanelPosition) -> [NSLayoutConstraint] {
extension FloatingPanelLayoutAnchor {
public func layoutConstraints(_ vc: FloatingPanelController, for position: FloatingPanelPosition) -> [NSLayoutConstraint] {
let layoutGuide = referenceGuide.layoutGuide(vc: vc)
switch position {
case .top:
@@ -72,7 +74,7 @@ public extension FloatingPanelLayoutAnchor {
return [edgeAnchor.constraint(equalTo: layoutGuide.topAnchor, constant: inset)]
}
let offsetAnchor = layoutGuide.topAnchor.anchorWithOffset(to: edgeAnchor)
return [offsetAnchor.constraint(equalTo:layoutGuide.heightAnchor, multiplier: inset)]
return [offsetAnchor.constraint(equalTo: layoutGuide.heightAnchor, multiplier: inset)]
case .bottom:
if isAbsolute {
return [layoutGuide.bottomAnchor.constraint(equalTo: edgeAnchor, constant: inset)]
@@ -105,7 +107,8 @@ public extension FloatingPanelLayoutAnchor {
}
/// An object that defines how to settles a panel with the intrinsic size for a content.
@objc final public class FloatingPanelIntrinsicLayoutAnchor: NSObject, FloatingPanelLayoutAnchoring /*, NSCopying */ {
@objc
public final class FloatingPanelIntrinsicLayoutAnchor: NSObject, FloatingPanelLayoutAnchoring {
/// Returns a layout anchor with the specified offset by an absolute value and reference guide for a panel.
///
@@ -141,8 +144,8 @@ public extension FloatingPanelLayoutAnchor {
@objc public let referenceGuide: FloatingPanelLayoutReferenceGuide
}
public extension FloatingPanelIntrinsicLayoutAnchor {
func layoutConstraints(_ vc: FloatingPanelController, for position: FloatingPanelPosition) -> [NSLayoutConstraint] {
extension FloatingPanelIntrinsicLayoutAnchor {
public func layoutConstraints(_ vc: FloatingPanelController, for position: FloatingPanelPosition) -> [NSLayoutConstraint] {
let surfaceIntrinsicLength = position.mainDimension(vc.surfaceView.intrinsicContentSize)
let constant = isAbsolute ? surfaceIntrinsicLength - offset : surfaceIntrinsicLength * (1 - offset)
let layoutGuide = referenceGuide.layoutGuide(vc: vc)
@@ -160,7 +163,8 @@ public extension FloatingPanelIntrinsicLayoutAnchor {
}
/// An object that defines how to settles a panel with a layout guide of a content view.
@objc final public class FloatingPanelAdaptiveLayoutAnchor: NSObject, FloatingPanelLayoutAnchoring /*, NSCopying */ {
@objc
public final class FloatingPanelAdaptiveLayoutAnchor: NSObject, FloatingPanelLayoutAnchoring {
/// Returns a layout anchor with the specified offset by an absolute value to display a panel with its intrinsic content size.
///
@@ -222,8 +226,8 @@ public extension FloatingPanelIntrinsicLayoutAnchor {
@objc public let contentBoundingGuide: FloatingPanelLayoutContentBoundingGuide
}
public extension FloatingPanelAdaptiveLayoutAnchor {
func layoutConstraints(_ vc: FloatingPanelController, for position: FloatingPanelPosition) -> [NSLayoutConstraint] {
extension FloatingPanelAdaptiveLayoutAnchor {
public func layoutConstraints(_ vc: FloatingPanelController, for position: FloatingPanelPosition) -> [NSLayoutConstraint] {
var constraints = [NSLayoutConstraint]()
let layoutGuide = referenceGuide.layoutGuide(vc: vc)
+2 -2
View File
@@ -37,7 +37,7 @@ extension FloatingPanelLayoutReferenceGuide {
func layoutGuide(vc: UIViewController) -> LayoutGuideProvider {
switch self {
case .safeArea:
return vc.view.safeAreaLayoutGuide
return vc.fp_safeAreaLayoutGuide
case .superview:
return vc.view
}
@@ -57,7 +57,7 @@ extension FloatingPanelLayoutContentBoundingGuide {
case .superview:
return fpc.view
case .safeArea:
return fpc.view.safeAreaLayoutGuide
return fpc.fp_safeAreaLayoutGuide
case .none:
return nil
}
+100
View File
@@ -0,0 +1,100 @@
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
import Foundation
import os.log
// Must be a variable to use `hook` property in testing
var log = {
return Logger()
}()
struct Logger {
private let osLog: OSLog
private let s = DispatchSemaphore(value: 1)
enum Level: Int, Comparable {
case debug = 0
case info = 1
case warning = 2
case error = 3
var displayName: String {
switch self {
case .debug:
return "Debug:"
case .info:
return "Info:"
case .warning:
return "Warning:"
case .error:
return "Error:"
}
}
@available(iOS 10.0, *)
var osLogType: OSLogType {
switch self {
case .debug: return .debug
case .info: return .info
case .warning: return .default
case .error: return .error
}
}
static func < (lhs: Logger.Level, rhs: Logger.Level) -> Bool {
return lhs.rawValue < rhs.rawValue
}
}
typealias Hook = ((String, Level) -> Void)
var hook: Hook?
fileprivate init() {
osLog = OSLog(subsystem: "com.scenee.FloatingPanel", category: "FloatingPanel")
}
private func log(_ level: Level, _ message: Any, _ arguments: [Any], tag: String, function: String, line: UInt) {
_ = s.wait(timeout: .now() + 0.033)
defer { s.signal() }
let extraMessage: String = arguments.map({ String(describing: $0) }).joined(separator: " ")
let _tag = tag.isEmpty ? "" : "\(tag):"
let log: String = {
switch level {
case .debug:
return "\(level.displayName)\(_tag) \(message) \(extraMessage) (\(function):\(line))"
default:
return "\(level.displayName)\(_tag) \(message) \(extraMessage)"
}
}()
hook?(log, level)
os_log("%{public}@", log: osLog, type: level.osLogType, log)
}
private func getPrettyFunction(_ function: String, _ file: String) -> String {
if let filename = file.split(separator: "/").last {
return filename + ":" + function
} else {
return file + ":" + function
}
}
func debug(_ log: Any, _ arguments: Any..., tag: String = "", function: String = #function, file: String = #file, line: UInt = #line) {
#if __FP_LOG
self.log(.debug, log, arguments, tag: tag, function: getPrettyFunction(function, file), line: line)
#endif
}
func info(_ log: Any, _ arguments: Any..., tag: String = "", function: String = #function, file: String = #file, line: UInt = #line) {
self.log(.info, log, arguments, tag: tag, function: getPrettyFunction(function, file), line: line)
}
func warning(_ log: Any, _ arguments: Any..., function: String = #function, file: String = #file, line: UInt = #line) {
self.log(.warning, log, arguments, tag: "", function: getPrettyFunction(function, file), line: line)
}
func error(_ log: Any, _ arguments: Any..., function: String = #function, file: String = #file, line: UInt = #line) {
self.log(.error, log, arguments, tag: "", function: getPrettyFunction(function, file), line: line)
}
}
-17
View File
@@ -1,17 +0,0 @@
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
import os.log
let msg = StaticString("%{public}@")
let sysLog = OSLog(subsystem: Logging.subsystem, category: Logging.category)
#if FP_LOG
let devLog = OSLog(subsystem: Logging.subsystem, category: "\(Logging.category):dev")
#else
let devLog = OSLog.disabled
#endif
struct Logging {
static let subsystem = "com.scenee.FloatingPanel"
static let category = "FloatingPanel"
private init() {}
}
+144 -112
View File
@@ -1,7 +1,6 @@
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
import UIKit
import os.log
/// An object for customizing the appearance of a surface view
@objc(FloatingPanelSurfaceAppearance)
@@ -56,11 +55,11 @@ public class SurfaceAppearance: NSObject {
/// Defaults to `.circular`.
@available(iOS 13.0, *)
public var cornerCurve: CALayerCornerCurve {
get { _cornerCurve as? CALayerCornerCurve ?? .circular }
get { _cornerCurve ?? .circular }
set { _cornerCurve = newValue }
}
private var _cornerCurve: Any?
private var _cornerCurve: CALayerCornerCurve?
/// An array of shadows used to create drop shadows underneath a surface view.
public var shadows: [Shadow] = [Shadow()]
@@ -82,9 +81,11 @@ public class SurfaceView: UIView {
public let grabberHandle = GrabberView()
/// Offset of the grabber handle from the interactive edge.
public var grabberHandlePadding: CGFloat = 6.0 { didSet {
setNeedsUpdateConstraints()
} }
public var grabberHandlePadding: CGFloat = 6.0 {
didSet {
setNeedsUpdateConstraints()
}
}
/// The offset from the move edge to prevent the content scroll
public var grabberAreaOffset: CGFloat = 36.0
@@ -92,9 +93,11 @@ public class SurfaceView: UIView {
/// The grabber handle size
///
/// On left/right positioned panel the width dimension is used as the height of ``grabberHandle``, and vice versa.
public var grabberHandleSize: CGSize = CGSize(width: 36.0, height: 5.0) { didSet {
setNeedsUpdateConstraints()
} }
public var grabberHandleSize: CGSize = CGSize(width: 36.0, height: 5.0) {
didSet {
setNeedsUpdateConstraints()
}
}
/// The content view to be assigned a view of the content view controller of `FloatingPanelController`
public weak var contentView: UIView?
@@ -109,19 +112,26 @@ public class SurfaceView: UIView {
public override var backgroundColor: UIColor? {
get { return appearance.backgroundColor }
set { appearance.backgroundColor = newValue; setNeedsLayout() }
set {
appearance.backgroundColor = newValue
setNeedsLayout()
}
}
/// The appearance settings for a surface view.
public var appearance = SurfaceAppearance() { didSet {
shadowLayers = appearance.shadows.map { _ in CAShapeLayer() }
setNeedsLayout()
}}
public var appearance = SurfaceAppearance() {
didSet {
shadowLayers = appearance.shadows.map { _ in CAShapeLayer() }
setNeedsLayout()
}
}
/// The margins to use when laying out the container view wrapping content.
public var containerMargins: UIEdgeInsets = .zero { didSet {
setNeedsUpdateConstraints()
} }
public var containerMargins: UIEdgeInsets = .zero {
didSet {
setNeedsUpdateConstraints()
}
}
/// The view that displays an actual surface shape.
///
@@ -131,7 +141,7 @@ public class SurfaceView: UIView {
/// content view.
public let containerView: UIView = UIView()
var containerOverflow: CGFloat = 0.0 { // Must not call setNeedsLayout()
var containerOverflow: CGFloat = 0.0 { // Must not call setNeedsLayout()
didSet {
// Calling setNeedsUpdateConstraints() is necessary to fix a layout break
// when the contentMode is changed after laying out a panel, for instance,
@@ -143,10 +153,12 @@ public class SurfaceView: UIView {
var position: FloatingPanelPosition = .bottom {
didSet {
guard position != oldValue else { return }
NSLayoutConstraint.deactivate([grabberHandleEdgePaddingConstraint,
grabberHandleCenterConstraint,
grabberHandleWidthConstraint,
grabberHandleHeightConstraint])
NSLayoutConstraint.deactivate([
grabberHandleEdgePaddingConstraint,
grabberHandleCenterConstraint,
grabberHandleWidthConstraint,
grabberHandleHeightConstraint,
])
switch position {
case .top:
grabberHandleEdgePaddingConstraint = grabberHandle.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -grabberHandlePadding)
@@ -169,10 +181,12 @@ public class SurfaceView: UIView {
grabberHandleWidthConstraint = grabberHandle.widthAnchor.constraint(equalToConstant: grabberHandleSize.height)
grabberHandleHeightConstraint = grabberHandle.heightAnchor.constraint(equalToConstant: grabberHandleSize.width)
}
NSLayoutConstraint.activate([grabberHandleEdgePaddingConstraint,
grabberHandleCenterConstraint,
grabberHandleWidthConstraint,
grabberHandleHeightConstraint])
NSLayoutConstraint.activate([
grabberHandleEdgePaddingConstraint,
grabberHandleCenterConstraint,
grabberHandleWidthConstraint,
grabberHandleHeightConstraint,
])
setNeedsUpdateConstraints()
}
}
@@ -180,17 +194,25 @@ public class SurfaceView: UIView {
var grabberAreaFrame: CGRect {
switch position {
case .top:
return CGRect(origin: .init(x: bounds.minX, y: bounds.maxY - grabberAreaOffset),
size: .init(width: bounds.width, height: grabberAreaOffset))
return CGRect(
origin: .init(x: bounds.minX, y: bounds.maxY - grabberAreaOffset),
size: .init(width: bounds.width, height: grabberAreaOffset)
)
case .left:
return CGRect(origin: .init(x: bounds.maxX - grabberAreaOffset, y: bounds.minY),
size: .init(width: grabberAreaOffset, height: bounds.height))
return CGRect(
origin: .init(x: bounds.maxX - grabberAreaOffset, y: bounds.minY),
size: .init(width: grabberAreaOffset, height: bounds.height)
)
case .bottom:
return CGRect(origin: CGPoint(x: bounds.minX, y: bounds.minY),
size: CGSize(width: bounds.width, height: grabberAreaOffset))
return CGRect(
origin: CGPoint(x: bounds.minX, y: bounds.minY),
size: CGSize(width: bounds.width, height: grabberAreaOffset)
)
case .right:
return CGRect(origin: .init(x: bounds.minX, y: bounds.minY),
size: .init(width: grabberAreaOffset, height: bounds.height))
return CGRect(
origin: .init(x: bounds.minX, y: bounds.minY),
size: .init(width: grabberAreaOffset, height: bounds.height)
)
}
}
@@ -210,7 +232,7 @@ public class SurfaceView: UIView {
private lazy var grabberHandleWidthConstraint = grabberHandle.widthAnchor.constraint(equalToConstant: grabberHandleSize.width)
private lazy var grabberHandleHeightConstraint = grabberHandle.heightAnchor.constraint(equalToConstant: grabberHandleSize.height)
private lazy var grabberHandleCenterConstraint = grabberHandle.centerXAnchor.constraint(equalTo: centerXAnchor)
private lazy var grabberHandleCenterConstraint = grabberHandle.centerXAnchor.constraint(equalTo: centerXAnchor)
private lazy var grabberHandleEdgePaddingConstraint = grabberHandle.topAnchor.constraint(equalTo: topAnchor, constant: grabberHandlePadding)
private var shadowLayers: [CALayer] = [] {
@@ -244,27 +266,31 @@ public class SurfaceView: UIView {
addSubview(containerView)
containerView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
containerViewTopConstraint,
containerViewLeftConstraint,
containerViewBottomConstraint,
containerViewRightConstraint,
].map {
$0.identifier = "FloatingPanel-surface-container"
return $0;
})
NSLayoutConstraint.activate(
[
containerViewTopConstraint,
containerViewLeftConstraint,
containerViewBottomConstraint,
containerViewRightConstraint,
].map {
$0.identifier = "FloatingPanel-surface-container"
return $0
}
)
addSubview(grabberHandle)
grabberHandle.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
grabberHandleEdgePaddingConstraint,
grabberHandleCenterConstraint,
grabberHandleWidthConstraint,
grabberHandleHeightConstraint,
].map {
$0.identifier = "FloatingPanel-surface-grabber"
return $0;
})
NSLayoutConstraint.activate(
[
grabberHandleEdgePaddingConstraint,
grabberHandleCenterConstraint,
grabberHandleWidthConstraint,
grabberHandleHeightConstraint,
].map {
$0.identifier = "FloatingPanel-surface-grabber"
return $0
}
)
shadowLayers = appearance.shadows.map { _ in CALayer() }
}
@@ -312,14 +338,14 @@ public class SurfaceView: UIView {
case .left, .right:
grabberHandleWidthConstraint.constant = grabberHandleSize.height
grabberHandleHeightConstraint.constant = grabberHandleSize.width
}
}
super.updateConstraints()
}
public override func layoutSubviews() {
super.layoutSubviews()
os_log(msg, log: devLog, type: .debug, "surface view frame = \(frame)")
log.debug("surface view frame = \(frame)")
containerView.backgroundColor = appearance.backgroundColor
@@ -333,8 +359,10 @@ public class SurfaceView: UIView {
public override var intrinsicContentSize: CGSize {
let fittingSize = UIView.layoutFittingCompressedSize
let contentSize = contentView?.systemLayoutSizeFitting(fittingSize) ?? .zero
return CGSize(width: containerMargins.horizontalInset + contentPadding.horizontalInset + contentSize.width,
height: containerMargins.verticalInset + contentPadding.verticalInset + contentSize.height)
return CGSize(
width: containerMargins.horizontalInset + contentPadding.horizontalInset + contentSize.width,
height: containerMargins.verticalInset + contentPadding.verticalInset + contentSize.height
)
}
private func updateShadow() {
@@ -349,8 +377,10 @@ public class SurfaceView: UIView {
let spread = shadow.spread
let shadowRect = containerView.frame.insetBy(dx: -spread, dy: -spread)
let shadowPath = UIBezierPath.path(roundedRect: shadowRect,
appearance: appearance)
let shadowPath = UIBezierPath.path(
roundedRect: shadowRect,
appearance: appearance
)
shadowLayer.shadowPath = shadowPath.cgPath
shadowLayer.shadowColor = shadow.color.cgColor
shadowLayer.shadowOffset = shadow.offset
@@ -359,11 +389,19 @@ public class SurfaceView: UIView {
shadowLayer.shadowOpacity = shadow.opacity
let mask = CAShapeLayer()
let path = UIBezierPath.path(roundedRect: containerView.frame,
appearance: appearance)
let path = UIBezierPath.path(
roundedRect: containerView.frame,
appearance: appearance
)
let size = window?.bounds.size ?? CGSize(width: 1000.0, height: 1000.0)
path.append(UIBezierPath(rect: layer.bounds.insetBy(dx: -size.width,
dy: -size.height)))
path.append(
UIBezierPath(
rect: layer.bounds.insetBy(
dx: -size.width,
dy: -size.height
)
)
)
mask.fillRule = .evenOdd
mask.path = path.cgPath
if #available(iOS 13.0, *) {
@@ -383,24 +421,31 @@ public class SurfaceView: UIView {
}
containerView.layer.masksToBounds = true
if position.inset(containerMargins) != 0 {
containerView.layer.maskedCorners = [
.layerMinXMinYCorner, .layerMaxXMinYCorner,
.layerMinXMaxYCorner, .layerMaxXMaxYCorner
]
if #available(iOS 11, *) {
containerView.layer.maskedCorners = [
.layerMinXMinYCorner, .layerMaxXMinYCorner,
.layerMinXMaxYCorner, .layerMaxXMaxYCorner,
]
}
return
}
// 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]
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.
}
}
@@ -412,46 +457,33 @@ public class SurfaceView: UIView {
func set(contentView: UIView, mode: FloatingPanelController.ContentMode) {
containerView.addSubview(contentView)
self.contentView = contentView
/* contentView.frame = bounds */ // MUST NOT: Because the top safe area inset of a content VC will be incorrect.
/**
// MUST NOT: Because the top safe area inset of a content VC will be incorrect.
contentView.frame = bounds
*/
contentView.translatesAutoresizingMaskIntoConstraints = false
let topConstraint = contentView.topAnchor.constraint(equalTo: topAnchor, constant: containerMargins.top + contentPadding.top)
let leftConstraint = contentView.leftAnchor.constraint(equalTo: leftAnchor, constant: containerMargins.left + contentPadding.left)
let rightConstraint = rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: containerMargins.right + contentPadding.right)
let bottomConstraint = bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: containerMargins.bottom + contentPadding.bottom)
var constraints = [
topConstraint,
leftConstraint,
rightConstraint,
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
// The reason why this priority is set to .required - 1 is #359, which fixed #294.
case .fitToBounds:
$0.priority = .required - 1
[
topConstraint,
leftConstraint,
rightConstraint,
bottomConstraint,
].map {
switch mode {
case .static:
$0.priority = .required
// The reason why this priority is set to .required - 1 is #359, which fixed #294.
case .fitToBounds:
$0.priority = .required - 1
}
$0.identifier = "FloatingPanel-surface-content"
return $0
}
$0.identifier = "FloatingPanel-surface-content"
return $0
})
)
self.contentViewTopConstraint = topConstraint
self.contentViewLeftConstraint = leftConstraint
self.contentViewRightConstraint = rightConstraint
+16 -12
View File
@@ -3,9 +3,11 @@
import UIKit
class ModalTransition: NSObject, UIViewControllerTransitioningDelegate {
func animationController(forPresented presented: UIViewController,
presenting: UIViewController,
source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
func animationController(
forPresented presented: UIViewController,
presenting: UIViewController,
source: UIViewController
) -> UIViewControllerAnimatedTransitioning? {
return ModalPresentTransition()
}
@@ -52,11 +54,11 @@ class PresentationController: UIPresentationController {
view is added unnecessarily.
*/
fpc.presentedViewController == nil
else { return }
else { return }
/*
* Layout the views managed by `FloatingPanelController` here for the
* sake of the presentation and dismissal modally from the controller.
/**
Layout the views managed by `FloatingPanelController` here for the
sake of the presentation and dismissal modally from the controller.
*/
addFloatingPanel()
@@ -72,7 +74,7 @@ class PresentationController: UIPresentationController {
guard
let containerView = self.containerView,
let fpc = presentedViewController as? FloatingPanelController
else { fatalError() }
else { fatalError() }
containerView.addSubview(fpc.view)
fpc.view.frame = containerView.bounds
@@ -84,7 +86,7 @@ class ModalPresentTransition: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
guard
let fpc = transitionContext?.viewController(forKey: .to) as? FloatingPanelController
else { fatalError()}
else { fatalError() }
let animator = fpc.animatorForPresenting(to: fpc.layout.initialState)
return TimeInterval(animator.duration)
@@ -119,7 +121,7 @@ class ModalDismissTransition: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
guard
let fpc = transitionContext?.viewController(forKey: .from) as? FloatingPanelController
else { fatalError()}
else { fatalError() }
let animator = fpc.animatorForDismissing(with: .zero)
return TimeInterval(animator.duration)
@@ -139,11 +141,13 @@ class ModalDismissTransition: NSObject, UIViewControllerAnimatedTransitioning {
fpc?.suspendTransitionAnimator(false)
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
return fpc.transitionAnimator!
guard let transitionAnimator = fpc.transitionAnimator else {
fatalError("Something wrong happened in Core.move(from:to:animated:completion)")
}
return transitionAnimator
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
self.interruptibleAnimator(using: transitionContext).startAnimation()
}
}
+35 -71
View File
@@ -1,39 +1,32 @@
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
import OSLog
import XCTest
@testable import FloatingPanel
class ControllerTests: XCTestCase {
override func setUp() {}
override func tearDown() {}
#if swift(>=5.5) // Avoid the 'No exact matches in call to initializer' build failure for OSLogStore when running this test case on iOS 13.7 using Xcode 12.5.1
func test_warningRetainCycle() throws {
guard #available(iOS 15.0, *) else {
throw XCTSkip("Unsupported iOS version: this test needs iOS 15 or later")
func test_warningRetainCycle() {
let exp = expectation(description: "Warning retain cycle")
exp.expectedFulfillmentCount = 2 // For layout & behavior logs
log.hook = { (log, level) in
if log.contains("A memory leak will occur by a retain cycle because") {
XCTAssert(level == .warning)
exp.fulfill()
}
}
let myVC = MyZombieViewController(nibName: nil, bundle: nil)
myVC.loadViewIfNeeded()
let store = try OSLogStore(scope: .currentProcessIdentifier)
let found = try store
.getEntries(
at: store.position(timeIntervalSinceLatestBoot: 0),
matching: .init(format: "subsystem == '\(Logging.subsystem)'")
)
.contains {
$0.composedMessage.contains("A memory leak occurs due to a retain cycle, as")
}
XCTAssertTrue(found)
wait(for: [exp], timeout: 10)
}
#endif
func test_addPanel() {
let rootVC = UIViewController()
rootVC.loadViewIfNeeded()
rootVC.view.bounds = .init(origin: .zero, size: .init(width: 390, height: 844))
let fpc = FloatingPanelController()
fpc.addPanel(toParent: rootVC)
XCTAssertEqual(fpc.surfaceLocation.y, fpc.surfaceLocation(for: .half).y)
@@ -41,13 +34,8 @@ class ControllerTests: XCTestCase {
XCTAssertEqual(fpc.surfaceLocation.y, fpc.surfaceLocation(for: .tip).y)
}
func test_updateLayout_willTransition() throws {
guard #available(iOS 12, *) else {
throw XCTSkip("Unsupported iOS version: this test needs iOS 12 or later")
}
if #available(iOS 17, *) {
throw XCTSkip("Unsupported iOS version: this test doesn't support iOS 17 or later")
}
@available(iOS 12.0, *)
func test_updateLayout_willTransition() {
class MyDelegate: FloatingPanelControllerDelegate {
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout {
if newCollection.userInterfaceStyle == .dark {
@@ -58,8 +46,7 @@ class ControllerTests: XCTestCase {
}
let myDelegate = MyDelegate()
let fpc = FloatingPanelController(delegate: myDelegate)
let traitCollection = UITraitCollection(traitsFrom: [fpc.traitCollection,
UITraitCollection(userInterfaceStyle: .dark)])
let traitCollection = UITraitCollection(traitsFrom: [fpc.traitCollection, UITraitCollection(userInterfaceStyle: .dark)])
XCTAssertEqual(traitCollection.userInterfaceStyle, .dark)
}
@@ -72,7 +59,7 @@ class ControllerTests: XCTestCase {
fpc.hide()
XCTAssertEqual(delegate.position, .hidden)
fpc.move(to: .full, animated: false)
XCTAssertEqual(fpc.state, .full)
XCTAssertEqual(delegate.position, .full)
@@ -205,18 +192,18 @@ class ControllerTests: XCTestCase {
XCTAssertEqual(delegate.position, .hidden)
XCTAssertEqual(fpc.surfaceLocation.y, fpc.surfaceLocation(for: .hidden).y)
}
func test_moveWithNearbyPosition() {
let delegate = FloatingPanelTestDelegate()
let fpc = FloatingPanelController(delegate: delegate)
XCTAssertEqual(delegate.position, .hidden)
fpc.showForTest()
XCTAssertEqual(fpc.nearbyState, .half)
fpc.hide()
XCTAssertEqual(fpc.nearbyState, .tip)
fpc.move(to: .full, animated: false)
XCTAssertEqual(fpc.nearbyState, .full)
XCTAssertEqual(fpc.surfaceView.frame.minY, fpc.surfaceLocation(for: .full).y)
@@ -330,40 +317,11 @@ class ControllerTests: XCTestCase {
fpc.move(to: .full, animated: false)
XCTAssertEqual(fpc.surfaceView.frame.height, fpc.view.bounds.height - fpc.surfaceLocation(for: .full).y)
fpc.move(to: .half, animated: false)
print(1 / fpc.surfaceView.fp_displayScale)
XCTAssertEqual(fpc.surfaceView.frame.height, fpc.view.bounds.height - fpc.surfaceLocation(for: .half).y)
fpc.move(to: .tip, animated: false)
XCTAssertEqual(fpc.surfaceView.frame.height, fpc.view.bounds.height - fpc.surfaceLocation(for: .tip).y)
}
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 {
@@ -381,15 +339,21 @@ private class MyZombieViewController: UIViewController, FloatingPanelLayout, Flo
return .half
}
let anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] = [
.full: FloatingPanelLayoutAnchor(absoluteInset: UIScreen.main.bounds.height == 667.0 ? 18.0 : 16.0,
edge: .top,
referenceGuide: .superview),
.half: FloatingPanelLayoutAnchor(absoluteInset: 250.0,
edge: .bottom,
referenceGuide: .superview),
.tip: FloatingPanelLayoutAnchor(absoluteInset: 60.0,
edge: .bottom,
referenceGuide: .superview),
let anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] = [
.full: FloatingPanelLayoutAnchor(
absoluteInset: UIScreen.main.bounds.height == 667.0 ? 18.0 : 16.0,
edge: .top,
referenceGuide: .superview
),
.half: FloatingPanelLayoutAnchor(
absoluteInset: 250.0,
edge: .bottom,
referenceGuide: .superview
),
.tip: FloatingPanelLayoutAnchor(
absoluteInset: 60.0,
edge: .bottom,
referenceGuide: .superview
),
]
}
+360 -441
View File
File diff suppressed because it is too large Load Diff
+1 -16
View File
@@ -1,6 +1,7 @@
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
import XCTest
@testable import FloatingPanel
class ExtensionTests: XCTestCase {
@@ -9,20 +10,4 @@ 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)
}
}
-139
View File
@@ -1,139 +0,0 @@
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
import XCTest
@testable import FloatingPanel
final class GestureTests: XCTestCase {
func test_delegateProxy_shouldRecognizeSimultaneouslyWith() throws {
class GestureDelegateProxy: NSObject, UIGestureRecognizerDelegate {
var callsOfShouldRecognizeSimultaneouslyWith = 0
func gestureRecognizer(
_ gestureRecognizer: UIGestureRecognizer,
shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer
) -> Bool {
callsOfShouldRecognizeSimultaneouslyWith += 1
return true
}
}
let fpc = FloatingPanelController()
fpc.showForTest()
let delegateProxy = GestureDelegateProxy()
// Set a proxy delegate
fpc.panGestureRecognizer.delegateProxy = delegateProxy
_ = fpc.panGestureRecognizer.delegate!.gestureRecognizer?(
UIGestureRecognizer(),
shouldRecognizeSimultaneouslyWith: UIGestureRecognizer()
)
XCTAssertEqual(delegateProxy.callsOfShouldRecognizeSimultaneouslyWith, 1)
// Check whether the default delegate method is called when the proxy delegate doesn't implement it.
XCTAssertTrue(
fpc.panGestureRecognizer.delegate!.gestureRecognizer!(
fpc.panGestureRecognizer,
shouldRequireFailureOf: FloatingPanelPanGestureRecognizer()
)
)
// Clear the proxy delegate
fpc.panGestureRecognizer.delegateProxy = nil
_ = fpc.panGestureRecognizer.delegate!.gestureRecognizer?(
UIGestureRecognizer(),
shouldRecognizeSimultaneouslyWith: UIGestureRecognizer()
)
XCTAssertEqual(delegateProxy.callsOfShouldRecognizeSimultaneouslyWith, 1)
}
func test_delegateProxy_shouldRequireFailureOf() throws {
class GestureDelegateProxy: NSObject, UIGestureRecognizerDelegate {
var callsOfShouldRequireFailureOf = 0
func gestureRecognizer(
_ gestureRecognizer: UIGestureRecognizer,
shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer
) -> Bool {
callsOfShouldRequireFailureOf += 1
return true
}
}
let fpc = FloatingPanelController()
fpc.showForTest()
let delegateProxy = GestureDelegateProxy()
// Set a proxy delegate
fpc.panGestureRecognizer.delegateProxy = delegateProxy
_ = fpc.panGestureRecognizer.delegate!.gestureRecognizer?(
UIGestureRecognizer(),
shouldRequireFailureOf: UIGestureRecognizer()
)
XCTAssertEqual(delegateProxy.callsOfShouldRequireFailureOf, 1)
// Clear the proxy delegate
fpc.panGestureRecognizer.delegateProxy = nil
_ = fpc.panGestureRecognizer.delegate!.gestureRecognizer?(
UIGestureRecognizer(),
shouldRequireFailureOf: UIGestureRecognizer()
)
XCTAssertEqual(delegateProxy.callsOfShouldRequireFailureOf, 1)
}
func test_delegateProxy_shouldBeRequiredToFailBy() throws {
class GestureDelegateProxy: NSObject, UIGestureRecognizerDelegate {
var callsOfShouldBeRequiredToFailBy = 0
func gestureRecognizer(
_ gestureRecognizer: UIGestureRecognizer,
shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer
) -> Bool {
callsOfShouldBeRequiredToFailBy += 1
return false
}
}
let fpc = FloatingPanelController()
fpc.showForTest()
let delegateProxy = GestureDelegateProxy()
fpc.panGestureRecognizer.delegateProxy = delegateProxy
_ = fpc.panGestureRecognizer.delegate!.gestureRecognizer?(
UIGestureRecognizer(),
shouldBeRequiredToFailBy: UIGestureRecognizer()
)
XCTAssertEqual(delegateProxy.callsOfShouldBeRequiredToFailBy, 1)
// Check whether the delegate method of the "proxy" object is called.
let otherPanGesture = UIPanGestureRecognizer()
otherPanGesture.name = "_UISheetInteractionBackgroundDismissRecognizer"
XCTAssertFalse(
fpc.panGestureRecognizer.delegate!.gestureRecognizer!(
fpc.panGestureRecognizer,
shouldBeRequiredToFailBy: otherPanGesture
)
)
XCTAssertEqual(delegateProxy.callsOfShouldBeRequiredToFailBy, 2)
fpc.panGestureRecognizer.delegateProxy = nil
// Check whether the delegate method of the "default" object is called.
let otherPanGesture2 = UIPanGestureRecognizer()
otherPanGesture2.name = "_UISheetInteractionBackgroundDismissRecognizer"
XCTAssertTrue(
fpc.panGestureRecognizer.delegate!.gestureRecognizer!(
fpc.panGestureRecognizer,
shouldBeRequiredToFailBy: otherPanGesture2
)
)
XCTAssertEqual(delegateProxy.callsOfShouldBeRequiredToFailBy, 2)
}
}
+397 -270
View File
@@ -1,6 +1,7 @@
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
import XCTest
@testable import FloatingPanel
class LayoutTests: XCTestCase {
@@ -20,7 +21,7 @@ class LayoutTests: XCTestCase {
let anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] = [
.full: FloatingPanelLayoutAnchor(absoluteInset: 18.0, edge: .top, referenceGuide: .safeArea),
.half: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .safeArea),
.hidden: FloatingPanelLayoutAnchor(absoluteInset: 0, edge: .bottom, referenceGuide: .superview)
.hidden: FloatingPanelLayoutAnchor(absoluteInset: 0, edge: .bottom, referenceGuide: .superview),
]
let initialState: FloatingPanelState = .hidden
let position: FloatingPanelPosition = .bottom
@@ -44,7 +45,7 @@ class LayoutTests: XCTestCase {
func test_layoutSegment_3position() {
class FloatingPanelLayout3Positions: FloatingPanelTestLayout {
override var initialState: FloatingPanelState { .half }
override var initialState: FloatingPanelState { .half }
}
fpc.layout = FloatingPanelLayout3Positions()
@@ -56,25 +57,29 @@ class LayoutTests: XCTestCase {
let minPos = CGFloat.leastNormalMagnitude
let maxPos = CGFloat.greatestFiniteMagnitude
assertLayoutSegment(fpc.floatingPanel, with: [
(#line, pos: minPos, forwardY: true, lower: nil, upper: .full),
(#line, pos: minPos, forwardY: false, lower: nil, upper: .full),
(#line, pos: fullPos, forwardY: true, lower: .full, upper: .half),
(#line, pos: fullPos, forwardY: false, lower: nil, upper: .full),
(#line, pos: halfPos, forwardY: true, lower: .half, upper: .tip),
(#line, pos: halfPos, forwardY: false, lower: .full, upper: .half),
(#line, pos: tipPos, forwardY: true, lower: .tip, upper: nil),
(#line, pos: tipPos, forwardY: false, lower: .half, upper: .tip),
(#line, pos: maxPos, forwardY: true, lower: .tip, upper: nil),
(#line, pos: maxPos, forwardY: false, lower: .tip, upper: nil),
])
assertLayoutSegment(
fpc.floatingPanel,
with: [
(#line, pos: minPos, forwardY: true, lower: nil, upper: .full),
(#line, pos: minPos, forwardY: false, lower: nil, upper: .full),
(#line, pos: fullPos, forwardY: true, lower: .full, upper: .half),
(#line, pos: fullPos, forwardY: false, lower: nil, upper: .full),
(#line, pos: halfPos, forwardY: true, lower: .half, upper: .tip),
(#line, pos: halfPos, forwardY: false, lower: .full, upper: .half),
(#line, pos: tipPos, forwardY: true, lower: .tip, upper: nil),
(#line, pos: tipPos, forwardY: false, lower: .half, upper: .tip),
(#line, pos: maxPos, forwardY: true, lower: .tip, upper: nil),
(#line, pos: maxPos, forwardY: false, lower: .tip, upper: nil),
]
)
}
func test_layoutSegment_2positions() {
class FloatingPanelLayout2Positions: FloatingPanelTestLayout {
override var initialState: FloatingPanelState { .half }
override var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring]
{ super.anchors.filter { (key, _) in key != .tip } }
override var initialState: FloatingPanelState { .half }
override var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] {
super.anchors.filter { (key, _) in key != .tip }
}
}
fpc.layout = FloatingPanelLayout2Positions()
@@ -85,23 +90,27 @@ class LayoutTests: XCTestCase {
let minPos = CGFloat.leastNormalMagnitude
let maxPos = CGFloat.greatestFiniteMagnitude
assertLayoutSegment(fpc.floatingPanel, with: [
(#line, pos: minPos, forwardY: true, lower: nil, upper: .full),
(#line, pos: minPos, forwardY: false, lower: nil, upper: .full),
(#line, pos: fullPos, forwardY: true, lower: .full, upper: .half),
(#line, pos: fullPos, forwardY: false, lower: nil, upper: .full),
(#line, pos: halfPos, forwardY: true, lower: .half, upper: nil),
(#line, pos: halfPos, forwardY: false, lower: .full, upper: .half),
(#line, pos: maxPos, forwardY: true, lower: .half, upper: nil),
(#line, pos: maxPos, forwardY: false, lower: .half, upper: nil),
])
assertLayoutSegment(
fpc.floatingPanel,
with: [
(#line, pos: minPos, forwardY: true, lower: nil, upper: .full),
(#line, pos: minPos, forwardY: false, lower: nil, upper: .full),
(#line, pos: fullPos, forwardY: true, lower: .full, upper: .half),
(#line, pos: fullPos, forwardY: false, lower: nil, upper: .full),
(#line, pos: halfPos, forwardY: true, lower: .half, upper: nil),
(#line, pos: halfPos, forwardY: false, lower: .full, upper: .half),
(#line, pos: maxPos, forwardY: true, lower: .half, upper: nil),
(#line, pos: maxPos, forwardY: false, lower: .half, upper: nil),
]
)
}
func test_layoutSegment_1positions() {
class FloatingPanelLayout1Positions: FloatingPanelTestLayout {
override var initialState: FloatingPanelState { .full }
override var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring]
{ super.anchors.filter { (key, _) in key == .full } }
override var initialState: FloatingPanelState { .full }
override var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] {
super.anchors.filter { (key, _) in key == .full }
}
}
fpc.layout = FloatingPanelLayout1Positions()
@@ -111,14 +120,17 @@ class LayoutTests: XCTestCase {
let minPos = CGFloat.leastNormalMagnitude
let maxPos = CGFloat.greatestFiniteMagnitude
assertLayoutSegment(fpc.floatingPanel, with: [
(#line, pos: minPos, forwardY: true, lower: nil, upper: .full),
(#line, pos: minPos, forwardY: false, lower: nil, upper: .full),
(#line, pos: fullPos, forwardY: true, lower: .full, upper: nil),
(#line, pos: fullPos, forwardY: false, lower: nil, upper: .full),
(#line, pos: maxPos, forwardY: true, lower: .full, upper: nil),
(#line, pos: maxPos, forwardY: false, lower: .full, upper: nil),
])
assertLayoutSegment(
fpc.floatingPanel,
with: [
(#line, pos: minPos, forwardY: true, lower: nil, upper: .full),
(#line, pos: minPos, forwardY: false, lower: nil, upper: .full),
(#line, pos: fullPos, forwardY: true, lower: .full, upper: nil),
(#line, pos: fullPos, forwardY: false, lower: nil, upper: .full),
(#line, pos: maxPos, forwardY: true, lower: .full, upper: nil),
(#line, pos: maxPos, forwardY: false, lower: .full, upper: nil),
]
)
}
func test_updateInteractiveEdgeConstraint() {
@@ -126,40 +138,50 @@ class LayoutTests: XCTestCase {
fpc.move(to: .full, animated: false)
fpc.floatingPanel.layoutAdapter.startInteraction(at: fpc.state)
fpc.floatingPanel.layoutAdapter.startInteraction(at: fpc.state) // Should be ignore
fpc.floatingPanel.layoutAdapter.startInteraction(at: fpc.state) // Should be ignore
let fullPos = fpc.surfaceLocation(for: .full).y
let tipPos = fpc.surfaceLocation(for: .tip).y
var next: CGFloat
fpc.floatingPanel.layoutAdapter.updateInteractiveEdgeConstraint(diff: -100.0,
scrollingContent: true,
allowsRubberBanding: fpc.floatingPanel.behaviorAdapter.allowsRubberBanding(for:))
fpc.floatingPanel.layoutAdapter.updateInteractiveEdgeConstraint(
diff: -100.0,
scrollingContent: true,
allowsRubberBanding: fpc.floatingPanel.behaviorAdapter.allowsRubberBanding(for:)
)
next = fpc.surfaceLocation.y
XCTAssertEqual(next, fullPos)
fpc.floatingPanel.layoutAdapter.updateInteractiveEdgeConstraint(diff: 100.0,
scrollingContent: false,
allowsRubberBanding: fpc.floatingPanel.behaviorAdapter.allowsRubberBanding(for:))
fpc.floatingPanel.layoutAdapter.updateInteractiveEdgeConstraint(
diff: 100.0,
scrollingContent: false,
allowsRubberBanding: fpc.floatingPanel.behaviorAdapter.allowsRubberBanding(for:)
)
next = fpc.surfaceLocation.y
XCTAssertEqual(next, fullPos + 100.0)
fpc.floatingPanel.layoutAdapter.updateInteractiveEdgeConstraint(diff: tipPos - fullPos,
scrollingContent: false,
allowsRubberBanding: fpc.floatingPanel.behaviorAdapter.allowsRubberBanding(for:))
fpc.floatingPanel.layoutAdapter.updateInteractiveEdgeConstraint(
diff: tipPos - fullPos,
scrollingContent: false,
allowsRubberBanding: fpc.floatingPanel.behaviorAdapter.allowsRubberBanding(for:)
)
next = fpc.surfaceLocation.y
XCTAssertEqual(next, tipPos)
fpc.floatingPanel.layoutAdapter.updateInteractiveEdgeConstraint(diff: tipPos - fullPos + 100.0,
scrollingContent: true,
allowsRubberBanding: fpc.floatingPanel.behaviorAdapter.allowsRubberBanding(for:))
fpc.floatingPanel.layoutAdapter.updateInteractiveEdgeConstraint(
diff: tipPos - fullPos + 100.0,
scrollingContent: true,
allowsRubberBanding: fpc.floatingPanel.behaviorAdapter.allowsRubberBanding(for:)
)
next = fpc.surfaceLocation.y
XCTAssertEqual(next, tipPos)
fpc.floatingPanel.layoutAdapter.updateInteractiveEdgeConstraint(diff: tipPos - fullPos + 100.0,
scrollingContent: false,
allowsRubberBanding: fpc.floatingPanel.behaviorAdapter.allowsRubberBanding(for:))
fpc.floatingPanel.layoutAdapter.updateInteractiveEdgeConstraint(
diff: tipPos - fullPos + 100.0,
scrollingContent: false,
allowsRubberBanding: fpc.floatingPanel.behaviorAdapter.allowsRubberBanding(for:)
)
next = fpc.surfaceLocation.y
XCTAssertEqual(next, tipPos + 100.0)
@@ -171,11 +193,10 @@ class LayoutTests: XCTestCase {
fpc.showForTest()
fpc.move(to: .tip, animated: false)
XCTAssertEqual(fpc.surfaceView.frame, CGRect(x: 0.0, y: -667.0 + 60.0, width: 375.0, height: 667))
XCTAssertEqual(fpc.surfaceView.containerView.frame, CGRect(x: 0.0, y: -667.0,
width: 375.0, height: 667 * 2.0))
XCTAssertEqual(fpc.surfaceView.containerView.frame, CGRect(x: 0.0, y: -667.0, width: 375.0, height: 667 * 2.0))
fpc.floatingPanel.layoutAdapter.startInteraction(at: fpc.state)
fpc.floatingPanel.layoutAdapter.startInteraction(at: fpc.state) // Should be ignore
fpc.floatingPanel.layoutAdapter.startInteraction(at: fpc.state) // Should be ignore
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.interactionConstraint?.constant, 60.0)
@@ -185,33 +206,43 @@ class LayoutTests: XCTestCase {
var pre: CGFloat
var next: CGFloat
pre = fpc.surfaceLocation.y
fpc.floatingPanel.layoutAdapter.updateInteractiveEdgeConstraint(diff: -100.0,
scrollingContent: true,
allowsRubberBanding: fpc.floatingPanel.behaviorAdapter.allowsRubberBanding(for:))
fpc.floatingPanel.layoutAdapter.updateInteractiveEdgeConstraint(
diff: -100.0,
scrollingContent: true,
allowsRubberBanding: fpc.floatingPanel.behaviorAdapter.allowsRubberBanding(for:)
)
next = fpc.surfaceLocation.y
XCTAssertEqual(next, pre)
fpc.floatingPanel.layoutAdapter.updateInteractiveEdgeConstraint(diff: 100.0,
scrollingContent: false,
allowsRubberBanding: fpc.floatingPanel.behaviorAdapter.allowsRubberBanding(for:))
fpc.floatingPanel.layoutAdapter.updateInteractiveEdgeConstraint(
diff: 100.0,
scrollingContent: false,
allowsRubberBanding: fpc.floatingPanel.behaviorAdapter.allowsRubberBanding(for:)
)
next = fpc.surfaceLocation.y
XCTAssertEqual(next, tipPos + 100.0)
fpc.floatingPanel.layoutAdapter.updateInteractiveEdgeConstraint(diff: fullPos - tipPos,
scrollingContent: false,
allowsRubberBanding: fpc.floatingPanel.behaviorAdapter.allowsRubberBanding(for:))
fpc.floatingPanel.layoutAdapter.updateInteractiveEdgeConstraint(
diff: fullPos - tipPos,
scrollingContent: false,
allowsRubberBanding: fpc.floatingPanel.behaviorAdapter.allowsRubberBanding(for:)
)
next = fpc.surfaceLocation.y
XCTAssertEqual(next, fullPos)
fpc.floatingPanel.layoutAdapter.updateInteractiveEdgeConstraint(diff: fullPos - tipPos + 100,
scrollingContent: true,
allowsRubberBanding: fpc.floatingPanel.behaviorAdapter.allowsRubberBanding(for:))
fpc.floatingPanel.layoutAdapter.updateInteractiveEdgeConstraint(
diff: fullPos - tipPos + 100,
scrollingContent: true,
allowsRubberBanding: fpc.floatingPanel.behaviorAdapter.allowsRubberBanding(for:)
)
next = fpc.surfaceLocation.y
XCTAssertEqual(next, fullPos)
fpc.floatingPanel.layoutAdapter.updateInteractiveEdgeConstraint(diff: fullPos - tipPos + 100,
scrollingContent: false,
allowsRubberBanding: fpc.floatingPanel.behaviorAdapter.allowsRubberBanding(for:))
fpc.floatingPanel.layoutAdapter.updateInteractiveEdgeConstraint(
diff: fullPos - tipPos + 100,
scrollingContent: false,
allowsRubberBanding: fpc.floatingPanel.behaviorAdapter.allowsRubberBanding(for:)
)
next = fpc.surfaceLocation.y
XCTAssertEqual(next, fullPos + 100.0)
@@ -238,21 +269,27 @@ class LayoutTests: XCTestCase {
var next: CGFloat
fpc.floatingPanel.layoutAdapter.updateInteractiveEdgeConstraint(diff: -100.0,
scrollingContent: true,
allowsRubberBanding: fpc.floatingPanel.behaviorAdapter.allowsRubberBanding(for:))
fpc.floatingPanel.layoutAdapter.updateInteractiveEdgeConstraint(
diff: -100.0,
scrollingContent: true,
allowsRubberBanding: fpc.floatingPanel.behaviorAdapter.allowsRubberBanding(for:)
)
next = fpc.surfaceLocation.y
XCTAssertEqual(next, fullPos)
fpc.floatingPanel.layoutAdapter.updateInteractiveEdgeConstraint(diff: -100.0,
scrollingContent: false,
allowsRubberBanding: fpc.floatingPanel.behaviorAdapter.allowsRubberBanding(for:))
fpc.floatingPanel.layoutAdapter.updateInteractiveEdgeConstraint(
diff: -100.0,
scrollingContent: false,
allowsRubberBanding: fpc.floatingPanel.behaviorAdapter.allowsRubberBanding(for:)
)
next = fpc.surfaceLocation.y
XCTAssertEqual(next, fullPos - 100.0)
fpc.floatingPanel.layoutAdapter.updateInteractiveEdgeConstraint(diff: hiddenPos - fullPos + 100.0,
scrollingContent: false,
allowsRubberBanding: fpc.floatingPanel.behaviorAdapter.allowsRubberBanding(for:))
fpc.floatingPanel.layoutAdapter.updateInteractiveEdgeConstraint(
diff: hiddenPos - fullPos + 100.0,
scrollingContent: false,
allowsRubberBanding: fpc.floatingPanel.behaviorAdapter.allowsRubberBanding(for:)
)
next = fpc.surfaceLocation.y
XCTAssertEqual(next, hiddenPos + 100.0)
@@ -279,21 +316,27 @@ class LayoutTests: XCTestCase {
var next: CGFloat
fpc.floatingPanel.layoutAdapter.updateInteractiveEdgeConstraint(diff: 100.0,
scrollingContent: true,
allowsRubberBanding: fpc.floatingPanel.behaviorAdapter.allowsRubberBanding(for:))
fpc.floatingPanel.layoutAdapter.updateInteractiveEdgeConstraint(
diff: 100.0,
scrollingContent: true,
allowsRubberBanding: fpc.floatingPanel.behaviorAdapter.allowsRubberBanding(for:)
)
next = fpc.surfaceLocation.y
XCTAssertEqual(next, fullPos)
fpc.floatingPanel.layoutAdapter.updateInteractiveEdgeConstraint(diff: 100.0,
scrollingContent: false,
allowsRubberBanding: fpc.floatingPanel.behaviorAdapter.allowsRubberBanding(for:))
fpc.floatingPanel.layoutAdapter.updateInteractiveEdgeConstraint(
diff: 100.0,
scrollingContent: false,
allowsRubberBanding: fpc.floatingPanel.behaviorAdapter.allowsRubberBanding(for:)
)
next = fpc.surfaceLocation.y
XCTAssertEqual(next, fullPos + 100.0)
fpc.floatingPanel.layoutAdapter.updateInteractiveEdgeConstraint(diff: hiddenPos - fullPos + 100.0,
scrollingContent: false,
allowsRubberBanding: fpc.floatingPanel.behaviorAdapter.allowsRubberBanding(for:))
fpc.floatingPanel.layoutAdapter.updateInteractiveEdgeConstraint(
diff: hiddenPos - fullPos + 100.0,
scrollingContent: false,
allowsRubberBanding: fpc.floatingPanel.behaviorAdapter.allowsRubberBanding(for:)
)
next = fpc.surfaceLocation.y
XCTAssertEqual(next, hiddenPos + 100.0)
@@ -317,21 +360,27 @@ class LayoutTests: XCTestCase {
let tipPos = fpc.surfaceLocation(for: .tip).y
var next: CGFloat
fpc.floatingPanel.layoutAdapter.updateInteractiveEdgeConstraint(diff: -100.0,
scrollingContent: true,
allowsRubberBanding: fpc.floatingPanel.behaviorAdapter.allowsRubberBanding(for:))
fpc.floatingPanel.layoutAdapter.updateInteractiveEdgeConstraint(
diff: -100.0,
scrollingContent: true,
allowsRubberBanding: fpc.floatingPanel.behaviorAdapter.allowsRubberBanding(for:)
)
next = fpc.surfaceLocation.y
XCTAssertEqual(next, fullPos)
fpc.floatingPanel.layoutAdapter.updateInteractiveEdgeConstraint(diff: -100.0,
scrollingContent: false,
allowsRubberBanding: fpc.floatingPanel.behaviorAdapter.allowsRubberBanding(for:))
fpc.floatingPanel.layoutAdapter.updateInteractiveEdgeConstraint(
diff: -100.0,
scrollingContent: false,
allowsRubberBanding: fpc.floatingPanel.behaviorAdapter.allowsRubberBanding(for:)
)
next = fpc.surfaceLocation.y
XCTAssertEqual(next, fullPos - 100)
fpc.floatingPanel.layoutAdapter.updateInteractiveEdgeConstraint(diff: tipPos - fullPos + 100.0,
scrollingContent: false,
allowsRubberBanding: fpc.floatingPanel.behaviorAdapter.allowsRubberBanding(for:))
fpc.floatingPanel.layoutAdapter.updateInteractiveEdgeConstraint(
diff: tipPos - fullPos + 100.0,
scrollingContent: false,
allowsRubberBanding: fpc.floatingPanel.behaviorAdapter.allowsRubberBanding(for:)
)
next = fpc.surfaceLocation.y
XCTAssertEqual(next, tipPos + 100)
@@ -366,7 +415,7 @@ class LayoutTests: XCTestCase {
XCTAssertEqual(fpc.layout.anchors.filter({ $0.value.referenceGuide != .safeArea }).count, 0)
XCTAssertEqual(fpc.surfaceLocation(for: .full).y, myLayout.fullInset + fpc.fp_safeAreaInsets.top)
XCTAssertEqual(fpc.surfaceLocation(for: .half).y, bounds.height - myLayout.halfInset + fpc.fp_safeAreaInsets.bottom)
XCTAssertEqual(fpc.surfaceLocation(for: .tip).y, bounds.height - myLayout.tipInset + fpc.fp_safeAreaInsets.bottom)
XCTAssertEqual(fpc.surfaceLocation(for: .tip).y, bounds.height - myLayout.tipInset + fpc.fp_safeAreaInsets.bottom)
XCTAssertEqual(fpc.surfaceLocation(for: .hidden).y, bounds.height + 100.0)
}
@@ -375,7 +424,7 @@ class LayoutTests: XCTestCase {
fpc.loadViewIfNeeded()
fpc.view.frame = CGRect(x: 0, y: 0, width: 375, height: 667)
class MyFloatingPanelFullLayout: FloatingPanelTop2BottomTestLayout { }
class MyFloatingPanelFullLayout: FloatingPanelTop2BottomTestLayout {}
class MyFloatingPanelSafeAreaLayout: FloatingPanelTop2BottomTestLayout {
override var referenceGuide: FloatingPanelLayoutReferenceGuide {
return .safeArea
@@ -389,10 +438,9 @@ class LayoutTests: XCTestCase {
XCTAssertEqual(fpc.layout.anchors.filter({ $0.value.referenceGuide != .superview }).count, 0)
XCTAssertEqual(fpc.surfaceLocation(for: .full).y, bounds.height - myLayout.fullInset)
XCTAssertEqual(fpc.surfaceLocation(for: .half).y, myLayout.halfInset)
XCTAssertEqual(fpc.surfaceLocation(for: .tip).y, myLayout.tipInset)
XCTAssertEqual(fpc.surfaceLocation(for: .tip).y, myLayout.tipInset)
XCTAssertEqual(fpc.surfaceLocation(for: .hidden).y, -100.0)
fpc.layout = MyFloatingPanelSafeAreaLayout()
XCTAssertEqual(fpc.layout.anchors.filter({ $0.value.referenceGuide != .safeArea }).count, 0)
@@ -410,56 +458,93 @@ 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.view.safeAreaLayoutGuide.topAnchor)),
(anchor: FloatingPanelLayoutAnchor(absoluteInset: 100.0, edge: .top, referenceGuide: .safeArea),
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)),
(
anchor: FloatingPanelLayoutAnchor(absoluteInset: 0.0, edge: .top, referenceGuide: .safeArea),
result: (#line, constant: 0.0, firstAnchor: fpc.surfaceView.bottomAnchor, secondAnchor: fpc.fp_safeAreaLayoutGuide.topAnchor)
),
(
anchor: FloatingPanelLayoutAnchor(absoluteInset: 100.0, edge: .top, referenceGuide: .safeArea),
result: (#line, constant: 100.0, firstAnchor: fpc.surfaceView.bottomAnchor, secondAnchor: fpc.fp_safeAreaLayoutGuide.topAnchor)
),
(
anchor: FloatingPanelLayoutAnchor(absoluteInset: 0.0, edge: .top, referenceGuide: .superview),
result: (#line, constant: 0.0, firstAnchor: fpc.surfaceView.bottomAnchor, secondAnchor: fpc.view.topAnchor)
),
(
anchor: FloatingPanelLayoutAnchor(absoluteInset: 100.0, edge: .top, referenceGuide: .superview),
result: (#line, constant: 100.0, firstAnchor: fpc.surfaceView.bottomAnchor, secondAnchor: fpc.view.topAnchor)
),
// from bottom edge
(anchor: FloatingPanelLayoutAnchor(absoluteInset: 0.0, edge: .bottom, referenceGuide: .safeArea),
result: (#line, constant: 0.0, firstAnchor: fpc.view.safeAreaLayoutGuide.bottomAnchor, secondAnchor: fpc.surfaceView.bottomAnchor)),
(anchor: FloatingPanelLayoutAnchor(absoluteInset: 100.0, edge: .bottom, referenceGuide: .safeArea),
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),
result: (#line, constant: 100.0, firstAnchor: fpc.view.bottomAnchor, secondAnchor: fpc.surfaceView.bottomAnchor)),
] {
let c = prop.anchor.layoutConstraints(fpc, for: position)[0]
XCTAssertEqual(c.constant, CGFloat(prop.result.constant), line: UInt(prop.result.0))
XCTAssertEqual(c.firstAnchor, prop.result.firstAnchor, line: UInt(prop.result.0))
XCTAssertEqual(c.secondAnchor, prop.result.secondAnchor, line: UInt(prop.result.0))
(
anchor: FloatingPanelLayoutAnchor(absoluteInset: 0.0, edge: .bottom, referenceGuide: .safeArea),
result: (#line, constant: 0.0, firstAnchor: fpc.fp_safeAreaLayoutGuide.bottomAnchor, secondAnchor: fpc.surfaceView.bottomAnchor)
),
(
anchor: FloatingPanelLayoutAnchor(absoluteInset: 100.0, edge: .bottom, referenceGuide: .safeArea),
result: (#line, constant: 100.0, firstAnchor: fpc.fp_safeAreaLayoutGuide.bottomAnchor, secondAnchor: fpc.surfaceView.bottomAnchor)
),
(
anchor: FloatingPanelLayoutAnchor(absoluteInset: 0.0, edge: .bottom, referenceGuide: .superview),
result: (#line, constant: 0.0, firstAnchor: fpc.view.bottomAnchor, secondAnchor: fpc.surfaceView.bottomAnchor)
),
(
anchor: FloatingPanelLayoutAnchor(absoluteInset: 100.0, edge: .bottom, referenceGuide: .superview),
result: (#line, constant: 100.0, firstAnchor: fpc.view.bottomAnchor, secondAnchor: fpc.surfaceView.bottomAnchor)
),
] {
let c = prop.anchor.layoutConstraints(fpc, for: position)[0]
XCTAssertEqual(c.constant, CGFloat(prop.result.constant), line: UInt(prop.result.0))
XCTAssertEqual(c.firstAnchor, prop.result.firstAnchor, line: UInt(prop.result.0))
XCTAssertEqual(c.secondAnchor, prop.result.secondAnchor, line: UInt(prop.result.0))
}
// fractional
for prop in [
// from top edge
(anchor: FloatingPanelLayoutAnchor(fractionalInset: 0.0, edge: .top, referenceGuide: .safeArea),
result: (#line, multiplier: 1.0, secondAnchor: nil)),
(anchor: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .top, referenceGuide: .safeArea),
result: (#line, multiplier: 0.5, secondAnchor: fpc.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),
result: (#line, multiplier: 0.5, secondAnchor: fpc.view.heightAnchor)),
(
anchor: FloatingPanelLayoutAnchor(fractionalInset: 0.0, edge: .top, referenceGuide: .safeArea),
result: (#line, multiplier: 1.0, secondAnchor: nil)
),
(
anchor: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .top, referenceGuide: .safeArea),
result: (#line, multiplier: 0.5, secondAnchor: fpc.fp_safeAreaLayoutGuide.heightAnchor)
),
(
anchor: FloatingPanelLayoutAnchor(fractionalInset: 0.0, edge: .top, referenceGuide: .superview),
result: (#line, multiplier: 1.0, secondAnchor: nil)
),
(
anchor: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .top, referenceGuide: .superview),
result: (#line, multiplier: 0.5, secondAnchor: fpc.view.heightAnchor)
),
// from bottom edge
(anchor: FloatingPanelLayoutAnchor(fractionalInset: 0.0, edge: .bottom, referenceGuide: .safeArea),
result: (#line, multiplier: 1.0, secondAnchor: nil)),
(anchor: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .safeArea),
result: (#line, multiplier: 0.5, secondAnchor: fpc.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),
result: (#line, multiplier: 0.5, secondAnchor: fpc.view.heightAnchor)),
] {
let c = prop.anchor.layoutConstraints(fpc, for: position)[0]
XCTAssertEqual(c.multiplier, CGFloat(prop.result.multiplier), line: UInt(prop.result.0))
XCTAssertTrue(c.firstAnchor is NSLayoutAnchor<NSLayoutDimension>, line: UInt(prop.result.0))
(
anchor: FloatingPanelLayoutAnchor(fractionalInset: 0.0, edge: .bottom, referenceGuide: .safeArea),
result: (#line, multiplier: 1.0, secondAnchor: nil)
),
(
anchor: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .safeArea),
result: (#line, multiplier: 0.5, secondAnchor: fpc.fp_safeAreaLayoutGuide.heightAnchor)
),
(
anchor: FloatingPanelLayoutAnchor(fractionalInset: 0.0, edge: .bottom, referenceGuide: .superview),
result: (#line, multiplier: 1.0, secondAnchor: nil)
),
(
anchor: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .superview),
result: (#line, multiplier: 0.5, secondAnchor: fpc.view.heightAnchor)
),
] {
let c = prop.anchor.layoutConstraints(fpc, for: position)[0]
XCTAssertEqual(c.multiplier, CGFloat(prop.result.multiplier), line: UInt(prop.result.0))
XCTAssertTrue(c.firstAnchor is NSLayoutAnchor<NSLayoutDimension>, line: UInt(prop.result.0))
// On iOS 10, `c.secondAnchor` can't be equal object to `prop.result.secondAnchor`
// because there is no safe area on iOS 10 and `fp_safeAreaLayoutGuide` emulates it.
if #available(iOS 11, *) {
XCTAssertEqual(c.secondAnchor, prop.result.secondAnchor, line: UInt(prop.result.0))
}
print(c)
}
}
func test_layoutAnchor_bottomPosition() {
@@ -471,57 +556,94 @@ 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.view.safeAreaLayoutGuide.topAnchor)),
(anchor: FloatingPanelLayoutAnchor(absoluteInset: 100.0, edge: .top, referenceGuide: .safeArea),
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),
result: (#line, constant: 100.0, firstAnchor: fpc.surfaceView.topAnchor, secondAnchor: fpc.view.topAnchor)),
(
anchor: FloatingPanelLayoutAnchor(absoluteInset: 0.0, edge: .top, referenceGuide: .safeArea),
result: (#line, constant: 0.0, firstAnchor: fpc.surfaceView.topAnchor, secondAnchor: fpc.fp_safeAreaLayoutGuide.topAnchor)
),
(
anchor: FloatingPanelLayoutAnchor(absoluteInset: 100.0, edge: .top, referenceGuide: .safeArea),
result: (#line, constant: 100.0, firstAnchor: fpc.surfaceView.topAnchor, secondAnchor: fpc.fp_safeAreaLayoutGuide.topAnchor)
),
(
anchor: FloatingPanelLayoutAnchor(absoluteInset: 0.0, edge: .top, referenceGuide: .superview),
result: (#line, constant: 0.0, firstAnchor: fpc.surfaceView.topAnchor, secondAnchor: fpc.view.topAnchor)
),
(
anchor: FloatingPanelLayoutAnchor(absoluteInset: 100.0, edge: .top, referenceGuide: .superview),
result: (#line, constant: 100.0, firstAnchor: fpc.surfaceView.topAnchor, secondAnchor: fpc.view.topAnchor)
),
// from bottom edge
(anchor: FloatingPanelLayoutAnchor(absoluteInset: 0.0, edge: .bottom, referenceGuide: .safeArea),
result: (#line, constant: 0.0, firstAnchor: fpc.view.safeAreaLayoutGuide.bottomAnchor, secondAnchor: fpc.surfaceView.topAnchor)),
(anchor: FloatingPanelLayoutAnchor(absoluteInset: 100.0, edge: .bottom, referenceGuide: .safeArea),
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),
result: (#line, constant: 100.0, firstAnchor: fpc.view.bottomAnchor, secondAnchor: fpc.surfaceView.topAnchor)),
] {
let c = prop.anchor.layoutConstraints(fpc, for: position)[0]
XCTAssertEqual(c.constant, CGFloat(prop.result.constant), line: UInt(prop.result.0))
XCTAssertEqual(c.firstAnchor, prop.result.firstAnchor, line: UInt(prop.result.0))
XCTAssertEqual(c.secondAnchor, prop.result.secondAnchor, line: UInt(prop.result.0))
(
anchor: FloatingPanelLayoutAnchor(absoluteInset: 0.0, edge: .bottom, referenceGuide: .safeArea),
result: (#line, constant: 0.0, firstAnchor: fpc.fp_safeAreaLayoutGuide.bottomAnchor, secondAnchor: fpc.surfaceView.topAnchor)
),
(
anchor: FloatingPanelLayoutAnchor(absoluteInset: 100.0, edge: .bottom, referenceGuide: .safeArea),
result: (#line, constant: 100.0, firstAnchor: fpc.fp_safeAreaLayoutGuide.bottomAnchor, secondAnchor: fpc.surfaceView.topAnchor)
),
(
anchor: FloatingPanelLayoutAnchor(absoluteInset: 0.0, edge: .bottom, referenceGuide: .superview),
result: (#line, constant: 0.0, firstAnchor: fpc.view.bottomAnchor, secondAnchor: fpc.surfaceView.topAnchor)
),
(
anchor: FloatingPanelLayoutAnchor(absoluteInset: 100.0, edge: .bottom, referenceGuide: .superview),
result: (#line, constant: 100.0, firstAnchor: fpc.view.bottomAnchor, secondAnchor: fpc.surfaceView.topAnchor)
),
] {
let c = prop.anchor.layoutConstraints(fpc, for: position)[0]
XCTAssertEqual(c.constant, CGFloat(prop.result.constant), line: UInt(prop.result.0))
XCTAssertEqual(c.firstAnchor, prop.result.firstAnchor, line: UInt(prop.result.0))
XCTAssertEqual(c.secondAnchor, prop.result.secondAnchor, line: UInt(prop.result.0))
}
// fractional
for prop in [
// from top edge
(anchor: FloatingPanelLayoutAnchor(fractionalInset: 0.0, edge: .top, referenceGuide: .safeArea),
result: (#line, multiplier: 1.0, secondAnchor: nil)),
(anchor: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .top, referenceGuide: .safeArea),
result: (#line, multiplier: 0.5, secondAnchor: fpc.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),
result: (#line, multiplier: 0.5, secondAnchor: fpc.view.heightAnchor)),
(
anchor: FloatingPanelLayoutAnchor(fractionalInset: 0.0, edge: .top, referenceGuide: .safeArea),
result: (#line, multiplier: 1.0, secondAnchor: nil)
),
(
anchor: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .top, referenceGuide: .safeArea),
result: (#line, multiplier: 0.5, secondAnchor: fpc.fp_safeAreaLayoutGuide.heightAnchor)
),
(
anchor: FloatingPanelLayoutAnchor(fractionalInset: 0.0, edge: .top, referenceGuide: .superview),
result: (#line, multiplier: 1.0, secondAnchor: nil)
),
(
anchor: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .top, referenceGuide: .superview),
result: (#line, multiplier: 0.5, secondAnchor: fpc.view.heightAnchor)
),
// from bottom edge
(anchor: FloatingPanelLayoutAnchor(fractionalInset: 0.0, edge: .bottom, referenceGuide: .safeArea),
result: (#line, multiplier: 1.0, secondAnchor: nil)),
(anchor: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .safeArea),
result: (#line, multiplier: 0.5, secondAnchor: fpc.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),
result: (#line, multiplier: 0.5, secondAnchor: fpc.view.heightAnchor)),
] {
let c = prop.anchor.layoutConstraints(fpc, for: position)[0]
XCTAssertEqual(c.multiplier, CGFloat(prop.result.multiplier), line: UInt(prop.result.0))
XCTAssertTrue(c.firstAnchor is NSLayoutAnchor<NSLayoutDimension>, line: UInt(prop.result.0))
(
anchor: FloatingPanelLayoutAnchor(fractionalInset: 0.0, edge: .bottom, referenceGuide: .safeArea),
result: (#line, multiplier: 1.0, secondAnchor: nil)
),
(
anchor: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .safeArea),
result: (#line, multiplier: 0.5, secondAnchor: fpc.fp_safeAreaLayoutGuide.heightAnchor)
),
(
anchor: FloatingPanelLayoutAnchor(fractionalInset: 0.0, edge: .bottom, referenceGuide: .superview),
result: (#line, multiplier: 1.0, secondAnchor: nil)
),
(
anchor: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .superview),
result: (#line, multiplier: 0.5, secondAnchor: fpc.view.heightAnchor)
),
] {
let c = prop.anchor.layoutConstraints(fpc, for: position)[0]
XCTAssertEqual(c.multiplier, CGFloat(prop.result.multiplier), line: UInt(prop.result.0))
XCTAssertTrue(c.firstAnchor is NSLayoutAnchor<NSLayoutDimension>, line: UInt(prop.result.0))
// On iOS 10, `c.secondAnchor` can't be equal object to `prop.result.secondAnchor`
// because there is no safe area on iOS 10 and `fp_safeAreaLayoutGuide` emulates it.
if #available(iOS 11, *) {
XCTAssertEqual(c.secondAnchor, prop.result.secondAnchor, line: UInt(prop.result.0))
}
print(c)
}
}
@@ -546,32 +668,52 @@ class LayoutTests: XCTestCase {
fpc.set(contentViewController: contentVC)
for prop in [
(anchor: FloatingPanelIntrinsicLayoutAnchor(absoluteOffset: 0.0, referenceGuide: .safeArea),
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.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(absoluteOffset: 0.0, referenceGuide: .safeArea),
result: (#line, constant: 420, firstAnchor: fpc.surfaceView.bottomAnchor, secondAnchor: fpc.fp_safeAreaLayoutGuide.topAnchor)
),
(
anchor: FloatingPanelIntrinsicLayoutAnchor(absoluteOffset: 42.0, referenceGuide: .safeArea),
result: (#line, constant: 420 - 42, firstAnchor: fpc.surfaceView.bottomAnchor, secondAnchor: fpc.fp_safeAreaLayoutGuide.topAnchor)
),
(
anchor: FloatingPanelIntrinsicLayoutAnchor(absoluteOffset: 0.0, referenceGuide: .superview),
result: (#line, constant: 420, firstAnchor: fpc.surfaceView.bottomAnchor, secondAnchor: fpc.view.topAnchor)
),
(
anchor: FloatingPanelIntrinsicLayoutAnchor(absoluteOffset: 42.0, referenceGuide: .superview),
result: (#line, constant: 420 - 42, firstAnchor: fpc.surfaceView.bottomAnchor, secondAnchor: fpc.view.topAnchor)
),
(anchor: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 0.0, referenceGuide: .safeArea),
result: (#line, constant: 420, firstAnchor: fpc.surfaceView.bottomAnchor, secondAnchor: fpc.view.safeAreaLayoutGuide.topAnchor)),
(anchor: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 0.5, referenceGuide: .safeArea),
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.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),
result: (#line, constant: 210, firstAnchor: fpc.surfaceView.bottomAnchor, secondAnchor: fpc.view.topAnchor)),
(anchor: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 1.0, referenceGuide: .superview),
result: (#line, constant: 0, firstAnchor: fpc.surfaceView.bottomAnchor, secondAnchor: fpc.view.topAnchor)),
] {
let c = prop.anchor.layoutConstraints(fpc, for: position)[0]
XCTAssertEqual(c.constant, CGFloat(prop.result.constant), line: UInt(prop.result.0))
XCTAssertEqual(c.firstAnchor, prop.result.firstAnchor, line: UInt(prop.result.0))
XCTAssertEqual(c.secondAnchor, prop.result.secondAnchor, line: UInt(prop.result.0))
(
anchor: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 0.0, referenceGuide: .safeArea),
result: (#line, constant: 420, firstAnchor: fpc.surfaceView.bottomAnchor, secondAnchor: fpc.fp_safeAreaLayoutGuide.topAnchor)
),
(
anchor: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 0.5, referenceGuide: .safeArea),
result: (#line, constant: 210, firstAnchor: fpc.surfaceView.bottomAnchor, secondAnchor: fpc.fp_safeAreaLayoutGuide.topAnchor)
),
(
anchor: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 1.0, referenceGuide: .safeArea),
result: (#line, constant: 0, firstAnchor: fpc.surfaceView.bottomAnchor, secondAnchor: fpc.fp_safeAreaLayoutGuide.topAnchor)
),
(
anchor: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 0.0, referenceGuide: .superview),
result: (#line, constant: 420, firstAnchor: fpc.surfaceView.bottomAnchor, secondAnchor: fpc.view.topAnchor)
),
(
anchor: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 0.5, referenceGuide: .superview),
result: (#line, constant: 210, firstAnchor: fpc.surfaceView.bottomAnchor, secondAnchor: fpc.view.topAnchor)
),
(
anchor: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 1.0, referenceGuide: .superview),
result: (#line, constant: 0, firstAnchor: fpc.surfaceView.bottomAnchor, secondAnchor: fpc.view.topAnchor)
),
] {
let c = prop.anchor.layoutConstraints(fpc, for: position)[0]
XCTAssertEqual(c.constant, CGFloat(prop.result.constant), line: UInt(prop.result.0))
XCTAssertEqual(c.firstAnchor, prop.result.firstAnchor, line: UInt(prop.result.0))
XCTAssertEqual(c.secondAnchor, prop.result.secondAnchor, line: UInt(prop.result.0))
}
}
@@ -596,67 +738,52 @@ class LayoutTests: XCTestCase {
fpc.set(contentViewController: contentVC)
for prop in [
(anchor: FloatingPanelIntrinsicLayoutAnchor(absoluteOffset: 0.0, referenceGuide: .safeArea),
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.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(absoluteOffset: 0.0, referenceGuide: .safeArea),
result: (#line, constant: -420, firstAnchor: fpc.surfaceView.topAnchor, secondAnchor: fpc.fp_safeAreaLayoutGuide.bottomAnchor)
),
(
anchor: FloatingPanelIntrinsicLayoutAnchor(absoluteOffset: 42.0, referenceGuide: .safeArea),
result: (#line, constant: -420 + 42, firstAnchor: fpc.surfaceView.topAnchor, secondAnchor: fpc.fp_safeAreaLayoutGuide.bottomAnchor)
),
(
anchor: FloatingPanelIntrinsicLayoutAnchor(absoluteOffset: 0.0, referenceGuide: .superview),
result: (#line, constant: -420, firstAnchor: fpc.surfaceView.topAnchor, secondAnchor: fpc.view.bottomAnchor)
),
(
anchor: FloatingPanelIntrinsicLayoutAnchor(absoluteOffset: 42.0, referenceGuide: .superview),
result: (#line, constant: -420 + 42, firstAnchor: fpc.surfaceView.topAnchor, secondAnchor: fpc.view.bottomAnchor)
),
(anchor: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 0.0, referenceGuide: .safeArea),
result: (#line, constant: -420, firstAnchor: fpc.surfaceView.topAnchor, secondAnchor: fpc.view.safeAreaLayoutGuide.bottomAnchor)),
(anchor: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 0.5, referenceGuide: .safeArea),
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.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),
result: (#line, constant: -210, firstAnchor: fpc.surfaceView.topAnchor, secondAnchor: fpc.view.bottomAnchor)),
(anchor: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 1.0, referenceGuide: .superview),
result: (#line, constant: 0, firstAnchor: fpc.surfaceView.topAnchor, secondAnchor: fpc.view.bottomAnchor)),
] {
let c = prop.anchor.layoutConstraints(fpc, for: position)[0]
XCTAssertEqual(c.constant, CGFloat(prop.result.constant), line: UInt(prop.result.0))
XCTAssertEqual(c.firstAnchor, prop.result.firstAnchor, line: UInt(prop.result.0))
XCTAssertEqual(c.secondAnchor, prop.result.secondAnchor, line: UInt(prop.result.0))
}
}
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
)
(
anchor: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 0.0, referenceGuide: .safeArea),
result: (#line, constant: -420, firstAnchor: fpc.surfaceView.topAnchor, secondAnchor: fpc.fp_safeAreaLayoutGuide.bottomAnchor)
),
(
anchor: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 0.5, referenceGuide: .safeArea),
result: (#line, constant: -210, firstAnchor: fpc.surfaceView.topAnchor, secondAnchor: fpc.fp_safeAreaLayoutGuide.bottomAnchor)
),
(
anchor: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 1.0, referenceGuide: .safeArea),
result: (#line, constant: 0, firstAnchor: fpc.surfaceView.topAnchor, secondAnchor: fpc.fp_safeAreaLayoutGuide.bottomAnchor)
),
(
anchor: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 0.0, referenceGuide: .superview),
result: (#line, constant: -420, firstAnchor: fpc.surfaceView.topAnchor, secondAnchor: fpc.view.bottomAnchor)
),
(
anchor: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 0.5, referenceGuide: .superview),
result: (#line, constant: -210, firstAnchor: fpc.surfaceView.topAnchor, secondAnchor: fpc.view.bottomAnchor)
),
(
anchor: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 1.0, referenceGuide: .superview),
result: (#line, constant: 0, firstAnchor: fpc.surfaceView.topAnchor, secondAnchor: fpc.view.bottomAnchor)
),
] {
let c = prop.anchor.layoutConstraints(fpc, for: position)[0]
XCTAssertEqual(c.constant, CGFloat(prop.result.constant), line: UInt(prop.result.0))
XCTAssertEqual(c.firstAnchor, prop.result.firstAnchor, line: UInt(prop.result.0))
XCTAssertEqual(c.secondAnchor, prop.result.secondAnchor, line: UInt(prop.result.0))
}
}
}
+7 -6
View File
@@ -1,23 +1,24 @@
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
import XCTest
@testable import FloatingPanel
class StateTests: XCTestCase {
override func setUp() { }
override func tearDown() { }
override func setUp() {}
override func tearDown() {}
func test_nextAndPre() {
var positions: [FloatingPanelState]
positions = [.full, .half, .tip, .hidden]
XCTAssertEqual(FloatingPanelState.full.next(in: positions), .half)
XCTAssertEqual(FloatingPanelState.full.pre(in: positions), .full)
XCTAssertEqual(FloatingPanelState.full.next(in: positions), .half)
XCTAssertEqual(FloatingPanelState.full.pre(in: positions), .full)
XCTAssertEqual(FloatingPanelState.hidden.next(in: positions), .hidden)
XCTAssertEqual(FloatingPanelState.hidden.pre(in: positions), .tip)
positions = [.full, .hidden]
XCTAssertEqual(FloatingPanelState.full.next(in: positions), .hidden)
XCTAssertEqual(FloatingPanelState.full.pre(in: positions), .full)
XCTAssertEqual(FloatingPanelState.full.next(in: positions), .hidden)
XCTAssertEqual(FloatingPanelState.full.pre(in: positions), .full)
XCTAssertEqual(FloatingPanelState.hidden.next(in: positions), .hidden)
XCTAssertEqual(FloatingPanelState.hidden.pre(in: positions), .full)
}
+51 -33
View File
@@ -1,6 +1,7 @@
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
import XCTest
@testable import FloatingPanel
class SurfaceViewTests: XCTestCase {
@@ -68,12 +69,14 @@ class SurfaceViewTests: XCTestCase {
switch position {
case .top:
XCTAssertEqual(surface.containerView.frame,
CGRect(x: 0.0, y: -height, width: 320.0, height: 480.0 * 3),
line: line)
XCTAssertEqual(surface.convert(surface.contentView?.frame ?? .zero, from: surface.containerView),
surface.bounds,
line: line)
XCTAssertEqual(
surface.containerView.frame,
CGRect(x: 0.0, y: -height, width: 320.0, height: 480.0 * 3),
line: line)
XCTAssertEqual(
surface.convert(surface.contentView?.frame ?? .zero, from: surface.containerView),
surface.bounds,
line: line)
case .bottom:
XCTAssertEqual(surface.contentView?.frame ?? .zero, surface.bounds, line: line)
default:
@@ -82,14 +85,13 @@ class SurfaceViewTests: XCTestCase {
}
}
func test_surfaceView_grabberHandle() {
XCTContext.runActivity(named: "Bottom sheet") { _ in
let surface = SurfaceView(frame: CGRect(x: 0.0, y: 0.0, width: 320.0, height: 480.0))
XCTAssertNil(surface.contentView)
surface.layoutIfNeeded()
XCTAssertEqual(surface.grabberHandle.frame.minY, 6.0)
XCTAssertEqual(surface.grabberHandle.frame.minY, 6.0)
XCTAssertEqual(surface.grabberHandle.frame.width, surface.grabberHandleSize.width)
XCTAssertEqual(surface.grabberHandle.frame.height, surface.grabberHandleSize.height)
@@ -97,7 +99,7 @@ class SurfaceViewTests: XCTestCase {
surface.grabberHandleSize = CGSize(width: 44.0, height: 12.0)
surface.setNeedsLayout()
surface.layoutIfNeeded()
XCTAssertEqual(surface.grabberHandle.frame.minY, surface.grabberHandlePadding)
XCTAssertEqual(surface.grabberHandle.frame.minY, surface.grabberHandlePadding)
XCTAssertEqual(surface.grabberHandle.frame.width, surface.grabberHandleSize.width)
XCTAssertEqual(surface.grabberHandle.frame.height, surface.grabberHandleSize.height)
}
@@ -115,7 +117,7 @@ class SurfaceViewTests: XCTestCase {
surface.grabberHandlePadding = 10.0
surface.setNeedsLayout()
surface.layoutIfNeeded()
XCTAssertEqual(surface.grabberHandle.frame.maxY, surface.bounds.maxY - surface.grabberHandlePadding)
XCTAssertEqual(surface.grabberHandle.frame.maxY, surface.bounds.maxY - surface.grabberHandlePadding)
}
}
@@ -204,7 +206,7 @@ class SurfaceViewTests: XCTestCase {
XCTAssert(surface.containerView.layer.cornerRadius == 0.0)
XCTAssert(surface.containerView.layer.masksToBounds == false)
surface.containerView.layer.cornerRadius = 12.0 // Don't change it directly
surface.containerView.layer.cornerRadius = 12.0 // Don't change it directly
XCTAssert(surface.containerView.layer.cornerRadius == 12.0)
XCTAssertFalse(surface.containerView.layer.masksToBounds == true)
@@ -231,49 +233,65 @@ class SurfaceViewTests: XCTestCase {
func test_surfaceView_grabberArea() {
let sv = SurfaceView()
sv.bounds = .init(x: 0, y: 0, width: 375, height: 500)
sv.bounds = .init(x: 0, y: 0, width: 375, height: 500)
sv.grabberAreaOffset = 44.0
// Top
do {
sv.position = .top
XCTAssertEqual(sv.grabberAreaFrame,
.init(x: 0,
y: sv.bounds.height - sv.grabberAreaOffset,
width: sv.bounds.width,
height: sv.grabberAreaOffset))
XCTAssertEqual(
sv.grabberAreaFrame,
.init(
x: 0,
y: sv.bounds.height - sv.grabberAreaOffset,
width: sv.bounds.width,
height: sv.grabberAreaOffset
)
)
}
// Bottom
do {
sv.position = .bottom
XCTAssertEqual(sv.grabberAreaFrame,
.init(x: 0,
y: 0,
width: sv.bounds.width,
height: sv.grabberAreaOffset))
XCTAssertEqual(
sv.grabberAreaFrame,
.init(
x: 0,
y: 0,
width: sv.bounds.width,
height: sv.grabberAreaOffset
)
)
}
// Left
do {
sv.position = .left
XCTAssertEqual(sv.grabberAreaFrame,
.init(x: sv.bounds.width - sv.grabberAreaOffset,
y: 0,
width: sv.grabberAreaOffset,
height: sv.bounds.height))
XCTAssertEqual(
sv.grabberAreaFrame,
.init(
x: sv.bounds.width - sv.grabberAreaOffset,
y: 0,
width: sv.grabberAreaOffset,
height: sv.bounds.height
)
)
}
// Right
do {
sv.position = .right
XCTAssertEqual(sv.grabberAreaFrame,
.init(x: 0,
y: 0,
width: sv.grabberAreaOffset,
height: sv.bounds.height))
XCTAssertEqual(
sv.grabberAreaFrame,
.init(
x: 0,
y: 0,
width: sv.grabberAreaOffset,
height: sv.bounds.height
)
)
}
}
func test_surfaceView_grabberAreaDetection() {
let sv = SurfaceView()
sv.bounds = .init(x: 0, y: 0, width: 375, height: 500)
sv.bounds = .init(x: 0, y: 0, width: 375, height: 500)
// Top
do {
sv.position = .top
+4 -14
View File
@@ -1,6 +1,7 @@
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
import Foundation
@testable import FloatingPanel
func waitRunLoop(secs: TimeInterval = 0) {
@@ -40,7 +41,7 @@ class FloatingPanelTestLayout: FloatingPanelLayout {
var referenceGuide: FloatingPanelLayoutReferenceGuide {
return .superview
}
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] {
return [
.full: FloatingPanelLayoutAnchor(absoluteInset: fullInset, edge: .top, referenceGuide: referenceGuide),
.half: FloatingPanelLayoutAnchor(absoluteInset: halfInset, edge: .bottom, referenceGuide: referenceGuide),
@@ -63,7 +64,7 @@ class FloatingPanelTop2BottomTestLayout: FloatingPanelLayout {
var referenceGuide: FloatingPanelLayoutReferenceGuide {
return .superview
}
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] {
return [
.full: FloatingPanelLayoutAnchor(absoluteInset: fullInset, edge: .bottom, referenceGuide: referenceGuide),
.half: FloatingPanelLayoutAnchor(absoluteInset: halfInset, edge: .top, referenceGuide: referenceGuide),
@@ -72,18 +73,8 @@ 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 {
func shouldProjectMomentum(_ fpc: FloatingPanelController, to proposedTargetPosition: FloatingPanelState) -> Bool {
return true
}
}
@@ -108,4 +99,3 @@ class MockTransitionCoordinator: NSObject, UIViewControllerTransitionCoordinator
var containerView: UIView { UIView() }
var targetTransform: CGAffineTransform = .identity
}
+50
View File
@@ -0,0 +1,50 @@
{
"pins" : [
{
"identity" : "swift-argument-parser",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-argument-parser.git",
"state" : {
"revision" : "9f39744e025c7d377987f30b03770805dcb0bcd1",
"version" : "1.1.4"
}
},
{
"identity" : "swift-format",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-format.git",
"state" : {
"branch" : "508.0.1",
"revision" : "fbfe1869527923dd9f9b2edac148baccfce0dce7"
}
},
{
"identity" : "swift-syntax",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-syntax.git",
"state" : {
"revision" : "cd793adf5680e138bf2bcbaacc292490175d0dcd",
"version" : "508.0.0"
}
},
{
"identity" : "swift-system",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-system.git",
"state" : {
"revision" : "836bc4557b74fe6d2660218d56e3ce96aff76574",
"version" : "1.1.1"
}
},
{
"identity" : "swift-tools-support-core",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-tools-support-core.git",
"state" : {
"revision" : "93784c59434dbca8e8a9e4b700d0d6d94551da6a",
"version" : "0.5.2"
}
}
],
"version" : 2
}
+37
View File
@@ -0,0 +1,37 @@
// swift-tools-version: 5.8
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "build-tools",
platforms: [.macOS(.v12)],
products: [
.plugin(name: "SwiftFormatCommand", targets: ["SwiftFormatCommand"]),
.plugin(name: "SwiftFormatBuildTool", targets: ["SwiftFormatBuildTool"]),
],
dependencies: [
.package(url: "https://github.com/apple/swift-format.git", branch: "508.0.1")
],
targets: [
.plugin(
name: "SwiftFormatCommand",
capability: .command(
intent: .sourceCodeFormatting(),
permissions: [
.writeToPackageDirectory(reason: "")
]
),
dependencies: [
.product(name: "swift-format", package: "swift-format")
]
),
.plugin(
name: "SwiftFormatBuildTool",
capability: .buildTool(),
dependencies: [
.product(name: "swift-format", package: "swift-format")
]
),
]
)
@@ -0,0 +1,52 @@
import Foundation
import PackagePlugin
@main
struct SwiftFormatBuildTool: BuildToolPlugin {
func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] {
debugPrint("BuildToolPlugin -> \(context.package.directory)")
let configFile = context.package.directory.appending(".swift-format")
return [
.prebuildCommand(
displayName: "Run swift-format",
executable: try context.tool(named: "swift-format").path,
arguments: [
"lint",
"--configuration",
configFile.string,
"-r",
"\(context.pluginWorkDirectory.string)",
],
outputFilesDirectory: context.package.directory
)
]
}
}
#if canImport(XcodeProjectPlugin)
import XcodeProjectPlugin
extension SwiftFormatBuildTool: XcodeBuildToolPlugin {
func createBuildCommands(context: XcodePluginContext, target: XcodeTarget) throws -> [Command] {
debugPrint("XcodeBuildToolPlugin -> \(context.xcodeProject.directory.string)")
let configFile = context.xcodeProject.directory.appending(".swift-format")
let sourceFiles = context.xcodeProject.directory.appending("Sources")
let testFiles = context.xcodeProject.directory.appending("Tests")
return [
.buildCommand(
displayName: "Run swift-format(xcode)",
executable: try context.tool(named: "swift-format").path,
arguments: [
"lint",
"--configuration",
configFile.string,
"-r",
"\(sourceFiles.string)",
"\(testFiles.string)",
],
inputFiles: [],
outputFiles: []
)
]
}
}
#endif
@@ -0,0 +1,72 @@
import Foundation
import PackagePlugin
@main
struct SwiftFormatCommand: CommandPlugin {
func performCommand(context: PluginContext, arguments: [String]) async throws {
debugPrint("CommandPlugin start: \(context)")
let swiftFormatTool = try context.tool(named: "swift-format")
debugPrint("XcodeCommandPlugin: swift-format: \(swiftFormatTool.path)")
let configFile = context.package.directory.appending(".swift-format")
debugPrint("CommandPlugin using config: \(configFile)")
var argExtractor = ArgumentExtractor(arguments)
let targetNames = argExtractor.extractOption(named: "target")
let targetsToFormat = try context.package.targets(named: targetNames)
let sourceCodeTargets = targetsToFormat.compactMap { $0 as? SourceModuleTarget }
try runCommand(swiftFormatTool, configFile: configFile, filePaths: sourceCodeTargets.map(\.directory))
}
func runCommand(_ swiftFormatTool: PackagePlugin.PluginContext.Tool, configFile: Path, filePaths: [Path]) throws {
// Invoke `swift-format` on the target directory, passing a configuration
// file from the package directory.
let swiftFormatExec = URL(fileURLWithPath: swiftFormatTool.path.string)
let swiftFormatArgs =
[
"--configuration",
"\(configFile.string)",
"--in-place",
"--recursive",
] + filePaths.map(\.string)
let process = try Process.run(swiftFormatExec, arguments: swiftFormatArgs)
process.waitUntilExit()
debugPrint("result: \(process.terminationStatus)")
if process.terminationReason == .exit && process.terminationStatus == 0 {
print("success.")
} else {
let problem = "\(process.terminationReason):\(process.terminationStatus)"
Diagnostics.error("failed: \(problem)")
}
}
}
#if canImport(XcodeProjectPlugin)
import XcodeProjectPlugin
extension SwiftFormatCommand: XcodeCommandPlugin {
func performCommand(context: XcodeProjectPlugin.XcodePluginContext, arguments: [String]) throws {
debugPrint("start")
let swiftFormatTool = try context.tool(named: "swift-format")
debugPrint("swift-format executable = \(swiftFormatTool.path)")
let configFile = context.xcodeProject.directory.appending(".swift-format")
debugPrint("config file = \(swiftFormatTool.path)")
var argExtractor = ArgumentExtractor(arguments)
let targetNames = argExtractor.extractOption(named: "target")
let xcodeTargets = context.xcodeProject.targets.filter { targetNames.contains($0.product?.name ?? "") }
let filePaths = xcodeTargets.flatMap(\.inputFiles).filter { $0.type == .source }.map(\.path)
debugPrint("files to format = \(filePaths.map(\.lastComponent))")
try runCommand(swiftFormatTool, configFile: configFile, filePaths: filePaths)
}
}
#endif