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
35 changed files with 1971 additions and 1523 deletions
+31 -4
View File
@@ -4,12 +4,39 @@ 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.runsOn }}
env:
@@ -52,10 +79,10 @@ jobs:
fail-fast: false
matrix:
include:
- os: "16.4"
xcode: "14.3.1"
- os: "16.1"
xcode: "14.1"
sim: "iPhone 14 Pro"
runsOn: macos-13
runsOn: macos-12
- os: "15.5"
xcode: "13.4.1"
sim: "iPhone 13 Pro"
-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
}
+1 -1
View File
@@ -319,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
}
}
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="21701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="RoN-h0-uBD">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="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="21679"/>
<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,7 +780,7 @@ 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>
<!--Adaptive Layout Test View Controller-->
<scene sceneID="rDI-lU-wEx">
@@ -37,14 +37,6 @@ final class DebugTextViewController: UIViewController, UITextViewDelegate {
}
}
@IBAction func toggleTopMargin(_ sender: UISwitch) {
if sender.isOn {
textViewTopConstraint.constant = 160
} else {
textViewTopConstraint.constant = 16
}
}
@IBAction func close(sender: UIButton) {
// (self.parent as? FloatingPanelController)?.removePanelFromParent(animated: true, completion: nil)
dismiss(animated: true, completion: nil)
@@ -10,7 +10,6 @@ enum UseCase: Int, CaseIterable {
case showPanelModal
case showMultiPanelModal
case showPanelInSheetModal
case showOnWindow
case showTabBar
case showPageView
case showPageContentView
@@ -35,7 +34,6 @@ extension UseCase {
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"
@@ -67,7 +65,6 @@ extension UseCase {
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))
@@ -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
@@ -158,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()
@@ -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;
+1 -1
View File
@@ -1,7 +1,7 @@
Pod::Spec.new do |s|
s.name = "FloatingPanel"
s.version = "2.7.0"
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.
+21 -11
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 */
@@ -515,7 +518,7 @@
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",
@@ -535,7 +538,7 @@
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",
@@ -651,7 +654,7 @@
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",
@@ -698,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 */;
}
+3 -3
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](https://floatingpanel.github.io/2.7.0/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)
@@ -425,7 +425,7 @@ 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
}
}
@@ -451,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
}
}
+20 -20
View File
@@ -12,43 +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 position.
///
/// The default implementation of this method returns true. This method is called for a layout to support all positions(tip, half and full).
/// Therefore, `proposedState` can only be `FloatingPanelState.tip` or `FloatingPanelState.full`.
@objc optional
func shouldProjectMomentum(_ fpc: FloatingPanelController, to proposedState: FloatingPanelState) -> Bool
/// 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
@@ -76,7 +76,7 @@ open class FloatingPanelDefaultBehavior: FloatingPanelBehavior {
open func allowsRubberBanding(for edge: UIRectEdge) -> Bool {
return false
}
open var removalInteractionVelocityThreshold: CGFloat = 5.5
}
@@ -100,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 {
@@ -121,6 +121,6 @@ class BehaviorAdapter {
extension FloatingPanelController {
var _behavior: FloatingPanelBehavior {
get { floatingPanel.behaviorAdapter.behavior }
set { floatingPanel.behaviorAdapter.behavior = newValue}
set { floatingPanel.behaviorAdapter.behavior = newValue }
}
}
+77 -84
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,8 +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
optional func floatingPanel(_ fpc: FloatingPanelController, contentOffsetForPinning trackingScrollView: UIScrollView) -> CGPoint
}
///
@@ -120,9 +111,9 @@ open class FloatingPanelController: UIViewController {
}
/// The delegate of a panel controller object.
@objc
public weak var delegate: FloatingPanelControllerDelegate?{
didSet{
@objc
public weak var delegate: FloatingPanelControllerDelegate? {
didSet {
didUpdateDelegate()
}
}
@@ -173,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.")
}
}
}
@@ -186,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.")
}
}
}
@@ -201,7 +190,7 @@ open class FloatingPanelController: UIViewController {
/// The behavior for determining the adjusted content offsets.
///
/// This property specifies how the content area of the tracking scroll view is modified using ``adjustedContentInsets``. The default value of this property is FloatingPanelController.ContentInsetAdjustmentBehavior.always.
@objc
@objc
public var contentInsetAdjustmentBehavior: ContentInsetAdjustmentBehavior = .always
/// A Boolean value that determines whether the removal interaction is enabled.
@@ -221,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.
@@ -236,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()
@@ -275,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
}
@@ -381,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
@@ -422,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)
}
}
@@ -469,17 +451,21 @@ open class FloatingPanelController: UIViewController {
// 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.
@@ -491,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.")
@@ -508,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 }
@@ -609,7 +595,7 @@ open class FloatingPanelController: UIViewController {
break
}
}
/// [Experimental] Allows the panel to move as its tracking scroll view bounces.
///
/// This method must be called in the delegate method, `UIScrollViewDelegate.scrollViewDidScroll(_:)`,
@@ -689,34 +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 = {
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)
}
}()
}
@@ -724,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
+242 -307
View File
File diff suppressed because it is too large Load Diff
+86 -53
View File
@@ -10,7 +10,7 @@ extension CGFloat {
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)
}
}
@@ -52,12 +52,14 @@ private class CustomLayoutGuide: LayoutGuideProvider {
let rightAnchor: NSLayoutXAxisAnchor
let widthAnchor: NSLayoutDimension
let heightAnchor: NSLayoutDimension
init(topAnchor: NSLayoutYAxisAnchor,
leftAnchor: NSLayoutXAxisAnchor,
bottomAnchor: NSLayoutYAxisAnchor,
rightAnchor: NSLayoutXAxisAnchor,
widthAnchor: NSLayoutDimension,
heightAnchor: NSLayoutDimension) {
init(
topAnchor: NSLayoutYAxisAnchor,
leftAnchor: NSLayoutXAxisAnchor,
bottomAnchor: NSLayoutYAxisAnchor,
rightAnchor: NSLayoutXAxisAnchor,
widthAnchor: NSLayoutDimension,
heightAnchor: NSLayoutDimension
) {
self.topAnchor = topAnchor
self.leftAnchor = leftAnchor
self.bottomAnchor = bottomAnchor
@@ -72,23 +74,27 @@ extension UIViewController {
if #available(iOS 11.0, *) {
return view.safeAreaInsets
} else {
return UIEdgeInsets(top: topLayoutGuide.length,
left: 0.0,
bottom: bottomLayoutGuide.length,
right: 0.0)
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
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))
return CustomLayoutGuide(
topAnchor: topLayoutGuide.bottomAnchor,
leftAnchor: view.leftAnchor,
bottomAnchor: bottomLayoutGuide.topAnchor,
rightAnchor: view.rightAnchor,
widthAnchor: view.widthAnchor,
heightAnchor: topLayoutGuide.bottomAnchor.anchorWithOffset(to: bottomLayoutGuide.topAnchor)
)
}
}
}
@@ -133,9 +139,14 @@ 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)
}
}
@@ -157,7 +168,7 @@ extension UIGestureRecognizer.State: CustomDebugStringConvertible {
extension UIScrollView {
var isLocked: Bool {
return !showsVerticalScrollIndicator && !bounces && isDirectionalLockEnabled
return !showsVerticalScrollIndicator && isDirectionalLockEnabled
}
var fp_contentInset: UIEdgeInsets {
if #available(iOS 11.0, *) {
@@ -167,8 +178,10 @@ extension UIScrollView {
}
}
var fp_contentOffsetMax: CGPoint {
return CGPoint(x: max((contentSize.width + fp_contentInset.right) - bounds.width, 0.0),
y: max((contentSize.height + fp_contentInset.bottom) - bounds.height, 0.0))
return CGPoint(
x: max((contentSize.width + fp_contentInset.right) - bounds.width, 0.0),
y: max((contentSize.height + fp_contentInset.bottom) - bounds.height, 0.0)
)
}
}
@@ -208,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()
@@ -218,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)
@@ -266,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.7.0</string>
<string>2.6.2</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
</dict>
+99 -65
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),
@@ -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,14 +271,12 @@ class LayoutAdapter {
}
var offsetFromMostExpandedAnchor: CGFloat {
let offset: CGFloat
switch position {
case .top, .left:
offset = edgePosition(surfaceView.presentationFrame) - position(for: mostExpandedState)
return edgePosition(surfaceView.presentationFrame) - position(for: mostExpandedState)
case .bottom, .right:
offset = position(for: mostExpandedState) - edgePosition(surfaceView.presentationFrame)
return position(for: mostExpandedState) - edgePosition(surfaceView.presentationFrame)
}
return offset.rounded(by: surfaceView.fp_displayScale)
}
private var hiddenAnchor: FloatingPanelLayoutAnchoring {
@@ -436,10 +439,10 @@ class LayoutAdapter {
}
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
@@ -474,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)
@@ -552,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
@@ -561,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
@@ -574,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
@@ -594,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
@@ -615,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
@@ -650,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
@@ -659,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:
@@ -678,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 {
@@ -694,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)
)
}
}
@@ -706,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)
@@ -753,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)")
}
}
@@ -790,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)")
}
}
}
@@ -809,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)
+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() {}
}
+124 -79
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)
@@ -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, *) {
@@ -384,8 +422,10 @@ public class SurfaceView: UIView {
containerView.layer.masksToBounds = true
if position.inset(containerMargins) != 0 {
if #available(iOS 11, *) {
containerView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner,
.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
containerView.layer.maskedCorners = [
.layerMinXMinYCorner, .layerMaxXMinYCorner,
.layerMinXMaxYCorner, .layerMaxXMaxYCorner,
]
}
return
}
@@ -417,28 +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)
NSLayoutConstraint.activate([
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
NSLayoutConstraint.activate(
[
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 -41
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,6 +317,7 @@ 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)
@@ -351,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 -327
View File
@@ -1,38 +1,34 @@
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
import XCTest
@testable import FloatingPanel
class CoreTests: XCTestCase {
override func setUp() {}
override func tearDown() {}
func test_scrollLock() {
func test_scrolllock() {
let fpc = FloatingPanelController()
let contentVC1 = UITableViewController(nibName: nil, bundle: nil)
XCTAssertEqual(contentVC1.tableView.showsVerticalScrollIndicator, true)
XCTAssertEqual(contentVC1.tableView.bounces, true)
fpc.set(contentViewController: contentVC1)
fpc.track(scrollView: contentVC1.tableView)
fpc.showForTest()
XCTAssertEqual(fpc.state, .half)
XCTAssertEqual(contentVC1.tableView.showsVerticalScrollIndicator, false)
XCTAssertEqual(contentVC1.tableView.bounces, false)
fpc.move(to: .full, animated: false)
XCTAssertEqual(contentVC1.tableView.showsVerticalScrollIndicator, true)
XCTAssertEqual(contentVC1.tableView.bounces, true)
fpc.move(to: .tip, animated: false)
XCTAssertEqual(contentVC1.tableView.showsVerticalScrollIndicator, false)
XCTAssertEqual(contentVC1.tableView.bounces, false)
let exp1 = expectation(description: "move to full with animation")
fpc.move(to: .full, animated: true) {
XCTAssertEqual(contentVC1.tableView.showsVerticalScrollIndicator, true)
XCTAssertEqual(contentVC1.tableView.bounces, true)
exp1.fulfill()
}
wait(for: [exp1], timeout: 1.0)
@@ -40,7 +36,6 @@ class CoreTests: XCTestCase {
let exp2 = expectation(description: "move to tip with animation")
fpc.move(to: .tip, animated: false) {
XCTAssertEqual(contentVC1.tableView.showsVerticalScrollIndicator, false)
XCTAssertEqual(contentVC1.tableView.bounces, false)
exp2.fulfill()
}
wait(for: [exp2], timeout: 1.0)
@@ -48,20 +43,18 @@ class CoreTests: XCTestCase {
// Reset the content vc
let contentVC2 = UITableViewController(nibName: nil, bundle: nil)
XCTAssertEqual(contentVC2.tableView.showsVerticalScrollIndicator, true)
XCTAssertEqual(contentVC2.tableView.bounces, true)
fpc.set(contentViewController: contentVC2)
fpc.track(scrollView: contentVC2.tableView)
fpc.show(animated: false, completion: nil)
XCTAssertEqual(fpc.state, .half)
XCTAssertEqual(contentVC2.tableView.showsVerticalScrollIndicator, false)
XCTAssertEqual(contentVC2.tableView.bounces, false)
}
func test_getBackdropAlpha_1positions() {
class FloatingPanelLayout1Positions: FloatingPanelLayout {
let initialState: FloatingPanelState = .full
let position: FloatingPanelPosition = .bottom
let anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] =
let anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] =
[.full: FloatingPanelLayoutAnchor(absoluteInset: 20.0, edge: .top, referenceGuide: .superview)]
}
@@ -81,9 +74,9 @@ class CoreTests: XCTestCase {
func test_getBackdropAlpha_1positionsWithInitialHidden() {
class FloatingPanelLayout2Positions: FloatingPanelTestLayout {
override var initialState: FloatingPanelState { .hidden }
override var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
override var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] {
return [
.full: FloatingPanelLayoutAnchor(absoluteInset: fullInset, edge: .top, referenceGuide: referenceGuide),
.full: FloatingPanelLayoutAnchor(absoluteInset: fullInset, edge: .top, referenceGuide: referenceGuide)
]
}
}
@@ -96,7 +89,7 @@ class CoreTests: XCTestCase {
let fullPos = fpc.surfaceLocation(for: .full).y
let hiddenPos = fpc.surfaceLocation(for: .hidden).y
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: fullPos - 100.0, with: -100.0), 0.3)
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: fullPos - 100.0, with: -100.0), 0.3)
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: fullPos, with: 0.0), 0.3)
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: hiddenPos, with: 100.0), 0.0)
}
@@ -105,7 +98,7 @@ class CoreTests: XCTestCase {
class FloatingPanelLayout2Positions: FloatingPanelLayout {
let initialState: FloatingPanelState = .half
let position: FloatingPanelPosition = .bottom
let anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] = [
let anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] = [
.full: FloatingPanelLayoutAnchor(absoluteInset: 20.0, edge: .top, referenceGuide: .superview),
.half: FloatingPanelLayoutAnchor(absoluteInset: 250.0, edge: .bottom, referenceGuide: .superview),
]
@@ -135,7 +128,7 @@ class CoreTests: XCTestCase {
func test_getBackdropAlpha_2positionsWithHidden() {
class FloatingPanelLayout2Positions: FloatingPanelTestLayout {
override var initialState: FloatingPanelState { .hidden }
override var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
override var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] {
return [
.full: FloatingPanelLayoutAnchor(absoluteInset: fullInset, edge: .top, referenceGuide: referenceGuide),
.hidden: FloatingPanelLayoutAnchor(absoluteInset: 0.0, edge: .bottom, referenceGuide: referenceGuide),
@@ -151,7 +144,7 @@ class CoreTests: XCTestCase {
let fullPos = fpc.surfaceLocation(for: .full).y
let hiddenPos = fpc.surfaceLocation(for: .hidden).y
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: fullPos - 100.0, with: -100.0), 0.3)
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: fullPos - 100.0, with: -100.0), 0.3)
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: fullPos, with: 0.0), 0.3)
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: hiddenPos, with: 100.0), 0.0)
}
@@ -182,7 +175,6 @@ class CoreTests: XCTestCase {
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: halfPos, with: -1 * distance2), 0.0)
}
func test_updateBackdropAlpha() {
class BackdropTestLayout: FloatingPanelTestLayout {
override var initialState: FloatingPanelState { .hidden }
@@ -254,7 +246,7 @@ class CoreTests: XCTestCase {
fpc.move(to: .full, animated: false)
XCTAssertEqual(_floor(fpc.backdropView.alpha), 0.3)
fpc.willTransition(to: UITraitCollection(horizontalSizeClass: .regular), with: MockTransitionCoordinator())
XCTAssertEqual(fpc.backdropView.alpha, 0.0) // Must update the alpha by BackdropTestLayout2 in TestDelegate.
XCTAssertEqual(fpc.backdropView.alpha, 0.0) // Must update the alpha by BackdropTestLayout2 in TestDelegate.
// Test a view size change of FloatingPanelController.view
@@ -265,7 +257,7 @@ class CoreTests: XCTestCase {
delegate.layout = BackdropTestLayout2()
fpc.viewWillTransition(to: CGSize.zero, with: MockTransitionCoordinator())
XCTAssertEqual(fpc.backdropView.alpha, 0.0) // Must update the alpha by BackdropTestLayout2 in TestDelegate.
XCTAssertEqual(fpc.backdropView.alpha, 0.0) // Must update the alpha by BackdropTestLayout2 in TestDelegate.
}
func test_initial_surface_position() {
@@ -273,8 +265,9 @@ class CoreTests: XCTestCase {
class Layout: FloatingPanelLayout {
let initialState: FloatingPanelState = .full
let position: FloatingPanelPosition = .top
let anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring]
= [.full: FloatingPanelLayoutAnchor(absoluteInset: 20.0, edge: .bottom, referenceGuide: .superview)]
let anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] = [
.full: FloatingPanelLayoutAnchor(absoluteInset: 20.0, edge: .bottom, referenceGuide: .superview)
]
}
func floatingPanel(_ fpc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout {
Layout()
@@ -292,12 +285,13 @@ class CoreTests: XCTestCase {
}
}
func test_targetState_1positions() {
func test_targetPosition_1positions() {
class FloatingPanelLayout1Positions: FloatingPanelLayout {
let initialState: FloatingPanelState = .full
let position: FloatingPanelPosition = .bottom
let anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring]
= [.full: FloatingPanelLayoutAnchor(absoluteInset: 20.0, edge: .top, referenceGuide: .superview)]
let anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] = [
.full: FloatingPanelLayoutAnchor(absoluteInset: 20.0, edge: .top, referenceGuide: .superview)
]
}
let delegate = FloatingPanelTestDelegate()
@@ -309,22 +303,25 @@ class CoreTests: XCTestCase {
let fullPos = fpc.surfaceLocation(for: .full).y
fpc.move(to: .full, animated: false)
assertTargetState(fpc.floatingPanel, with: [
(#line, fullPos - 10.0, CGPoint(x: 0.0, y: 1000.0), .full), // redirect
(#line, fullPos, CGPoint(x: 0.0, y: -1000.0), .full), // redirect
(#line, fullPos, CGPoint(x: 0.0, y: -100.0), .full), // redirect
(#line, fullPos, CGPoint(x: 0.0, y: 0.0), .full),
(#line, fullPos, CGPoint(x: 0.0, y: 100.0), .full), // redirect
(#line, fullPos, CGPoint(x: 0.0, y: 1000.0), .full), // redirect
(#line, fullPos + 10.0, CGPoint(x: 0.0, y: 100.0), .full), // redirect
])
assertTargetPosition(
fpc.floatingPanel,
with: [
(#line, fullPos - 10.0, CGPoint(x: 0.0, y: 1000.0), .full), // redirect
(#line, fullPos, CGPoint(x: 0.0, y: -1000.0), .full), // redirect
(#line, fullPos, CGPoint(x: 0.0, y: -100.0), .full), // redirect
(#line, fullPos, CGPoint(x: 0.0, y: 0.0), .full),
(#line, fullPos, CGPoint(x: 0.0, y: 100.0), .full), // redirect
(#line, fullPos, CGPoint(x: 0.0, y: 1000.0), .full), // redirect
(#line, fullPos + 10.0, CGPoint(x: 0.0, y: 100.0), .full), // redirect
]
)
}
func test_targetState_2positions() {
func test_targetPosition_2positions() {
class FloatingPanelLayout2Positions: FloatingPanelLayout {
let initialState: FloatingPanelState = .half
let position: FloatingPanelPosition = .bottom
let anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] = [
let anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] = [
.full: FloatingPanelLayoutAnchor(absoluteInset: 20.0, edge: .top, referenceGuide: .superview),
.half: FloatingPanelLayoutAnchor(absoluteInset: 250.0, edge: .bottom, referenceGuide: .superview),
]
@@ -340,46 +337,52 @@ class CoreTests: XCTestCase {
let halfPos = fpc.surfaceLocation(for: .half).y
fpc.move(to: .full, animated: false)
assertTargetState(fpc.floatingPanel, with: [
(#line, fullPos - 10.0, CGPoint(x: 0.0, y: 1000.0), .half), // project to half
(#line, fullPos, CGPoint(x: 0.0, y: -1000.0), .full), // redirect
(#line, fullPos, CGPoint(x: 0.0, y: -100.0), .full),
(#line, fullPos, CGPoint(x: 0.0, y: 0.0), .full),
(#line, fullPos, CGPoint(x: 0.0, y: 100.0), .full),
(#line, fullPos, CGPoint(x: 0.0, y: 1000.0), .half), // project to half
(#line, fullPos + 10.0, CGPoint(x: 0.0, y: 100.0), .full), // redirect
(#line, halfPos - 10.0, CGPoint(x: 0.0, y: -100.0), .half), // redirect
(#line, halfPos, CGPoint(x: 0.0, y: -1000.0), .full), // project to full
(#line, halfPos, CGPoint(x: 0.0, y: -100.0), .half),
(#line, halfPos, CGPoint(x: 0.0, y: 0.0), .half),
(#line, halfPos, CGPoint(x: 0.0, y: 100.0), .half),
(#line, halfPos, CGPoint(x: 0.0, y: 1000.0), .half), // redirect
(#line, halfPos + 10.0, CGPoint(x: 0.0, y: -1000.0), .full), // project to full
])
assertTargetPosition(
fpc.floatingPanel,
with: [
(#line, fullPos - 10.0, CGPoint(x: 0.0, y: 1000.0), .half), // project to half
(#line, fullPos, CGPoint(x: 0.0, y: -1000.0), .full), // redirect
(#line, fullPos, CGPoint(x: 0.0, y: -100.0), .full),
(#line, fullPos, CGPoint(x: 0.0, y: 0.0), .full),
(#line, fullPos, CGPoint(x: 0.0, y: 100.0), .full),
(#line, fullPos, CGPoint(x: 0.0, y: 1000.0), .half), // project to half
(#line, fullPos + 10.0, CGPoint(x: 0.0, y: 100.0), .full), // redirect
(#line, halfPos - 10.0, CGPoint(x: 0.0, y: -100.0), .half), // redirect
(#line, halfPos, CGPoint(x: 0.0, y: -1000.0), .full), // project to full
(#line, halfPos, CGPoint(x: 0.0, y: -100.0), .half),
(#line, halfPos, CGPoint(x: 0.0, y: 0.0), .half),
(#line, halfPos, CGPoint(x: 0.0, y: 100.0), .half),
(#line, halfPos, CGPoint(x: 0.0, y: 1000.0), .half), // redirect
(#line, halfPos + 10.0, CGPoint(x: 0.0, y: -1000.0), .full), // project to full
]
)
fpc.move(to: .half, animated: false)
assertTargetState(fpc.floatingPanel, with: [
(#line, fullPos - 10.0, CGPoint(x: 0.0, y: 1000.0), .half), // project to half
(#line, fullPos, CGPoint(x: 0.0, y: -1000.0), .full), // redirect
(#line, fullPos, CGPoint(x: 0.0, y: -100.0), .full),
(#line, fullPos, CGPoint(x: 0.0, y: 0.0), .full),
(#line, fullPos, CGPoint(x: 0.0, y: 100.0), .full),
(#line, fullPos, CGPoint(x: 0.0, y: 1000.0), .half), // project to half
(#line, fullPos + 10.0, CGPoint(x: 0.0, y: 100.0), .full), // redirect
(#line, halfPos - 10.0, CGPoint(x: 0.0, y: -100.0), .half), // redirect
(#line, halfPos, CGPoint(x: 0.0, y: -1000.0), .full), // project to full
(#line, halfPos, CGPoint(x: 0.0, y: -100.0), .half),
(#line, halfPos, CGPoint(x: 0.0, y: 0.0), .half),
(#line, halfPos, CGPoint(x: 0.0, y: 100.0), .half),
(#line, halfPos, CGPoint(x: 0.0, y: 1000.0), .half), // redirect
(#line, halfPos + 10.0, CGPoint(x: 0.0, y: -1000.0), .full), // project to full
])
assertTargetPosition(
fpc.floatingPanel,
with: [
(#line, fullPos - 10.0, CGPoint(x: 0.0, y: 1000.0), .half), // project to half
(#line, fullPos, CGPoint(x: 0.0, y: -1000.0), .full), // redirect
(#line, fullPos, CGPoint(x: 0.0, y: -100.0), .full),
(#line, fullPos, CGPoint(x: 0.0, y: 0.0), .full),
(#line, fullPos, CGPoint(x: 0.0, y: 100.0), .full),
(#line, fullPos, CGPoint(x: 0.0, y: 1000.0), .half), // project to half
(#line, fullPos + 10.0, CGPoint(x: 0.0, y: 100.0), .full), // redirect
(#line, halfPos - 10.0, CGPoint(x: 0.0, y: -100.0), .half), // redirect
(#line, halfPos, CGPoint(x: 0.0, y: -1000.0), .full), // project to full
(#line, halfPos, CGPoint(x: 0.0, y: -100.0), .half),
(#line, halfPos, CGPoint(x: 0.0, y: 0.0), .half),
(#line, halfPos, CGPoint(x: 0.0, y: 100.0), .half),
(#line, halfPos, CGPoint(x: 0.0, y: 1000.0), .half), // redirect
(#line, halfPos + 10.0, CGPoint(x: 0.0, y: -1000.0), .full), // project to full
]
)
}
func test_targetState_2positionsWithHidden() {
func test_targetPosition_2positionsWithHidden() {
class FloatingPanelLayout2Positions: FloatingPanelLayout {
let initialState: FloatingPanelState = .hidden
let position: FloatingPanelPosition = .bottom
let anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] = [
let anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] = [
.full: FloatingPanelLayoutAnchor(absoluteInset: 20.0, edge: .top, referenceGuide: .superview),
.hidden: FloatingPanelLayoutAnchor(absoluteInset: 0, edge: .bottom, referenceGuide: .superview),
]
@@ -395,42 +398,46 @@ class CoreTests: XCTestCase {
let hiddenPos = fpc.surfaceLocation(for: .hidden).y
fpc.move(to: .full, animated: false)
assertTargetState(fpc.floatingPanel, with: [
(#line, fullPos - 10.0, CGPoint(x: 0.0, y: 1000.0), .hidden), // project to hidden
(#line, fullPos, CGPoint(x: 0.0, y: -1000.0), .full), // redirect
(#line, fullPos, CGPoint(x: 0.0, y: -100.0), .full),
(#line, fullPos, CGPoint(x: 0.0, y: 0.0), .full),
(#line, fullPos, CGPoint(x: 0.0, y: 100.0), .full),
(#line, fullPos, CGPoint(x: 0.0, y: 1000.0), .hidden), // project to hidden
(#line, fullPos + 10.0, CGPoint(x: 0.0, y: 100.0), .full), // redirect
(#line, hiddenPos - 10.0, CGPoint(x: 0.0, y: -100.0), .hidden), // redirect
(#line, hiddenPos, CGPoint(x: 0.0, y: -1000.0), .full), // project to full
(#line, hiddenPos, CGPoint(x: 0.0, y: -100.0), .hidden),
(#line, hiddenPos, CGPoint(x: 0.0, y: 0.0), .hidden),
(#line, hiddenPos, CGPoint(x: 0.0, y: 100.0), .hidden),
(#line, hiddenPos, CGPoint(x: 0.0, y: 1000.0), .hidden), // redirect
(#line, hiddenPos + 10.0, CGPoint(x: 0.0, y: -1000.0), .full), // project to full
assertTargetPosition(
fpc.floatingPanel,
with: [
(#line, fullPos - 10.0, CGPoint(x: 0.0, y: 1000.0), .hidden), // project to hidden
(#line, fullPos, CGPoint(x: 0.0, y: -1000.0), .full), // redirect
(#line, fullPos, CGPoint(x: 0.0, y: -100.0), .full),
(#line, fullPos, CGPoint(x: 0.0, y: 0.0), .full),
(#line, fullPos, CGPoint(x: 0.0, y: 100.0), .full),
(#line, fullPos, CGPoint(x: 0.0, y: 1000.0), .hidden), // project to hidden
(#line, fullPos + 10.0, CGPoint(x: 0.0, y: 100.0), .full), // redirect
(#line, hiddenPos - 10.0, CGPoint(x: 0.0, y: -100.0), .hidden), // redirect
(#line, hiddenPos, CGPoint(x: 0.0, y: -1000.0), .full), // project to full
(#line, hiddenPos, CGPoint(x: 0.0, y: -100.0), .hidden),
(#line, hiddenPos, CGPoint(x: 0.0, y: 0.0), .hidden),
(#line, hiddenPos, CGPoint(x: 0.0, y: 100.0), .hidden),
(#line, hiddenPos, CGPoint(x: 0.0, y: 1000.0), .hidden), // redirect
(#line, hiddenPos + 10.0, CGPoint(x: 0.0, y: -1000.0), .full), // project to full
])
fpc.move(to: .hidden, animated: false)
assertTargetState(fpc.floatingPanel, with: [
(#line, fullPos - 10.0, CGPoint(x: 0.0, y: 1000.0), .hidden), // project to hidden
(#line, fullPos, CGPoint(x: 0.0, y: -1000.0), .full), // redirect
(#line, fullPos, CGPoint(x: 0.0, y: -100.0), .full),
(#line, fullPos, CGPoint(x: 0.0, y: 0.0), .full),
(#line, fullPos, CGPoint(x: 0.0, y: 100.0), .full),
(#line, fullPos, CGPoint(x: 0.0, y: 1000.0), .hidden), // project to hidden
(#line, fullPos + 10.0, CGPoint(x: 0.0, y: 100.0), .full), // redirect
(#line, hiddenPos - 10.0, CGPoint(x: 0.0, y: -100.0), .hidden), // redirect
(#line, hiddenPos, CGPoint(x: 0.0, y: -1000.0), .full), // project to full
(#line, hiddenPos, CGPoint(x: 0.0, y: -100.0), .hidden),
(#line, hiddenPos, CGPoint(x: 0.0, y: 0.0), .hidden),
(#line, hiddenPos, CGPoint(x: 0.0, y: 100.0), .hidden),
(#line, hiddenPos, CGPoint(x: 0.0, y: 1000.0), .hidden), // redirect
(#line, hiddenPos + 10.0, CGPoint(x: 0.0, y: -1000.0), .full), // project to full
assertTargetPosition(
fpc.floatingPanel,
with: [
(#line, fullPos - 10.0, CGPoint(x: 0.0, y: 1000.0), .hidden), // project to hidden
(#line, fullPos, CGPoint(x: 0.0, y: -1000.0), .full), // redirect
(#line, fullPos, CGPoint(x: 0.0, y: -100.0), .full),
(#line, fullPos, CGPoint(x: 0.0, y: 0.0), .full),
(#line, fullPos, CGPoint(x: 0.0, y: 100.0), .full),
(#line, fullPos, CGPoint(x: 0.0, y: 1000.0), .hidden), // project to hidden
(#line, fullPos + 10.0, CGPoint(x: 0.0, y: 100.0), .full), // redirect
(#line, hiddenPos - 10.0, CGPoint(x: 0.0, y: -100.0), .hidden), // redirect
(#line, hiddenPos, CGPoint(x: 0.0, y: -1000.0), .full), // project to full
(#line, hiddenPos, CGPoint(x: 0.0, y: -100.0), .hidden),
(#line, hiddenPos, CGPoint(x: 0.0, y: 0.0), .hidden),
(#line, hiddenPos, CGPoint(x: 0.0, y: 100.0), .hidden),
(#line, hiddenPos, CGPoint(x: 0.0, y: 1000.0), .hidden), // redirect
(#line, hiddenPos + 10.0, CGPoint(x: 0.0, y: -1000.0), .full), // project to full
])
}
func test_targetState_3positionsFromFull() {
func test_targetPosition_3positionsFromFull() {
let delegate = FloatingPanelTestDelegate()
let fpc = FloatingPanelController(delegate: delegate)
fpc.layout = FloatingPanelLayout3Positions()
@@ -442,39 +449,41 @@ class CoreTests: XCTestCase {
let tipPos = fpc.surfaceLocation(for: .tip).y
// From .full
fpc.move(to: .full, animated: false)
assertTargetState(fpc.floatingPanel, with: [
(#line, fullPos - 500.0, CGPoint(x: 0.0, y: -100.0), .full), // far from topMostState
(#line, fullPos - 500.0, CGPoint(x: 0.0, y: 0.0), .full), // far from topMostState
(#line, fullPos - 500.0, CGPoint(x: 0.0, y: 100.0), .full), // far from topMostState
(#line, fullPos - 10.0, CGPoint(x: 0.0, y: 3000.0), .half), // block projecting to tip at half
(#line, fullPos, CGPoint(x: 0.0, y: -100.0), .full),
(#line, fullPos, CGPoint(x: 0.0, y: 0.0), .full),
(#line, fullPos, CGPoint(x: 0.0, y: 500.0), .half), // project to half
(#line, fullPos, CGPoint(x: 0.0, y: 1000.0), .half), // block projecting to tip at half
(#line, fullPos, CGPoint(x: 0.0, y: 3000.0), .half), // block projecting to tip at half
(#line, fullPos + 10.0, CGPoint(x: 0.0, y: 100.0), .full), // redirect
(#line, halfPos - 10.0, CGPoint(x: 0.0, y: -100.0), .half), // redirect
(#line, halfPos, CGPoint(x: 0.0, y: -1000.0), .full), //project to full
(#line, halfPos, CGPoint(x: 0.0, y: -100.0), .half),
(#line, halfPos, CGPoint(x: 0.0, y: 0.0), .half),
(#line, halfPos, CGPoint(x: 0.0, y: 100.0), .half),
(#line, halfPos, CGPoint(x: 0.0, y: 1000.0), .tip), // project to tip
(#line, halfPos + 10.0, CGPoint(x: 0.0, y: 100.0), .half), // redirect
(#line, tipPos - 10.0, CGPoint(x: 0.0, y: -100.0), .tip), // redirect
(#line, tipPos, CGPoint(x: 0.0, y: -3000.0), .half), // block projecting to full at half
(#line, tipPos, CGPoint(x: 0.0, y: -1000.0), .half), // block projecting to full at half
(#line, tipPos, CGPoint(x: 0.0, y: -500.0), .half), // project to half
(#line, tipPos, CGPoint(x: 0.0, y: -100.0), .tip),
(#line, tipPos, CGPoint(x: 0.0, y: 0.0), .tip),
(#line, tipPos, CGPoint(x: 0.0, y: 100.0), .tip),
(#line, tipPos + 10.0, CGPoint(x: 0.0, y: -3000.0), .half), // block projecting to full at half
(#line, tipPos + 500.0, CGPoint(x: 0.0, y: -100.0), .tip), // far from bottomMostState
(#line, tipPos + 500.0, CGPoint(x: 0.0, y: 0.0), .tip), // far from bottomMostState
(#line, tipPos + 500.0, CGPoint(x: 0.0, y: 100.0), .tip), // far from bottomMostState
])
assertTargetPosition(
fpc.floatingPanel,
with: [
(#line, fullPos - 500.0, CGPoint(x: 0.0, y: -100.0), .full), // far from topMostState
(#line, fullPos - 500.0, CGPoint(x: 0.0, y: 0.0), .full), // far from topMostState
(#line, fullPos - 500.0, CGPoint(x: 0.0, y: 100.0), .full), // far from topMostState
(#line, fullPos - 10.0, CGPoint(x: 0.0, y: 3000.0), .half), // block projecting to tip at half
(#line, fullPos, CGPoint(x: 0.0, y: -100.0), .full),
(#line, fullPos, CGPoint(x: 0.0, y: 0.0), .full),
(#line, fullPos, CGPoint(x: 0.0, y: 500.0), .half), // project to half
(#line, fullPos, CGPoint(x: 0.0, y: 1000.0), .half), // block projecting to tip at half
(#line, fullPos, CGPoint(x: 0.0, y: 3000.0), .half), // block projecting to tip at half
(#line, fullPos + 10.0, CGPoint(x: 0.0, y: 100.0), .full), // redirect
(#line, halfPos - 10.0, CGPoint(x: 0.0, y: -100.0), .half), // redirect
(#line, halfPos, CGPoint(x: 0.0, y: -1000.0), .full), //project to full
(#line, halfPos, CGPoint(x: 0.0, y: -100.0), .half),
(#line, halfPos, CGPoint(x: 0.0, y: 0.0), .half),
(#line, halfPos, CGPoint(x: 0.0, y: 100.0), .half),
(#line, halfPos, CGPoint(x: 0.0, y: 1000.0), .tip), // project to tip
(#line, halfPos + 10.0, CGPoint(x: 0.0, y: 100.0), .half), // redirect
(#line, tipPos - 10.0, CGPoint(x: 0.0, y: -100.0), .tip), // redirect
(#line, tipPos, CGPoint(x: 0.0, y: -3000.0), .half), // block projecting to full at half
(#line, tipPos, CGPoint(x: 0.0, y: -1000.0), .half), // block projecting to full at half
(#line, tipPos, CGPoint(x: 0.0, y: -500.0), .half), // project to half
(#line, tipPos, CGPoint(x: 0.0, y: -100.0), .tip),
(#line, tipPos, CGPoint(x: 0.0, y: 0.0), .tip),
(#line, tipPos, CGPoint(x: 0.0, y: 100.0), .tip),
(#line, tipPos + 10.0, CGPoint(x: 0.0, y: -3000.0), .half), // block projecting to full at half
(#line, tipPos + 500.0, CGPoint(x: 0.0, y: -100.0), .tip), // far from bottomMostState
(#line, tipPos + 500.0, CGPoint(x: 0.0, y: 0.0), .tip), // far from bottomMostState
(#line, tipPos + 500.0, CGPoint(x: 0.0, y: 100.0), .tip), // far from bottomMostState
])
}
func test_targetState_3positionsFromFull_bottomEdge() {
func test_targetPosition_3positionsFromFull_bottomEdge() {
let delegate = FloatingPanelTestDelegate()
let fpc = FloatingPanelController(delegate: delegate)
fpc.layout = FloatingPanelLayout3PositionsBottomEdge()
@@ -486,39 +495,41 @@ class CoreTests: XCTestCase {
let tipPos = fpc.surfaceLocation(for: .tip).y
// From .full
fpc.move(to: .full, animated: false)
assertTargetState(fpc.floatingPanel, with: [
(#line, tipPos - 500.0, CGPoint(x: 0.0, y: -100.0), .tip), // far from topMostState
(#line, tipPos - 500.0, CGPoint(x: 0.0, y: 0.0), .tip), // far from topMostState
(#line, tipPos - 500.0, CGPoint(x: 0.0, y: 100.0), .tip), // far from topMostState
(#line, tipPos - 10.0, CGPoint(x: 0.0, y: 3000.0), .half), // block projecting to full at half
(#line, tipPos, CGPoint(x: 0.0, y: -100.0), .tip),
(#line, tipPos, CGPoint(x: 0.0, y: 0.0), .tip),
(#line, tipPos, CGPoint(x: 0.0, y: 500.0), .half), // project to half
(#line, tipPos, CGPoint(x: 0.0, y: 1000.0), .half), // block projecting to full at half
(#line, tipPos, CGPoint(x: 0.0, y: 3000.0), .half), // block projecting to full at half
(#line, tipPos + 10.0, CGPoint(x: 0.0, y: 100.0), .tip), // redirect
(#line, halfPos - 10.0, CGPoint(x: 0.0, y: -100.0), .half), // redirect
(#line, halfPos, CGPoint(x: 0.0, y: -1000.0), .tip), //project to tip
(#line, halfPos, CGPoint(x: 0.0, y: -100.0), .half),
(#line, halfPos, CGPoint(x: 0.0, y: 0.0), .half),
(#line, halfPos, CGPoint(x: 0.0, y: 100.0), .half),
(#line, halfPos, CGPoint(x: 0.0, y: 1000.0), .full), // project to full
(#line, halfPos + 10.0, CGPoint(x: 0.0, y: 100.0), .half), // redirect
(#line, fullPos - 10.0, CGPoint(x: 0.0, y: -100.0), .full), // redirect
(#line, fullPos, CGPoint(x: 0.0, y: -3000.0), .half), // block projecting to tip at half
(#line, fullPos, CGPoint(x: 0.0, y: -1000.0), .half), // block projecting to tip at half
(#line, fullPos, CGPoint(x: 0.0, y: -500.0), .half), // project to half
(#line, fullPos, CGPoint(x: 0.0, y: -100.0), .full),
(#line, fullPos, CGPoint(x: 0.0, y: 0.0), .full),
(#line, fullPos, CGPoint(x: 0.0, y: 100.0), .full),
(#line, fullPos + 10.0, CGPoint(x: 0.0, y: -3000.0), .half), // block projecting to tip at half
(#line, fullPos + 500.0, CGPoint(x: 0.0, y: -100.0), .full), // far from bottomMostState
(#line, fullPos + 500.0, CGPoint(x: 0.0, y: 0.0), .full), // far from bottomMostState
(#line, fullPos + 500.0, CGPoint(x: 0.0, y: 100.0), .full), // far from bottomMostState
])
assertTargetPosition(
fpc.floatingPanel,
with: [
(#line, tipPos - 500.0, CGPoint(x: 0.0, y: -100.0), .tip), // far from topMostState
(#line, tipPos - 500.0, CGPoint(x: 0.0, y: 0.0), .tip), // far from topMostState
(#line, tipPos - 500.0, CGPoint(x: 0.0, y: 100.0), .tip), // far from topMostState
(#line, tipPos - 10.0, CGPoint(x: 0.0, y: 3000.0), .half), // block projecting to full at half
(#line, tipPos, CGPoint(x: 0.0, y: -100.0), .tip),
(#line, tipPos, CGPoint(x: 0.0, y: 0.0), .tip),
(#line, tipPos, CGPoint(x: 0.0, y: 500.0), .half), // project to half
(#line, tipPos, CGPoint(x: 0.0, y: 1000.0), .half), // block projecting to full at half
(#line, tipPos, CGPoint(x: 0.0, y: 3000.0), .half), // block projecting to full at half
(#line, tipPos + 10.0, CGPoint(x: 0.0, y: 100.0), .tip), // redirect
(#line, halfPos - 10.0, CGPoint(x: 0.0, y: -100.0), .half), // redirect
(#line, halfPos, CGPoint(x: 0.0, y: -1000.0), .tip), //project to tip
(#line, halfPos, CGPoint(x: 0.0, y: -100.0), .half),
(#line, halfPos, CGPoint(x: 0.0, y: 0.0), .half),
(#line, halfPos, CGPoint(x: 0.0, y: 100.0), .half),
(#line, halfPos, CGPoint(x: 0.0, y: 1000.0), .full), // project to full
(#line, halfPos + 10.0, CGPoint(x: 0.0, y: 100.0), .half), // redirect
(#line, fullPos - 10.0, CGPoint(x: 0.0, y: -100.0), .full), // redirect
(#line, fullPos, CGPoint(x: 0.0, y: -3000.0), .half), // block projecting to tip at half
(#line, fullPos, CGPoint(x: 0.0, y: -1000.0), .half), // block projecting to tip at half
(#line, fullPos, CGPoint(x: 0.0, y: -500.0), .half), // project to half
(#line, fullPos, CGPoint(x: 0.0, y: -100.0), .full),
(#line, fullPos, CGPoint(x: 0.0, y: 0.0), .full),
(#line, fullPos, CGPoint(x: 0.0, y: 100.0), .full),
(#line, fullPos + 10.0, CGPoint(x: 0.0, y: -3000.0), .half), // block projecting to tip at half
(#line, fullPos + 500.0, CGPoint(x: 0.0, y: -100.0), .full), // far from bottomMostState
(#line, fullPos + 500.0, CGPoint(x: 0.0, y: 0.0), .full), // far from bottomMostState
(#line, fullPos + 500.0, CGPoint(x: 0.0, y: 100.0), .full), // far from bottomMostState
])
}
func test_targetState_3positionsFromHalf() {
func test_targetPosition_3positionsFromHalf() {
let delegate = FloatingPanelTestDelegate()
let fpc = FloatingPanelController(delegate: delegate)
fpc.layout = FloatingPanelLayout3Positions()
@@ -530,79 +541,83 @@ class CoreTests: XCTestCase {
let tipPos = fpc.surfaceLocation(for: .tip).y
// From .half
fpc.move(to: .half, animated: false)
assertTargetState(fpc.floatingPanel, with: [
(#line, fullPos - 500.0, CGPoint(x: 0.0, y: -100.0), .full), // far from topMostState
(#line, fullPos - 500.0, CGPoint(x: 0.0, y: 0.0), .full), // far from topMostState
(#line, fullPos - 500.0, CGPoint(x: 0.0, y: 100.0), .full), // far from topMostState
(#line, fullPos, CGPoint(x: 0.0, y: -100.0), .full),
(#line, fullPos, CGPoint(x: 0.0, y: 0.0), .full),
(#line, fullPos, CGPoint(x: 0.0, y: 500.0), .half), // project to half
(#line, fullPos, CGPoint(x: 0.0, y: 1000.0), .half), // block projecting to tip at half
(#line, fullPos, CGPoint(x: 0.0, y: 3000.0), .half), // block projecting to tip at half
(#line, fullPos + 10.0, CGPoint(x: 0.0, y: 100.0), .full), // redirect
(#line, halfPos - 10.0, CGPoint(x: 0.0, y: -100.0), .half), // redirect
(#line, halfPos, CGPoint(x: 0.0, y: -1000.0), .full),// project to full
(#line, halfPos, CGPoint(x: 0.0, y: -100.0), .half),
(#line, halfPos, CGPoint(x: 0.0, y: 0.0), .half),
(#line, halfPos, CGPoint(x: 0.0, y: 100.0), .half),
(#line, halfPos, CGPoint(x: 0.0, y: 1000.0), .tip), // project to tip
(#line, halfPos + 10.0, CGPoint(x: 0.0, y: 100.0), .half), // redirect
(#line, tipPos - 10.0, CGPoint(x: 0.0, y: -100.0), .tip), // redirect
(#line, tipPos, CGPoint(x: 0.0, y: -3000.0), .half), // block projecting to full at half
(#line, tipPos, CGPoint(x: 0.0, y: -1000.0), .half), // block projecting to full at half
(#line, tipPos, CGPoint(x: 0.0, y: -500.0), .half), // project to half
(#line, tipPos, CGPoint(x: 0.0, y: -100.0), .tip),
(#line, tipPos, CGPoint(x: 0.0, y: 0.0), .tip),
(#line, tipPos, CGPoint(x: 0.0, y: 100.0), .tip),
(#line, tipPos + 500.0, CGPoint(x: 0.0, y: -100.0), .tip), // far from bottomMostState
(#line, tipPos + 500.0, CGPoint(x: 0.0, y: 0.0), .tip), // far from bottomMostState
(#line, tipPos + 500.0, CGPoint(x: 0.0, y: 100.0), .tip), // far from bottomMostState
])
assertTargetPosition(
fpc.floatingPanel,
with: [
(#line, fullPos - 500.0, CGPoint(x: 0.0, y: -100.0), .full), // far from topMostState
(#line, fullPos - 500.0, CGPoint(x: 0.0, y: 0.0), .full), // far from topMostState
(#line, fullPos - 500.0, CGPoint(x: 0.0, y: 100.0), .full), // far from topMostState
(#line, fullPos, CGPoint(x: 0.0, y: -100.0), .full),
(#line, fullPos, CGPoint(x: 0.0, y: 0.0), .full),
(#line, fullPos, CGPoint(x: 0.0, y: 500.0), .half), // project to half
(#line, fullPos, CGPoint(x: 0.0, y: 1000.0), .half), // block projecting to tip at half
(#line, fullPos, CGPoint(x: 0.0, y: 3000.0), .half), // block projecting to tip at half
(#line, fullPos + 10.0, CGPoint(x: 0.0, y: 100.0), .full), // redirect
(#line, halfPos - 10.0, CGPoint(x: 0.0, y: -100.0), .half), // redirect
(#line, halfPos, CGPoint(x: 0.0, y: -1000.0), .full), // project to full
(#line, halfPos, CGPoint(x: 0.0, y: -100.0), .half),
(#line, halfPos, CGPoint(x: 0.0, y: 0.0), .half),
(#line, halfPos, CGPoint(x: 0.0, y: 100.0), .half),
(#line, halfPos, CGPoint(x: 0.0, y: 1000.0), .tip), // project to tip
(#line, halfPos + 10.0, CGPoint(x: 0.0, y: 100.0), .half), // redirect
(#line, tipPos - 10.0, CGPoint(x: 0.0, y: -100.0), .tip), // redirect
(#line, tipPos, CGPoint(x: 0.0, y: -3000.0), .half), // block projecting to full at half
(#line, tipPos, CGPoint(x: 0.0, y: -1000.0), .half), // block projecting to full at half
(#line, tipPos, CGPoint(x: 0.0, y: -500.0), .half), // project to half
(#line, tipPos, CGPoint(x: 0.0, y: -100.0), .tip),
(#line, tipPos, CGPoint(x: 0.0, y: 0.0), .tip),
(#line, tipPos, CGPoint(x: 0.0, y: 100.0), .tip),
(#line, tipPos + 500.0, CGPoint(x: 0.0, y: -100.0), .tip), // far from bottomMostState
(#line, tipPos + 500.0, CGPoint(x: 0.0, y: 0.0), .tip), // far from bottomMostState
(#line, tipPos + 500.0, CGPoint(x: 0.0, y: 100.0), .tip), // far from bottomMostState
])
}
func test_targetState_3positionsFromHalf_bottomEdge() {
func test_targetPosition_3positionsFromHalf_bottomEdge() {
let delegate = FloatingPanelTestDelegate()
let fpc = FloatingPanelController(delegate: delegate)
fpc.layout = FloatingPanelLayout3PositionsBottomEdge()
fpc.showForTest()
fpc.showForTest()
let fullPos = fpc.surfaceLocation(for: .full).y
let halfPos = fpc.surfaceLocation(for: .half).y
let tipPos = fpc.surfaceLocation(for: .tip).y
// From .half
fpc.move(to: .half, animated: false)
assertTargetState(fpc.floatingPanel, with: [
(#line, tipPos - 500.0, CGPoint(x: 0.0, y: -100.0), .tip), // far from topMostState
(#line, tipPos - 500.0, CGPoint(x: 0.0, y: 0.0), .tip), // far from topMostState
(#line, tipPos - 500.0, CGPoint(x: 0.0, y: 100.0), .tip), // far from topMostState
let fullPos = fpc.surfaceLocation(for: .full).y
let halfPos = fpc.surfaceLocation(for: .half).y
let tipPos = fpc.surfaceLocation(for: .tip).y
// From .half
fpc.move(to: .half, animated: false)
assertTargetPosition(
fpc.floatingPanel,
with: [
(#line, tipPos - 500.0, CGPoint(x: 0.0, y: -100.0), .tip), // far from topMostState
(#line, tipPos - 500.0, CGPoint(x: 0.0, y: 0.0), .tip), // far from topMostState
(#line, tipPos - 500.0, CGPoint(x: 0.0, y: 100.0), .tip), // far from topMostState
(#line, tipPos, CGPoint(x: 0.0, y: -100.0), .tip),
(#line, tipPos, CGPoint(x: 0.0, y: 0.0), .tip),
(#line, tipPos, CGPoint(x: 0.0, y: 500.0), .half), // project to half
(#line, tipPos, CGPoint(x: 0.0, y: 1000.0), .half), // block projecting to full at half
(#line, tipPos, CGPoint(x: 0.0, y: 3000.0), .half), // block projecting to full at half
(#line, tipPos + 10.0, CGPoint(x: 0.0, y: 100.0), .tip), // redirect
(#line, halfPos - 10.0, CGPoint(x: 0.0, y: -100.0), .half), // redirect
(#line, halfPos, CGPoint(x: 0.0, y: -1000.0), .tip),// project to tip
(#line, tipPos, CGPoint(x: 0.0, y: 500.0), .half), // project to half
(#line, tipPos, CGPoint(x: 0.0, y: 1000.0), .half), // block projecting to full at half
(#line, tipPos, CGPoint(x: 0.0, y: 3000.0), .half), // block projecting to full at half
(#line, tipPos + 10.0, CGPoint(x: 0.0, y: 100.0), .tip), // redirect
(#line, halfPos - 10.0, CGPoint(x: 0.0, y: -100.0), .half), // redirect
(#line, halfPos, CGPoint(x: 0.0, y: -1000.0), .tip), // project to tip
(#line, halfPos, CGPoint(x: 0.0, y: -100.0), .half),
(#line, halfPos, CGPoint(x: 0.0, y: 0.0), .half),
(#line, halfPos, CGPoint(x: 0.0, y: 100.0), .half),
(#line, halfPos, CGPoint(x: 0.0, y: 1000.0), .full), // project to full
(#line, halfPos + 10.0, CGPoint(x: 0.0, y: 100.0), .half), // redirect
(#line, fullPos - 10.0, CGPoint(x: 0.0, y: -100.0), .full), // redirect
(#line, fullPos, CGPoint(x: 0.0, y: -3000.0), .half), // block projecting to tip at half
(#line, fullPos, CGPoint(x: 0.0, y: -1000.0), .half), // block projecting to tip at half
(#line, fullPos, CGPoint(x: 0.0, y: -500.0), .half), // project to half
(#line, halfPos, CGPoint(x: 0.0, y: 1000.0), .full), // project to full
(#line, halfPos + 10.0, CGPoint(x: 0.0, y: 100.0), .half), // redirect
(#line, fullPos - 10.0, CGPoint(x: 0.0, y: -100.0), .full), // redirect
(#line, fullPos, CGPoint(x: 0.0, y: -3000.0), .half), // block projecting to tip at half
(#line, fullPos, CGPoint(x: 0.0, y: -1000.0), .half), // block projecting to tip at half
(#line, fullPos, CGPoint(x: 0.0, y: -500.0), .half), // project to half
(#line, fullPos, CGPoint(x: 0.0, y: -100.0), .full),
(#line, fullPos, CGPoint(x: 0.0, y: 0.0), .full),
(#line, fullPos, CGPoint(x: 0.0, y: 100.0), .full),
(#line, fullPos + 500.0, CGPoint(x: 0.0, y: -100.0), .full), // far from bottomMostState
(#line, fullPos + 500.0, CGPoint(x: 0.0, y: 0.0), .full), // far from bottomMostState
(#line, fullPos + 500.0, CGPoint(x: 0.0, y: 100.0), .full), // far from bottomMostState
(#line, fullPos + 500.0, CGPoint(x: 0.0, y: -100.0), .full), // far from bottomMostState
(#line, fullPos + 500.0, CGPoint(x: 0.0, y: 0.0), .full), // far from bottomMostState
(#line, fullPos + 500.0, CGPoint(x: 0.0, y: 100.0), .full), // far from bottomMostState
])
}
func test_targetState_3positionsFromTip() {
func test_targetPosition_3positionsFromTip() {
let delegate = FloatingPanelTestDelegate()
let fpc = FloatingPanelController(delegate: delegate)
fpc.layout = FloatingPanelLayout3Positions()
@@ -615,80 +630,84 @@ class CoreTests: XCTestCase {
// From .tip
fpc.move(to: .tip, animated: false)
assertTargetState(fpc.floatingPanel, with: [
(#line, fullPos - 500.0, CGPoint(x: 0.0, y: -100.0), .full), // far from topMostState
(#line, fullPos - 500.0, CGPoint(x: 0.0, y: 0.0), .full), // far from topMostState
(#line, fullPos - 500.0, CGPoint(x: 0.0, y: 100.0), .full), // far from topMostState
(#line, fullPos, CGPoint(x: 0.0, y: -100.0), .full),
(#line, fullPos, CGPoint(x: 0.0, y: 0.0), .full),
(#line, fullPos, CGPoint(x: 0.0, y: 500.0), .half), // project to half
(#line, fullPos, CGPoint(x: 0.0, y: 1000.0), .half), // block projecting to tip at half
(#line, fullPos, CGPoint(x: 0.0, y: 3000.0), .half), // block projecting to tip at half
(#line, fullPos + 10.0, CGPoint(x: 0.0, y: 100.0), .full), // redirect
(#line, halfPos - 10.0, CGPoint(x: 0.0, y: -100.0), .half), // redirect
(#line, halfPos, CGPoint(x: 0.0, y: -3000.0), .full), // project to full
(#line, halfPos, CGPoint(x: 0.0, y: -100.0), .half),
(#line, halfPos, CGPoint(x: 0.0, y: 0.0), .half),
(#line, halfPos, CGPoint(x: 0.0, y: 100.0), .half),
(#line, halfPos, CGPoint(x: 0.0, y: 1000.0), .tip), // project to tip
(#line, halfPos + 10.0, CGPoint(x: 0.0, y: 100.0), .half), // redirect
(#line, tipPos - 10.0, CGPoint(x: 0.0, y: -100.0), .tip), // redirect
(#line, tipPos, CGPoint(x: 0.0, y: -3000.0), .half), // block projecting to full at half
(#line, tipPos, CGPoint(x: 0.0, y: -1000.0), .half), // block projecting to full at half
(#line, tipPos, CGPoint(x: 0.0, y: -500.0), .half), // project to half
(#line, tipPos, CGPoint(x: 0.0, y: -100.0), .tip),
(#line, tipPos, CGPoint(x: 0.0, y: 0.0), .tip),
(#line, tipPos, CGPoint(x: 0.0, y: 100.0), .tip),
(#line, tipPos + 500.0, CGPoint(x: 0.0, y: -100.0), .tip), // far from bottomMostState
(#line, tipPos + 500.0, CGPoint(x: 0.0, y: 0.0), .tip), // far from bottomMostState
(#line, tipPos + 500.0, CGPoint(x: 0.0, y: 100.0), .tip), // far from bottomMostState
])
assertTargetPosition(
fpc.floatingPanel,
with: [
(#line, fullPos - 500.0, CGPoint(x: 0.0, y: -100.0), .full), // far from topMostState
(#line, fullPos - 500.0, CGPoint(x: 0.0, y: 0.0), .full), // far from topMostState
(#line, fullPos - 500.0, CGPoint(x: 0.0, y: 100.0), .full), // far from topMostState
(#line, fullPos, CGPoint(x: 0.0, y: -100.0), .full),
(#line, fullPos, CGPoint(x: 0.0, y: 0.0), .full),
(#line, fullPos, CGPoint(x: 0.0, y: 500.0), .half), // project to half
(#line, fullPos, CGPoint(x: 0.0, y: 1000.0), .half), // block projecting to tip at half
(#line, fullPos, CGPoint(x: 0.0, y: 3000.0), .half), // block projecting to tip at half
(#line, fullPos + 10.0, CGPoint(x: 0.0, y: 100.0), .full), // redirect
(#line, halfPos - 10.0, CGPoint(x: 0.0, y: -100.0), .half), // redirect
(#line, halfPos, CGPoint(x: 0.0, y: -3000.0), .full), // project to full
(#line, halfPos, CGPoint(x: 0.0, y: -100.0), .half),
(#line, halfPos, CGPoint(x: 0.0, y: 0.0), .half),
(#line, halfPos, CGPoint(x: 0.0, y: 100.0), .half),
(#line, halfPos, CGPoint(x: 0.0, y: 1000.0), .tip), // project to tip
(#line, halfPos + 10.0, CGPoint(x: 0.0, y: 100.0), .half), // redirect
(#line, tipPos - 10.0, CGPoint(x: 0.0, y: -100.0), .tip), // redirect
(#line, tipPos, CGPoint(x: 0.0, y: -3000.0), .half), // block projecting to full at half
(#line, tipPos, CGPoint(x: 0.0, y: -1000.0), .half), // block projecting to full at half
(#line, tipPos, CGPoint(x: 0.0, y: -500.0), .half), // project to half
(#line, tipPos, CGPoint(x: 0.0, y: -100.0), .tip),
(#line, tipPos, CGPoint(x: 0.0, y: 0.0), .tip),
(#line, tipPos, CGPoint(x: 0.0, y: 100.0), .tip),
(#line, tipPos + 500.0, CGPoint(x: 0.0, y: -100.0), .tip), // far from bottomMostState
(#line, tipPos + 500.0, CGPoint(x: 0.0, y: 0.0), .tip), // far from bottomMostState
(#line, tipPos + 500.0, CGPoint(x: 0.0, y: 100.0), .tip), // far from bottomMostState
])
}
func test_targetState_3positionsFromTip_bottomEdge() {
func test_targetPosition_3positionsFromTip_bottomEdge() {
let delegate = FloatingPanelTestDelegate()
let fpc = FloatingPanelController(delegate: delegate)
fpc.layout = FloatingPanelLayout3PositionsBottomEdge()
fpc.showForTest()
let fullPos = fpc.surfaceLocation(for: .full).y
let halfPos = fpc.surfaceLocation(for: .half).y
let tipPos = fpc.surfaceLocation(for: .tip).y
let fullPos = fpc.surfaceLocation(for: .full).y
let halfPos = fpc.surfaceLocation(for: .half).y
let tipPos = fpc.surfaceLocation(for: .tip).y
// From .tip
fpc.move(to: .tip, animated: false)
assertTargetState(fpc.floatingPanel, with: [
(#line, tipPos - 500.0, CGPoint(x: 0.0, y: -100.0), .tip), // far from topMostState
(#line, tipPos - 500.0, CGPoint(x: 0.0, y: 0.0), .tip), // far from topMostState
(#line, tipPos - 500.0, CGPoint(x: 0.0, y: 100.0), .tip), // far from topMostState
// From .tip
fpc.move(to: .tip, animated: false)
assertTargetPosition(
fpc.floatingPanel,
with: [
(#line, tipPos - 500.0, CGPoint(x: 0.0, y: -100.0), .tip), // far from topMostState
(#line, tipPos - 500.0, CGPoint(x: 0.0, y: 0.0), .tip), // far from topMostState
(#line, tipPos - 500.0, CGPoint(x: 0.0, y: 100.0), .tip), // far from topMostState
(#line, tipPos, CGPoint(x: 0.0, y: -100.0), .tip),
(#line, tipPos, CGPoint(x: 0.0, y: 0.0), .tip),
(#line, tipPos, CGPoint(x: 0.0, y: 500.0), .half), // project to half
(#line, tipPos, CGPoint(x: 0.0, y: 1000.0), .half), // block projecting to tip at half
(#line, tipPos, CGPoint(x: 0.0, y: 3000.0), .half), // block projecting to tip at half
(#line, tipPos + 10.0, CGPoint(x: 0.0, y: 100.0), .tip), // redirect
(#line, halfPos - 10.0, CGPoint(x: 0.0, y: -100.0), .half), // redirect
(#line, halfPos, CGPoint(x: 0.0, y: -3000.0), .tip), // project to full
(#line, tipPos, CGPoint(x: 0.0, y: 500.0), .half), // project to half
(#line, tipPos, CGPoint(x: 0.0, y: 1000.0), .half), // block projecting to tip at half
(#line, tipPos, CGPoint(x: 0.0, y: 3000.0), .half), // block projecting to tip at half
(#line, tipPos + 10.0, CGPoint(x: 0.0, y: 100.0), .tip), // redirect
(#line, halfPos - 10.0, CGPoint(x: 0.0, y: -100.0), .half), // redirect
(#line, halfPos, CGPoint(x: 0.0, y: -3000.0), .tip), // project to full
(#line, halfPos, CGPoint(x: 0.0, y: -100.0), .half),
(#line, halfPos, CGPoint(x: 0.0, y: 0.0), .half),
(#line, halfPos, CGPoint(x: 0.0, y: 100.0), .half),
(#line, halfPos, CGPoint(x: 0.0, y: 1000.0), .full), // project to tip
(#line, halfPos + 10.0, CGPoint(x: 0.0, y: 100.0), .half), // redirect
(#line, fullPos - 10.0, CGPoint(x: 0.0, y: -100.0), .full), // redirect
(#line, fullPos, CGPoint(x: 0.0, y: -3000.0), .half), // block projecting to full at half
(#line, fullPos, CGPoint(x: 0.0, y: -1000.0), .half), // block projecting to full at half
(#line, fullPos, CGPoint(x: 0.0, y: -500.0), .half), // project to half
(#line, halfPos, CGPoint(x: 0.0, y: 1000.0), .full), // project to tip
(#line, halfPos + 10.0, CGPoint(x: 0.0, y: 100.0), .half), // redirect
(#line, fullPos - 10.0, CGPoint(x: 0.0, y: -100.0), .full), // redirect
(#line, fullPos, CGPoint(x: 0.0, y: -3000.0), .half), // block projecting to full at half
(#line, fullPos, CGPoint(x: 0.0, y: -1000.0), .half), // block projecting to full at half
(#line, fullPos, CGPoint(x: 0.0, y: -500.0), .half), // project to half
(#line, fullPos, CGPoint(x: 0.0, y: -100.0), .full),
(#line, fullPos, CGPoint(x: 0.0, y: 0.0), .full),
(#line, fullPos, CGPoint(x: 0.0, y: 100.0), .full),
(#line, fullPos + 500.0, CGPoint(x: 0.0, y: -100.0), .full), // far from bottomMostState
(#line, fullPos + 500.0, CGPoint(x: 0.0, y: 0.0), .full), // far from bottomMostState
(#line, fullPos + 500.0, CGPoint(x: 0.0, y: 100.0), .full), // far from bottomMostState
(#line, fullPos + 500.0, CGPoint(x: 0.0, y: -100.0), .full), // far from bottomMostState
(#line, fullPos + 500.0, CGPoint(x: 0.0, y: 0.0), .full), // far from bottomMostState
(#line, fullPos + 500.0, CGPoint(x: 0.0, y: 100.0), .full), // far from bottomMostState
])
}
func test_targetState_3positionsAllProjection() {
func test_targetPosition_3positionsAllProjection() {
let delegate = FloatingPanelTestDelegate()
let fpc = FloatingPanelController(delegate: delegate)
fpc.layout = FloatingPanelLayout3Positions()
@@ -702,45 +721,51 @@ class CoreTests: XCTestCase {
// From .full
fpc.move(to: .full, animated: false)
assertTargetState(fpc.floatingPanel, with: [
(#line, fullPos - 10.0, CGPoint(x: 0.0, y: 3000.0), .tip),
(#line, fullPos, CGPoint(x: 0.0, y: 1000.0), .tip),
(#line, fullPos, CGPoint(x: 0.0, y: 3000.0), .tip),
(#line, halfPos, CGPoint(x: 0.0, y: 1000.0), .tip),
(#line, halfPos, CGPoint(x: 0.0, y: -1000.0), .full),
(#line, tipPos, CGPoint(x: 0.0, y: -3000.0), .full),
(#line, tipPos, CGPoint(x: 0.0, y: -1000.0), .full),
(#line, tipPos + 10.0, CGPoint(x: 0.0, y: -3000.0), .full),
assertTargetPosition(
fpc.floatingPanel,
with: [
(#line, fullPos - 10.0, CGPoint(x: 0.0, y: 3000.0), .tip),
(#line, fullPos, CGPoint(x: 0.0, y: 1000.0), .tip),
(#line, fullPos, CGPoint(x: 0.0, y: 3000.0), .tip),
(#line, halfPos, CGPoint(x: 0.0, y: 1000.0), .tip),
(#line, halfPos, CGPoint(x: 0.0, y: -1000.0), .full),
(#line, tipPos, CGPoint(x: 0.0, y: -3000.0), .full),
(#line, tipPos, CGPoint(x: 0.0, y: -1000.0), .full),
(#line, tipPos + 10.0, CGPoint(x: 0.0, y: -3000.0), .full),
])
// From .half
fpc.move(to: .tip, animated: false)
assertTargetState(fpc.floatingPanel, with: [
(#line, fullPos, CGPoint(x: 0.0, y: 1000.0), .tip),
(#line, fullPos, CGPoint(x: 0.0, y: 3000.0), .tip),
(#line, tipPos, CGPoint(x: 0.0, y: -3000.0), .full),
(#line, tipPos, CGPoint(x: 0.0, y: -1000.0), .full),
assertTargetPosition(
fpc.floatingPanel,
with: [
(#line, fullPos, CGPoint(x: 0.0, y: 1000.0), .tip),
(#line, fullPos, CGPoint(x: 0.0, y: 3000.0), .tip),
(#line, tipPos, CGPoint(x: 0.0, y: -3000.0), .full),
(#line, tipPos, CGPoint(x: 0.0, y: -1000.0), .full),
])
// From .tip
fpc.move(to: .tip, animated: false)
assertTargetState(fpc.floatingPanel, with: [
(#line, fullPos - 10.0, CGPoint(x: 0.0, y: 3000.0), .tip),
(#line, fullPos, CGPoint(x: 0.0, y: 1000.0), .tip),
(#line, fullPos, CGPoint(x: 0.0, y: 3000.0), .tip),
(#line, halfPos, CGPoint(x: 0.0, y: 1000.0), .tip),
(#line, halfPos, CGPoint(x: 0.0, y: -1000.0), .full),
(#line, tipPos, CGPoint(x: 0.0, y: -3000.0), .full),
(#line, tipPos, CGPoint(x: 0.0, y: -1000.0), .full),
(#line, tipPos + 10.0, CGPoint(x: 0.0, y: -3000.0), .full),
assertTargetPosition(
fpc.floatingPanel,
with: [
(#line, fullPos - 10.0, CGPoint(x: 0.0, y: 3000.0), .tip),
(#line, fullPos, CGPoint(x: 0.0, y: 1000.0), .tip),
(#line, fullPos, CGPoint(x: 0.0, y: 3000.0), .tip),
(#line, halfPos, CGPoint(x: 0.0, y: 1000.0), .tip),
(#line, halfPos, CGPoint(x: 0.0, y: -1000.0), .full),
(#line, tipPos, CGPoint(x: 0.0, y: -3000.0), .full),
(#line, tipPos, CGPoint(x: 0.0, y: -1000.0), .full),
(#line, tipPos + 10.0, CGPoint(x: 0.0, y: -3000.0), .full),
])
}
func test_targetState_3positionsWithHidden() {
func test_targetPosition_3positionsWithHidden() {
class FloatingPanelLayout3PositionsWithHidden: FloatingPanelLayout {
let initialState: FloatingPanelState = .hidden
let position: FloatingPanelPosition = .bottom
let anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] = [
let anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] = [
.full: FloatingPanelLayoutAnchor(absoluteInset: 20.0, edge: .top, referenceGuide: .superview),
.half: FloatingPanelLayoutAnchor(absoluteInset: 250.0, edge: .bottom, referenceGuide: .superview),
.hidden: FloatingPanelLayoutAnchor(absoluteInset: 0, edge: .bottom, referenceGuide: .superview),
@@ -754,23 +779,27 @@ class CoreTests: XCTestCase {
XCTAssertEqual(fpc.state, .hidden)
fpc.move(to: .full, animated: false)
assertTargetState(fpc.floatingPanel, with: [
(#line, fpc.surfaceView.frame.minY, CGPoint(x: 0.0, y: 1000.0), .half),
assertTargetPosition(
fpc.floatingPanel,
with: [
(#line, fpc.surfaceView.frame.minY, CGPoint(x: 0.0, y: 1000.0), .half)
])
fpc.move(to: .half, animated: false)
assertTargetState(fpc.floatingPanel, with: [
(#line, fpc.surfaceView.frame.minY, CGPoint(x: 0.0, y: -100.0), .half),
(#line, fpc.surfaceView.frame.minY, CGPoint(x: 0.0, y: -1000.0), .full),
(#line, fpc.surfaceView.frame.minY, CGPoint(x: 0.0, y: 0.0), .half),
(#line, fpc.surfaceView.frame.minY, CGPoint(x: 0.0, y: 1000.0), .hidden),
assertTargetPosition(
fpc.floatingPanel,
with: [
(#line, fpc.surfaceView.frame.minY, CGPoint(x: 0.0, y: -100.0), .half),
(#line, fpc.surfaceView.frame.minY, CGPoint(x: 0.0, y: -1000.0), .full),
(#line, fpc.surfaceView.frame.minY, CGPoint(x: 0.0, y: 0.0), .half),
(#line, fpc.surfaceView.frame.minY, CGPoint(x: 0.0, y: 1000.0), .hidden),
])
}
func test_targetState_3positionsWithHiddenWithoutFull() {
func test_targetPosition_3positionsWithHiddenWithoutFull() {
class FloatingPanelLayout3Positions: FloatingPanelLayout {
let initialState: FloatingPanelState = .hidden
let position: FloatingPanelPosition = .bottom
let anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] = [
let anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] = [
.half: FloatingPanelLayoutAnchor(absoluteInset: 250.0, edge: .bottom, referenceGuide: .superview),
.tip: FloatingPanelLayoutAnchor(absoluteInset: 60.0, edge: .bottom, referenceGuide: .superview),
.hidden: FloatingPanelLayoutAnchor(absoluteInset: 0, edge: .bottom, referenceGuide: .superview),
@@ -790,27 +819,31 @@ class CoreTests: XCTestCase {
//let hiddenPos = fpc.surfaceLocation(for: .hidden)
fpc.move(to: .half, animated: false)
assertTargetState(fpc.floatingPanel, with: [
(#line, halfPos, CGPoint(x: 0.0, y: -100.0), .half),
(#line, halfPos, CGPoint(x: 0.0, y: 0.0), .half),
(#line, halfPos, CGPoint(x: 0.0, y: 385.0), .tip), // projection
(#line, halfPos, CGPoint(x: 0.0, y: 1000.0), .hidden), // projection
(#line, halfPos + 10.0, CGPoint(x: 0.0, y: 100.0), .half), // redirection
(#line, tipPos - 10.0, CGPoint(x: 0.0, y: -100.0), .tip), // redirection
(#line, tipPos, CGPoint(x: 0.0, y: -3000.0), .half), //projection
(#line, tipPos, CGPoint(x: 0.0, y: -10.0), .tip),
(#line, tipPos, CGPoint(x: 0.0, y: 0.0), .tip),
(#line, tipPos, CGPoint(x: 0.0, y: 10.0), .tip),
(#line, tipPos, CGPoint(x: 0.0, y: 1000.0), .hidden), //projection
(#line, tipPos + 10.0, CGPoint(x: 0.0, y: 10.0), .tip), // redirection
(#line, tipPos - 10.0, CGPoint(x: 0.0, y: 10.0), .tip), // redirection
assertTargetPosition(
fpc.floatingPanel,
with: [
(#line, halfPos, CGPoint(x: 0.0, y: -100.0), .half),
(#line, halfPos, CGPoint(x: 0.0, y: 0.0), .half),
(#line, halfPos, CGPoint(x: 0.0, y: 385.0), .tip), // projection
(#line, halfPos, CGPoint(x: 0.0, y: 1000.0), .hidden), // projection
(#line, halfPos + 10.0, CGPoint(x: 0.0, y: 100.0), .half), // redirection
(#line, tipPos - 10.0, CGPoint(x: 0.0, y: -100.0), .tip), // redirection
(#line, tipPos, CGPoint(x: 0.0, y: -3000.0), .half), //projection
(#line, tipPos, CGPoint(x: 0.0, y: -10.0), .tip),
(#line, tipPos, CGPoint(x: 0.0, y: 0.0), .tip),
(#line, tipPos, CGPoint(x: 0.0, y: 10.0), .tip),
(#line, tipPos, CGPoint(x: 0.0, y: 1000.0), .hidden), //projection
(#line, tipPos + 10.0, CGPoint(x: 0.0, y: 10.0), .tip), // redirection
(#line, tipPos - 10.0, CGPoint(x: 0.0, y: 10.0), .tip), // redirection
])
fpc.move(to: .tip, animated: false)
assertTargetState(fpc.floatingPanel, with: [
(#line, tipPos, CGPoint(x: 0.0, y: -100.0), .tip),
(#line, tipPos, CGPoint(x: 0.0, y: -1000.0), .half),
(#line, tipPos, CGPoint(x: 0.0, y: 0.0), .tip),
(#line, tipPos, CGPoint(x: 0.0, y: 1000.0), .hidden),
assertTargetPosition(
fpc.floatingPanel,
with: [
(#line, tipPos, CGPoint(x: 0.0, y: -100.0), .tip),
(#line, tipPos, CGPoint(x: 0.0, y: -1000.0), .half),
(#line, tipPos, CGPoint(x: 0.0, y: 0.0), .tip),
(#line, tipPos, CGPoint(x: 0.0, y: 1000.0), .hidden),
])
}
@@ -834,10 +867,10 @@ private class FloatingPanelLayout3PositionsBottomEdge: FloatingPanelTop2BottomTe
}
}
private typealias TestParameter = (UInt, CGFloat, CGPoint, FloatingPanelState)
private func assertTargetState(_ floatingPanel: Core, with params: [TestParameter]) {
private typealias TestParameter = (UInt, CGFloat, CGPoint, FloatingPanelState)
private func assertTargetPosition(_ floatingPanel: Core, with params: [TestParameter]) {
params.forEach { (line, pos, velocity, result) in
floatingPanel.surfaceView.frame.origin.y = pos
XCTAssertEqual(floatingPanel.targetState(from: pos, with: velocity.y), result, line: line)
XCTAssertEqual(floatingPanel.targetPosition(from: pos, with: velocity.y), result, line: line)
}
}
+1
View File
@@ -1,6 +1,7 @@
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
import XCTest
@testable import FloatingPanel
class ExtensionTests: XCTestCase {
-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)
}
}
+399 -246
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,60 +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.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)),
(
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.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))
(
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.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)),
(
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.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))
}
(
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() {
@@ -475,62 +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.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)),
(
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.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))
(
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.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)),
(
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.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)
(
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)
}
}
@@ -555,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.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(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.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))
(
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))
}
}
@@ -605,32 +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.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(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.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))
(
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 -4
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),
@@ -73,7 +74,7 @@ class FloatingPanelTop2BottomTestLayout: FloatingPanelLayout {
}
class FloatingPanelProjectableBehavior: FloatingPanelBehavior {
func shouldProjectMomentum(_ fpc: FloatingPanelController, to proposedState: FloatingPanelState) -> Bool {
func shouldProjectMomentum(_ fpc: FloatingPanelController, to proposedTargetPosition: FloatingPanelState) -> Bool {
return true
}
}
@@ -98,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