Compare commits

...

36 Commits

Author SHA1 Message Date
Shin Yamamoto debeca1fb2 Version 2.7.0 2023-08-21 19:28:57 +09:00
Shin Yamamoto 9da54f9fc1 Call floatingPanelWillRemove when a panel removes from a window 2023-08-20 22:23:41 +09:00
Shin Yamamoto a13d053867 Update the doc comment of floatingPanelDidEndDragging method 2023-08-20 22:23:41 +09:00
Shin Yamamoto eda7201fe8 Remove unnecessary code 2023-08-18 12:09:34 +09:00
Shin Yamamoto b1edef49a4 ci: update the push trigger of github actions 2023-08-17 09:02:35 +09:00
Shin Yamamoto a2db94a8c4 Fix invalid scroll offsets after moving between states
Sometimes, the content offset of the tracking scroll view would become
less than the content inset (e.g. when a panel moves from half to full
state displaying content with a top bar similar to 'Show Navigation
Controller' in the Samples app). This resulted in the content getting
fixed in an unintended position.

For this issue, this commit completely changes the scroll offset pinning
logic from one shot pinning by DispatchQueue at Core:L43-L56

That old logic was added when the UIViewPropertyAnimator was used to move
the panel. But now, the custom animator using CADisplayLink allows
fine-grained control of panel movement and then the scroll offset is
able to be pinned during its panel transitions.
2023-08-17 09:02:35 +09:00
Shin Yamamoto 57495cff84 Fix log prints 2023-08-14 22:32:14 +09:00
Shin Yamamoto eff5cde844 Remove libswiftCoreGraphics.tbd to fix a crash of SamplesObjC.app
Sometimes SamplesObjC app crashes by the following error:
> dyld[21380]: Library not loaded: @rpath/libswiftCoreGraphics.dylib
>   Reason: tried: '/usr/lib/system/introspection/libswiftCoreGraphics.dylib'
>   (no such file, not in dyld cache),
2023-08-14 22:32:14 +09:00
Shin Yamamoto c365eadf1e Retain scroll view position while moving between states (#587)
Previously, the panel might not consistently keep its scroll content
offset when moving from its most expanded state to another.

Changes made in this commit:

* Keep the content offset of tracking scroll view in the following cases.
  A panel is moved...
  1. Outside of the tracking scroll view.
  2. Inside of a navigation bar/toolbar over the tracking scroll view.

* Stopped the scroll offset reset of the `stopScrollDeceleration` flag
  in the  `panningEnd` method when the panel transitions from its most
  expanded state because there is no issue without the reset.
2023-08-14 22:28:19 +09:00
Shin Yamamoto 5d02681b05 Add 'Expand top margin' switcher in DebugTextViewController 2023-08-11 16:46:42 +09:00
Shin Yamamoto 6e17ff734a Modify shouldProjectMomentum(_:{proposedTargetPosition => proposedState}:) 2023-08-11 16:45:23 +09:00
Shin Yamamoto 02ed923e7b Replace 'Position' words 2023-08-11 16:45:23 +09:00
Shin Yamamoto 421335d98c Make the 'Show Panel over Window' panel removable 2023-08-11 16:09:47 +09:00
Shin Yamamoto 2618f49556 Add GestureTests 2023-08-11 15:01:25 +09:00
Shin Yamamoto 328116600f Update the minimum deployment target to iOS 11.0 on the Unit test target 2023-08-11 15:01:25 +09:00
Shin Yamamoto a10b1426cd Allow a 'delegateProxy' object to access the default implementations 2023-08-11 15:01:25 +09:00
Shin Yamamoto 5468856a93 Set isAttracting to true while moveAnimator is active
And make it calls the related delegate methods
2023-08-11 15:01:24 +09:00
Shin Yamamoto 8f3a7de321 Call the 'floatingPanelDidEndDragging' method after 'state' property changes
This change allows the library user to get the correct state in
'floatingPanelDidEndDragging' method while `attract` is false.
2023-08-11 15:01:01 +09:00
Shin Yamamoto 2de1fb9ac8 Call the 'floatingPanelDidMove' delegate at the end of a move interaction 2023-08-11 15:01:01 +09:00
Shin Yamamoto 27a2d81a71 Version 2.6.6 2023-08-11 14:17:09 +09:00
Shin Yamamoto 85ed3a6ce3 Fix scroll tracking issues of the scroll view with a positive scroll inset
These issues arose in 'Show Navigation Controller' sample of Samples app.

1. The scrollView's contentOffset always becomes (0, 0) instead of (0, -44),
   which is normal if there is a UINavigationBar.

2. The scrollView's contentOffset sometimes becomes (0, 0) after moving
   a panel quickly like picking.

Case 1 is caused by 7511ce5 commit.
Case 2 is caused by a workaround added at 81fd85e commit.

I tested this library from iOS 11 to iOS 16, and then I confirmed this
workaround doesn't need anymore.

Related to #602, #603.
2023-08-11 13:52:54 +09:00
Shin Yamamoto b34f1093de Version 2.6.5 2023-07-26 18:29:23 +09:00
Shin Yamamoto de1dbe70de Fix a method name for the method convention 2023-07-25 22:47:20 +09:00
Shin Yamamoto c593c646ca Fix a scroll tracking problem caused by a floating point error in LayoutAdapter.offsetFromMostExpandedAnchor
This problem arose after 6611ec8 commit. The root cause is linked to
this condition: `0 == layoutAdapter.offsetFromMostExpandedAnchor` at
line 589 in the method `Core.shouldScrollViewHandleTouch(_:point:velocity:)`.

If the value of `layoutAdapter.offsetFromMostExpandedAnchor` has a
floating point error, the condition evaluates to false. As a result,
the panel moves even when the tracking scroll view is intended to
scroll.

This problem may not occur if there is no floating point error.
2023-07-25 22:13:30 +09:00
Shin Yamamoto 0cb5307a61 Version 2.6.4 2023-07-22 10:18:18 +09:00
Shin Yamamoto 8ab3b7986c Fix test failures in ControllerTests 2023-07-20 22:42:58 +09:00
Shin Yamamoto bbdf3e7c6f Update the logging impl using unified logging system for Xcode 15
The Logging API document says that `os_log` is one of the legacy logging
symbols. However,  this library needs to be supported below iOS 14 so
`log(level:_:)` cannot be used.
2023-07-19 22:16:43 +09:00
Shin Yamamoto bdb756b665 ci: update the test matrix for iOS 16 2023-07-19 16:20:12 +09:00
Shin Yamamoto 6611ec83a2 Fix an issue where a tracking scroll content stops and the panel doesn't move (#530) 2023-07-19 16:18:41 +09:00
Shin Yamamoto a917d6a626 Version 2.6.3 2023-07-01 13:54:25 +09:00
Sören Gade 7511ce577d Fix scrollview content staying non-interactive after slowly swiping down (#597)
This was noticed when updating contained SwiftUI views rapidly.

This change is what fixed a certain bug the scroll content can be locked at a negative offset. It was most obvious when the whitespace was large, hence the offset was something around -100.0. At the same time, the contents (like buttons) were non-interactive.

Co-authored-by: Sören Gade <soeren.gade@lichtblick.de>
2023-07-01 11:52:38 +09:00
Shin Yamamoto e7d0a72440 Fix an issue where dismissalTapGestureRecognizer doesn't work in one case (#590)
`dismissalTapGestureRecognizer` didn't work when the panel is added into UIWindow directly as its subview. This PR fixes this issue and also adds the use case in Samples.app.
2023-07-01 11:49:36 +09:00
Shin Yamamoto 44923ef66e [Revised] Fix an issue scrolling jumps with a small scroll view content (#524)
This is the revised version of commit 448fc5c.

Commit 448fc5c has a critical regression in scroll tracking that can cause the
scroll content to bounce after moving a panel, for example, pulling down it from
full to half state.

By re-investigating #524, I found that this problem only occurred with the
`fitToBounds` content mode and a small scroll view content.

Therefore I fixed it in the more specific way.
2023-07-01 11:46:23 +09:00
Shin Yamamoto 2760bc7298 Modify a private typealias name 2023-06-21 21:04:28 +09:00
Shin Yamamoto d428e96b03 Add the SPI manifest YAML file 2023-06-12 20:32:04 +09:00
Shin Yamamoto 67495961e5 Remove unnecessary prints in unit testing 2023-06-11 21:59:29 +09:00
25 changed files with 570 additions and 340 deletions
+4 -3
View File
@@ -4,6 +4,7 @@ on:
push:
branches:
- master
- next
pull_request:
branches:
- '*'
@@ -51,10 +52,10 @@ jobs:
fail-fast: false
matrix:
include:
- os: "16.1"
xcode: "14.1"
- os: "16.4"
xcode: "14.3.1"
sim: "iPhone 14 Pro"
runsOn: macos-12
runsOn: macos-13
- os: "15.5"
xcode: "13.4.1"
sim: "iPhone 13 Pro"
+5
View File
@@ -0,0 +1,5 @@
version: 1
builder:
configs:
- documentation_targets: [FloatingPanel]
platform: ios
+1 -1
View File
@@ -319,7 +319,7 @@ class SearchPaneliPadBehavior: FloatingPanelBehavior {
var momentumProjectionRate: CGFloat {
return UIScrollView.DecelerationRate.fast.rawValue
}
func shouldProjectMomentum(_ fpc: FloatingPanelController, to proposedTargetPosition: FloatingPanelState) -> Bool {
func shouldProjectMomentum(_ fpc: FloatingPanelController, to proposedState: 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="21507" 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="21701" 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="21505"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21679"/>
<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="720"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="768"/>
<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="720" width="375" height="0.0"/>
<rect key="frame" x="0.0" y="768" 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="48" width="39" height="30"/>
<rect key="frame" x="20" y="0.0" 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="136" width="106" height="326"/>
<rect key="frame" x="134.5" y="88" 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="778"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="744"/>
<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,14 +761,33 @@ 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"/>
@@ -780,7 +799,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="-1" y="734"/>
<point key="canvasLocation" x="-2.1739130434782612" y="733.92857142857144"/>
</scene>
<!--Adaptive Layout Test View Controller-->
<scene sceneID="rDI-lU-wEx">
@@ -37,6 +37,14 @@ 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,6 +10,7 @@ enum UseCase: Int, CaseIterable {
case showPanelModal
case showMultiPanelModal
case showPanelInSheetModal
case showOnWindow
case showTabBar
case showPageView
case showPageContentView
@@ -34,6 +35,7 @@ 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"
@@ -65,6 +67,7 @@ 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,6 +11,7 @@ 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
@@ -157,6 +158,20 @@ 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,7 +15,6 @@
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 */
@@ -53,7 +52,6 @@
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.6.2"
s.version = "2.7.0"
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.
+11 -7
View File
@@ -21,11 +21,12 @@
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 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54ABD7AE216CCFF7002E6C13 /* Logger.swift */; };
54ABD7AF216CCFF7002E6C13 /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54ABD7AE216CCFF7002E6C13 /* Logging.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 */; };
@@ -63,11 +64,12 @@
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 /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; };
54ABD7AE216CCFF7002E6C13 /* Logging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logging.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>"; };
@@ -133,7 +135,7 @@
545DBA2A2152383100CA77B8 /* GrabberView.swift */,
54352E9521A51A2500CBCA08 /* Transitioning.swift */,
54DBA3DB262E938500D75969 /* Extensions.swift */,
54ABD7AE216CCFF7002E6C13 /* Logger.swift */,
54ABD7AE216CCFF7002E6C13 /* Logging.swift */,
545DB9C42151169500CA77B8 /* FloatingPanel.h */,
545DB9C52151169500CA77B8 /* Info.plist */,
54E3992627141F5100A8F9ED /* FloatingPanel.docc */,
@@ -146,6 +148,7 @@
children = (
54A6B6B022968B530077F348 /* CoreTests.swift */,
545DB9CF2151169500CA77B8 /* ControllerTests.swift */,
547F7A9B2A6E946000303905 /* GestureTests.swift */,
542753C522C49A6E00D17955 /* LayoutTests.swift */,
54A6B6B72296A8520077F348 /* SurfaceViewTests.swift */,
549E944422CF295D0050AECF /* StateTests.swift */,
@@ -282,7 +285,7 @@
54CDC5D5215B6D8D007D205C /* BackdropView.swift in Sources */,
54352E9821A521CA00CBCA08 /* PassthroughView.swift in Sources */,
54CFBFC5215CD09C006B5735 /* Core.swift in Sources */,
54ABD7AF216CCFF7002E6C13 /* Logger.swift in Sources */,
54ABD7AF216CCFF7002E6C13 /* Logging.swift in Sources */,
545DB9E021511AC100CA77B8 /* Controller.swift in Sources */,
54DBA3DC262E938500D75969 /* Extensions.swift in Sources */,
5450EEE421646DF500135936 /* Behavior.swift in Sources */,
@@ -304,6 +307,7 @@
542753C622C49A6E00D17955 /* LayoutTests.swift in Sources */,
54A6B6B82296A8520077F348 /* SurfaceViewTests.swift in Sources */,
546055BF2333C4740069F400 /* TestSupports.swift in Sources */,
547F7A9C2A6E946000303905 /* GestureTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -511,7 +515,7 @@
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = "";
INFOPLIST_FILE = Tests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -531,7 +535,7 @@
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = "";
INFOPLIST_FILE = Tests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -647,7 +651,7 @@
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = "";
INFOPLIST_FILE = Tests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
+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.6.2/documentation/floatingpanel/) for more details, powered by [DocC](https://developer.apple.com/documentation/docc).
Please see also [the API reference](https://floatingpanel.github.io/2.7.0/documentation/floatingpanel/) for more details.
![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 proposedTargetPosition: FloatingPanelState) -> Bool {
func shouldProjectMomentum(_ fpc: FloatingPanelController, to proposedState: 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 proposedTargetPosition: FloatingPanelPosition) -> Bool {
func shouldProjectMomentum(_ fpc: FloatingPanelController, to proposedState: FloatingPanelPosition) -> Bool {
return true
}
}
+2 -2
View File
@@ -28,9 +28,9 @@ public protocol FloatingPanelBehavior {
/// 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, `proposedTargetPosition` can only be `FloatingPanelState.tip` or `FloatingPanelState.full`.
/// Therefore, `proposedState` can only be `FloatingPanelState.tip` or `FloatingPanelState.full`.
@objc optional
func shouldProjectMomentum(_ fpc: FloatingPanelController, to proposedTargetPosition: FloatingPanelState) -> Bool
func shouldProjectMomentum(_ fpc: FloatingPanelController, to proposedState: FloatingPanelState) -> Bool
/// Returns the progress to redirect to the previous position.
///
+24 -9
View File
@@ -1,6 +1,7 @@
// 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
@@ -51,7 +52,12 @@ import UIKit
/// Called on finger up if the user dragged.
///
/// If `attract` is true, it will continue moving afterwards to a nearby state anchor.
/// 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)
@@ -167,7 +173,8 @@ open class FloatingPanelController: UIViewController {
set {
_layout = newValue
if let parent = parent, let layout = newValue as? UIViewController, layout == parent {
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.")
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)
}
}
}
@@ -179,7 +186,8 @@ open class FloatingPanelController: UIViewController {
set {
_behavior = newValue
if let parent = parent, let behavior = newValue as? UIViewController, behavior == parent {
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.")
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)
}
}
}
@@ -213,7 +221,7 @@ open class FloatingPanelController: UIViewController {
/// The NearbyState determines that finger's nearby state.
public var nearbyState: FloatingPanelState {
let currentY = surfaceLocation.y
return floatingPanel.targetPosition(from: currentY, with: .zero)
return floatingPanel.targetState(from: currentY, with: .zero)
}
/// Constants that define how a panel content fills in the surface.
@@ -375,7 +383,7 @@ open class FloatingPanelController: UIViewController {
preSafeAreaInsets != safeAreaInsets
else { return }
log.debug("Update safeAreaInsets", safeAreaInsets)
os_log(msg, log: devLog, type: .debug, "Update safeAreaInsets = \(safeAreaInsets)")
// Prevent an infinite loop on iOS 10: setUpLayout() -> viewDidLayoutSubviews() -> setUpLayout()
preSafeAreaInsets = safeAreaInsets
@@ -414,8 +422,15 @@ open class FloatingPanelController: UIViewController {
guard let self = self else { return }
self.delegate?.floatingPanelDidRemove?(self)
}
} else {
} 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)
}
}
}
@@ -476,7 +491,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 {
log.warning("Already added to a parent(\(parent))")
os_log(msg, log: sysLog, type: .error, "Warning: already added to a parent(\(parent))")
return
}
assert((parent is UINavigationController) == false, "UINavigationController displays only one child view controller at a time.")
@@ -695,7 +710,7 @@ extension FloatingPanelController {
// 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)
@@ -709,7 +724,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
+231 -147
View File
@@ -1,6 +1,7 @@
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
import UIKit
import os.log
///
/// The presentation model of FloatingPanel
@@ -20,11 +21,13 @@ class Core: NSObject, UIGestureRecognizerDelegate {
if let cur = scrollView {
if oldValue == nil {
initialScrollOffset = cur.contentOffset
scrollBounce = cur.bounces
scrollIndictorVisible = cur.showsVerticalScrollIndicator
}
} else {
if let pre = oldValue {
pre.isDirectionalLockEnabled = false
pre.bounces = scrollBounce
pre.showsVerticalScrollIndicator = scrollIndictorVisible
}
}
@@ -33,7 +36,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
private(set) var state: FloatingPanelState = .hidden {
didSet {
log.debug("state changed: \(oldValue) -> \(state)")
os_log(msg, log: devLog, type: .debug, "state changed: \(oldValue) -> \(state)")
if let vc = ownerVC {
vc.delegate?.floatingPanelDidChangeState?(vc)
}
@@ -41,6 +44,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
}
let panGestureRecognizer: FloatingPanelPanGestureRecognizer
let panGestureDelegateRouter: FloatingPanelPanGestureRecognizer.DelegateRouter
var isRemovalInteractionEnabled: Bool = false
fileprivate var isSuspended: Bool = false // Prevent a memory leak in the modal transition
@@ -61,8 +65,9 @@ class Core: NSObject, UIGestureRecognizerDelegate {
// Scroll handling
private var initialScrollOffset: CGPoint = .zero
private var stopScrollDeceleration: Bool = false
private var scrollBounce = false
private var scrollIndictorVisible = false
private var scrollBounceThreshold: CGFloat = -30.0
// MARK: - Interface
@@ -81,17 +86,19 @@ class Core: NSObject, UIGestureRecognizerDelegate {
behaviorAdapter = BehaviorAdapter(vc: vc, behavior: behavior)
panGestureRecognizer = FloatingPanelPanGestureRecognizer()
if #available(iOS 11.0, *) {
panGestureRecognizer.name = "FloatingPanelPanGestureRecognizer"
}
panGestureDelegateRouter = FloatingPanelPanGestureRecognizer.DelegateRouter(panGestureRecognizer: panGestureRecognizer)
super.init()
panGestureRecognizer.floatingPanel = self
panGestureRecognizer.set(floatingPanel: self)
surfaceView.addGestureRecognizer(panGestureRecognizer)
panGestureRecognizer.addTarget(self, action: #selector(handle(panGesture:)))
panGestureRecognizer.delegate = self
// Assign the delegate router to `FloatingPanelPanGestureRecognizer.delegate` only after setting
// `FloatingPanelPanGestureRecognizer.floatingPanel` property.
// This is because `delegateOrigin` is used at the time of assignment to its `delegate` property
// through the delegate router.
panGestureRecognizer.delegate = panGestureDelegateRouter
// Set the tap-to-dismiss action of the backdrop view.
// It's disabled by default. See also BackdropView.dismissalTapGestureRecognizer.
@@ -123,7 +130,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
if animated {
let updateScrollView: () -> Void = { [weak self] in
guard let self = self else { return }
if self.state == self.layoutAdapter.mostExpandedState, abs(self.layoutAdapter.offsetFromMostExpandedAnchor) <= 1.0 {
if self.state == self.layoutAdapter.mostExpandedState, 0 == self.layoutAdapter.offsetFromMostExpandedAnchor {
self.unlockScrollView()
} else {
self.lockScrollView()
@@ -138,10 +145,8 @@ class Core: NSObject, UIGestureRecognizerDelegate {
let animationVector = CGVector(dx: abs(removalVector.dx), dy: abs(removalVector.dy))
animator = vc.animatorForDismissing(with: animationVector)
default:
move(to: to, with: 0) { [weak self] in
guard let self = self else { return }
self.moveAnimator = nil
startAttraction(to: to, with: .zero) { [weak self] in
self?.endAttraction(false)
updateScrollView()
completion?()
}
@@ -159,7 +164,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
self.updateLayout(to: to)
if shouldDoubleLayout {
log.info("Lay out the surface again to modify an intrinsic size error according to UIStackView")
os_log(msg, log: sysLog, type: .info, "Lay out the surface again to modify an intrinsic size error according to UIStackView")
self.updateLayout(to: to)
}
}
@@ -228,7 +233,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
}
func getBackdropAlpha(at cur: CGFloat, with translation: CGFloat) -> CGFloat {
/* log.debug("currentY: \(currentY) translation: \(translation)") */
/* os_log(msg, log: devLog, type: .debug, "currentY: \(currentY) translation: \(translation)") */
let forwardY = (translation >= 0)
let segment = layoutAdapter.segment(at: cur, forward: forwardY)
@@ -255,13 +260,9 @@ class Core: NSObject, UIGestureRecognizerDelegate {
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if let result = panGestureRecognizer.delegateProxy?.gestureRecognizer?(gestureRecognizer, shouldRecognizeSimultaneouslyWith: otherGestureRecognizer) {
return result
}
guard gestureRecognizer == panGestureRecognizer else { return false }
/* log.debug("shouldRecognizeSimultaneouslyWith", otherGestureRecognizer) */
/* os_log(msg, log: devLog, type: .debug, "shouldRecognizeSimultaneouslyWith", otherGestureRecognizer) */
switch otherGestureRecognizer {
case is FloatingPanelPanGestureRecognizer:
@@ -286,10 +287,6 @@ class Core: NSObject, UIGestureRecognizerDelegate {
}
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if let result = panGestureRecognizer.delegateProxy?.gestureRecognizer?(gestureRecognizer, shouldBeRequiredToFailBy: otherGestureRecognizer) {
return result
}
if otherGestureRecognizer is FloatingPanelPanGestureRecognizer {
// If this panel is the farthest descendant of visible panels,
// its ancestors' pan gesture must wait for its pan gesture to fail
@@ -311,10 +308,6 @@ class Core: NSObject, UIGestureRecognizerDelegate {
}
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if let result = panGestureRecognizer.delegateProxy?.gestureRecognizer?(gestureRecognizer, shouldRequireFailureOf: otherGestureRecognizer) {
return result
}
guard gestureRecognizer == panGestureRecognizer else { return false }
// Should begin the pan gesture without waiting for the tracking scroll view's gestures.
@@ -334,7 +327,10 @@ class Core: NSObject, UIGestureRecognizerDelegate {
if surfaceView.grabberAreaContains(gestureRecognizer.location(in: surfaceView)) {
return false
}
return allowScrollPanGesture(for: scrollView)
guard state == layoutAdapter.mostExpandedState else { return false }
// The condition where offset > 0 must not be included here. Because it will stop recognizing
// the panel pan gesture if a user starts scrolling content from an offset greater than 0.
return allowScrollPanGesture(of: scrollView) { offset in offset <= scrollBounceThreshold }
default:
return false
}
@@ -385,15 +381,16 @@ class Core: NSObject, UIGestureRecognizerDelegate {
let velocity = value(of: panGesture.velocity(in: panGesture.view))
let location = panGesture.location(in: surfaceView)
let insideMostExpandedAnchor = 0 > layoutAdapter.offsetFromMostExpandedAnchor + (1.0 / surfaceView.fp_displayScale)
let insideMostExpandedAnchor = 0 > layoutAdapter.offsetFromMostExpandedAnchor
log.debug("""
os_log(msg, log: devLog, type: .debug, """
scroll gesture(\(state):\(panGesture.state)) -- \
inside expanded anchor = \(insideMostExpandedAnchor), \
interactionInProgress = \(interactionInProgress), \
scroll offset = \(value(of: scrollView.contentOffset)), \
location = \(value(of: location)), velocity = \(velocity)
""")
"""
)
let offsetDiff = value(of: scrollView.contentOffset - contentOffsetForPinning(of: scrollView))
@@ -401,7 +398,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
// Scroll offset pinning
if state == layoutAdapter.mostExpandedState {
if interactionInProgress {
log.debug("settle offset --", value(of: initialScrollOffset))
os_log(msg, log: devLog, type: .debug, "settle offset -- \(value(of: initialScrollOffset))")
stopScrolling(at: initialScrollOffset)
} else {
if surfaceView.grabberAreaContains(location) {
@@ -453,21 +450,24 @@ class Core: NSObject, UIGestureRecognizerDelegate {
}
} else {
if state == layoutAdapter.mostExpandedState {
let allowScroll = allowScrollPanGesture(of: scrollView) { offset in
offset <= scrollBounceThreshold || 0 < offset
}
switch layoutAdapter.position {
case .top, .left:
if velocity < 0, !allowScrollPanGesture(for: scrollView) {
lockScrollView()
if velocity < 0, !allowScroll {
lockScrollView(strict: true)
}
if velocity > 0, allowScrollPanGesture(for: scrollView) {
if velocity > 0, allowScroll {
unlockScrollView()
}
case .bottom, .right:
// Hide a scroll indicator just before starting an interaction by swiping a panel down.
if velocity > 0, !allowScrollPanGesture(for: scrollView) {
lockScrollView()
if velocity > 0, !allowScroll {
lockScrollView(strict: true)
}
// Show a scroll indicator when an animation is interrupted at the top and content is scrolled up
if velocity < 0, allowScrollPanGesture(for: scrollView) {
if velocity < 0, allowScroll {
unlockScrollView()
}
}
@@ -483,7 +483,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
let velocity = panGesture.velocity(in: panGesture.view)
let location = panGesture.location(in: panGesture.view)
log.debug("""
os_log(msg, log: devLog, type: .debug, """
panel gesture(\(state):\(panGesture.state)) -- \
translation = \(value(of: translation)), \
location = \(value(of: location)), \
@@ -533,19 +533,19 @@ class Core: NSObject, UIGestureRecognizerDelegate {
private func interruptAnimationIfNeeded() {
if let animator = self.moveAnimator, animator.isRunning {
log.debug("the attraction animator interrupted!!!")
os_log(msg, log: devLog, type: .debug, "the attraction animator interrupted!!!")
animator.stopAnimation(true)
endAttraction(false)
}
if let animator = self.transitionAnimator {
guard 0 >= layoutAdapter.offsetFromMostExpandedAnchor else { return }
log.debug("a panel animation(interruptible: \(animator.isInterruptible)) interrupted!!!")
os_log(msg, log: devLog, type: .debug, "a panel animation(interruptible: \(animator.isInterruptible)) interrupted!!!")
if animator.isInterruptible {
animator.stopAnimation(false)
// A user can stop a panel at the nearest Y of a target position so this fine-tunes
// the a small gap between the presentation layer frame and model layer frame
// to unlock scroll view properly at finishAnimation(at:)
if abs(layoutAdapter.offsetFromMostExpandedAnchor) <= 1.0 {
if 0 == layoutAdapter.offsetFromMostExpandedAnchor {
layoutAdapter.surfaceLocation = layoutAdapter.surfaceLocation(for: layoutAdapter.mostExpandedState)
}
animator.finishAnimation(at: .current)
@@ -631,7 +631,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
// A user interaction does not always start from Began state of the pan gesture
// because it can be recognized in scrolling a content in a content view controller.
// So here just preserve the current state if needed.
log.debug("panningBegan -- location = \(value(of: location))")
os_log(msg, log: devLog, type: .debug, "panningBegan -- location = \(value(of: location))")
guard let scrollView = scrollView else { return }
if state == layoutAdapter.mostExpandedState {
@@ -644,7 +644,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
}
private func panningChange(with translation: CGPoint) {
log.debug("panningChange -- translation = \(value(of: translation))")
os_log(msg, log: devLog, type: .debug, "panningChange -- translation = \(value(of: translation))")
let pre = value(of: layoutAdapter.surfaceLocation)
let diff = value(of: translation - initialTranslation)
let next = pre + diff
@@ -697,27 +697,18 @@ class Core: NSObject, UIGestureRecognizerDelegate {
}
private func panningEnd(with translation: CGPoint, velocity: CGPoint) {
log.debug("panningEnd -- translation = \(value(of: translation)), velocity = \(value(of: velocity))")
os_log(msg, log: devLog, type: .debug, "panningEnd -- translation = \(value(of: translation)), velocity = \(value(of: velocity))")
if state == .hidden {
log.debug("Already hidden")
os_log(msg, log: devLog, type: .debug, "Already hidden")
return
}
stopScrollDeceleration = (0 > layoutAdapter.offsetFromMostExpandedAnchor + (1.0 / surfaceView.fp_displayScale)) // Projecting the dragging to the scroll dragging or not
if stopScrollDeceleration {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.stopScrolling(at: self.initialScrollOffset)
}
}
let currentPos = value(of: layoutAdapter.surfaceLocation)
let mainVelocity = value(of: velocity)
var targetPosition = self.targetPosition(from: currentPos, with: mainVelocity)
var target = self.targetState(from: currentPos, with: mainVelocity)
endInteraction(for: targetPosition)
endInteraction(for: target)
if isRemovalInteractionEnabled {
let distToHidden = CGFloat(abs(currentPos - layoutAdapter.position(for: .hidden)))
@@ -734,17 +725,18 @@ class Core: NSObject, UIGestureRecognizerDelegate {
}
if let vc = ownerVC {
vc.delegate?.floatingPanelWillEndDragging?(vc, withVelocity: velocity, targetState: &targetPosition)
vc.delegate?.floatingPanelWillEndDragging?(vc, withVelocity: velocity, targetState: &target)
}
guard shouldAttract(to: targetPosition) else {
guard shouldAttract(to: target) else {
self.state = target
self.updateLayout(to: target)
self.unlockScrollView()
// The `floatingPanelDidEndDragging(_:willAttract:)` must be called after the state property changes.
// This allows library users to get the correct state in the delegate method.
if let vc = ownerVC {
vc.delegate?.floatingPanelDidEndDragging?(vc, willAttract: false)
}
self.state = targetPosition
self.updateLayout(to: targetPosition)
self.unlockScrollView()
return
}
@@ -752,18 +744,8 @@ class Core: NSObject, UIGestureRecognizerDelegate {
vc.delegate?.floatingPanelDidEndDragging?(vc, willAttract: true)
}
// Workaround: Disable a tracking scroll to prevent bouncing a scroll content in a panel animating
let isScrollEnabled = scrollView?.isScrollEnabled
if let scrollView = scrollView, targetPosition != layoutAdapter.mostExpandedState {
scrollView.isScrollEnabled = false
}
startAttraction(to: targetPosition, with: velocity)
// Workaround: Reset `self.scrollView.isScrollEnabled`
if let scrollView = scrollView, targetPosition != layoutAdapter.mostExpandedState,
let isScrollEnabled = isScrollEnabled {
scrollView.isScrollEnabled = isScrollEnabled
startAttraction(to: target, with: velocity) { [weak self] in
self?.endAttraction(true)
}
}
@@ -789,24 +771,56 @@ class Core: NSObject, UIGestureRecognizerDelegate {
private func startInteraction(with translation: CGPoint, at location: CGPoint) {
/* Don't lock a scroll view to show a scroll indicator after hitting the top */
log.debug("startInteraction -- translation = \(value(of: translation)), location = \(value(of: location))")
os_log(msg, log: devLog, type: .debug, "startInteraction -- translation = \(value(of: translation)), location = \(value(of: location))")
guard interactionInProgress == false else { return }
var offset: CGPoint = .zero
initialSurfaceLocation = layoutAdapter.surfaceLocation
if state == layoutAdapter.mostExpandedState, let scrollView = scrollView {
if surfaceView.grabberAreaContains(location) {
let scrollFrame = scrollView.convert(scrollView.bounds, to: nil)
let touchStartingPoint = surfaceView.convert(initialLocation, to: nil)
ifLabel: if surfaceView.grabberAreaContains(location) {
initialScrollOffset = scrollView.contentOffset
} else {
} else if scrollFrame.contains(touchStartingPoint) {
let pinningOffset = contentOffsetForPinning(of: scrollView)
// `scrollView.contentOffset` can be a value in [-30, 0) at this time by `allowScrollPanGesture(for:)`.
// Therefore the initial scroll offset must be reset to the pinning offset. Otherwise, the following
// `Fit the surface bounds` logic don't working and also the scroll content offset can be invalid.
// This code block handles the scenario where there's a navigation bar or toolbar
// above the tracking scroll view with corresponding content insets set, and users
// move the panel by interacting with these bars. One case of the scenario can be
// tested with 'Show Navigation Controller' in Samples.app
do {
// Adjust the location by subtracting scrollView's origin to reference the frame
// rectangle of the scroll view itself.
let _location = scrollView.convert(location, from: surfaceView) - scrollView.bounds.origin
os_log(msg, log: devLog, type: .debug, "startInteraction -- location in scroll view = \(_location))")
// Keep the scroll content offset if the current touch position is inside its
// content inset area.
switch layoutAdapter.position {
case .top, .left:
let base = value(of: scrollView.bounds.size)
if value(of: pinningOffset) + (base - value(of: _location)) < 0 {
initialScrollOffset = scrollView.contentOffset
break ifLabel
}
case .bottom, .right:
if value(of: pinningOffset) + value(of: _location) < 0 {
initialScrollOffset = scrollView.contentOffset
break ifLabel
}
}
}
// `initialScrollOffset` must be reset to the pinning offset because the value of `scrollView.contentOffset`,
// for instance, is a value in [-30, 0) on a bottom positioned panel with `allowScrollPanGesture(of:condition:)`.
// If it's not reset, the following logic to shift the surface frame will not work and then the scroll
// content offset will become an unexpected value.
initialScrollOffset = pinningOffset
// Fit the surface bounds to a scroll offset content by startInteraction(at:offset:)
// Shift the surface frame to negate the scroll content offset at startInteraction(at:offset:)
let offsetDiff = scrollView.contentOffset - pinningOffset
switch layoutAdapter.position {
case .top, .left:
@@ -818,8 +832,10 @@ class Core: NSObject, UIGestureRecognizerDelegate {
offset = -offsetDiff
}
}
} else {
initialScrollOffset = scrollView.contentOffset
}
log.debug("initial scroll offset --", initialScrollOffset)
os_log(msg, log: devLog, type: .debug, "initial scroll offset -- \(initialScrollOffset)")
}
initialTranslation = translation
@@ -835,21 +851,21 @@ class Core: NSObject, UIGestureRecognizerDelegate {
lockScrollView()
}
private func endInteraction(for targetPosition: FloatingPanelState) {
log.debug("endInteraction to \(targetPosition)")
private func endInteraction(for state: FloatingPanelState) {
os_log(msg, log: devLog, type: .debug, "endInteraction to \(state)")
if let scrollView = scrollView {
log.debug("endInteraction -- scroll offset = \(scrollView.contentOffset)")
os_log(msg, log: devLog, type: .debug, "endInteraction -- scroll offset = \(scrollView.contentOffset)")
}
interactionInProgress = false
// Prevent to keep a scroll view indicator visible at the half/tip position
if targetPosition != layoutAdapter.mostExpandedState {
if state != layoutAdapter.mostExpandedState {
lockScrollView()
}
layoutAdapter.endInteraction(at: targetPosition)
layoutAdapter.endInteraction(at: state)
}
private func tearDownActiveInteraction() {
@@ -859,26 +875,24 @@ class Core: NSObject, UIGestureRecognizerDelegate {
panGestureRecognizer.isEnabled = true
}
private func shouldAttract(to targetState: FloatingPanelState) -> Bool {
if layoutAdapter.position(for: targetState) == value(of: layoutAdapter.surfaceLocation) {
private func shouldAttract(to state: FloatingPanelState) -> Bool {
if layoutAdapter.position(for: state) == value(of: layoutAdapter.surfaceLocation) {
return false
}
return true
}
private func startAttraction(to targetPosition: FloatingPanelState, with velocity: CGPoint) {
log.debug("startAnimation to \(targetPosition) -- velocity = \(value(of: velocity))")
private func startAttraction(to state: FloatingPanelState, with velocity: CGPoint, completion: @escaping (() -> Void)) {
os_log(msg, log: devLog, type: .debug, "startAnimation to \(state) -- velocity = \(value(of: velocity))")
guard let vc = ownerVC else { return }
isAttracting = true
vc.delegate?.floatingPanelWillBeginAttracting?(vc, to: targetPosition)
move(to: targetPosition, with: value(of: velocity)) {
self.endAttraction(true)
}
vc.delegate?.floatingPanelWillBeginAttracting?(vc, to: state)
move(to: state, with: value(of: velocity), completion: completion)
}
private func move(to targetPosition: FloatingPanelState, with velocity: CGFloat, completion: @escaping (() -> Void)) {
let (animationConstraint, target) = layoutAdapter.setUpAttraction(to: targetPosition)
private func move(to state: FloatingPanelState, with velocity: CGFloat, completion: @escaping (() -> Void)) {
let (animationConstraint, target) = layoutAdapter.setUpAttraction(to: state)
let initialData = NumericSpringAnimator.Data(value: animationConstraint.constant, velocity: velocity)
moveAnimator = NumericSpringAnimator(
initialData: initialData,
@@ -891,22 +905,34 @@ class Core: NSObject, UIGestureRecognizerDelegate {
let ownerVC = self.ownerVC // Ensure the owner vc is existing for `layoutAdapter.surfaceLocation`
else { return }
animationConstraint.constant = data.value
let current = self.value(of: self.layoutAdapter.surfaceLocation)
let translation = data.value - initialData.value
self.backdropView.alpha = self.getBackdropAlpha(at: current, with: translation)
// Pin the offset of the tracking scroll view while moving by this animator
if let scrollView = self.scrollView {
self.stopScrolling(at: self.initialScrollOffset)
os_log(msg, log: devLog, type: .debug, "move -- pinning scroll offset = \(scrollView.contentOffset)")
}
ownerVC.notifyDidMove()
},
completion: { [weak self] in
guard let self = self,
self.ownerVC != nil else { return }
self.updateLayout(to: targetPosition)
let ownerVC = self.ownerVC
else { return }
self.updateLayout(to: state)
// Notify when it has reached the target anchor point. At this point, the surface location is equal to
// the target anchor location.
ownerVC.notifyDidMove()
completion()
})
moveAnimator?.startAnimation()
state = targetPosition
self.state = state
}
private func endAttraction(_ finished: Bool) {
private func endAttraction(_ tryUnlockScroll: Bool) {
self.isAttracting = false
self.moveAnimator = nil
@@ -915,18 +941,20 @@ class Core: NSObject, UIGestureRecognizerDelegate {
}
if let scrollView = scrollView {
log.debug("finishAnimation -- scroll offset = \(scrollView.contentOffset)")
os_log(msg, log: devLog, type: .debug, "finishAnimation -- scroll offset = \(scrollView.contentOffset)")
}
stopScrollDeceleration = false
log.debug("""
os_log(msg, log: devLog, type: .debug, """
finishAnimation -- state = \(state) \
surface location = \(layoutAdapter.surfaceLocation) \
edge most position = \(layoutAdapter.surfaceLocation(for: layoutAdapter.mostExpandedState))
""")
if finished, state == layoutAdapter.mostExpandedState, abs(layoutAdapter.offsetFromMostExpandedAnchor) <= 1.0 {
unlockScrollView()
if tryUnlockScroll {
if (state == layoutAdapter.mostExpandedState && 0 == layoutAdapter.offsetFromMostExpandedAnchor)
|| shouldLooselyLockScrollView {
unlockScrollView()
}
}
}
@@ -934,6 +962,10 @@ class Core: NSObject, UIGestureRecognizerDelegate {
return layoutAdapter.position.mainLocation(point)
}
func value(of size: CGSize) -> CGFloat {
return layoutAdapter.position.mainDimension(size)
}
func setValue(_ newValue: CGPoint, to point: inout CGPoint) {
switch layoutAdapter.position {
case .top, .bottom:
@@ -949,8 +981,8 @@ class Core: NSObject, UIGestureRecognizerDelegate {
return (initialVelocity / 1000.0) * decelerationRate / (1.0 - decelerationRate)
}
func targetPosition(from currentY: CGFloat, with velocity: CGFloat) -> (FloatingPanelState) {
log.debug("targetPosition -- currentY = \(currentY), velocity = \(velocity)")
func targetState(from currentY: CGFloat, with velocity: CGFloat) -> FloatingPanelState {
os_log(msg, log: devLog, type: .debug, "targetState -- currentY = \(currentY), velocity = \(velocity)")
let sortedPositions = layoutAdapter.sortedAnchorStatesByCoordinate
@@ -976,7 +1008,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
(fromPos, toPos) = forwardY ? (lowerPos, upperPos) : (upperPos, lowerPos)
if behaviorAdapter.shouldProjectMomentum(to: toPos) == false {
log.debug("targetPosition -- negate projection: distance = \(distance)")
os_log(msg, log: devLog, type: .debug, "targetState -- negate projection: distance = \(distance)")
let segment = layoutAdapter.segment(at: currentY, forward: forwardY)
var (lowerPos, upperPos) = (segment.lower ?? sortedPositions.first!, segment.upper ?? sortedPositions.last!)
// Equate the segment out of {top,bottom} most state to the {top,bottom} most segment
@@ -1021,31 +1053,51 @@ class Core: NSObject, UIGestureRecognizerDelegate {
scrollView.transform = CGAffineTransform(translationX: 0, y: contentOffset)
}
private func lockScrollView() {
private func lockScrollView(strict: Bool = false) {
guard let scrollView = scrollView else { return }
if scrollView.isLocked {
log.debug("Already scroll locked.")
os_log(msg, log: devLog, type: .debug, "Already scroll locked.")
return
}
log.debug("lock scroll view")
os_log(msg, log: devLog, type: .debug, "lock scroll view")
scrollIndictorVisible = scrollView.showsVerticalScrollIndicator
// Must not modify the UIScrollView.bounces property here. If you reset it to unlock the tracking scroll view,
// UIScrollView may unexpectedly alter the scroll offset when dealing with small scrollable content.
if !strict, shouldLooselyLockScrollView {
// Don't change its `bounces` property. If it's changed, it will cause its scroll content offset jump at
// the most expanded anchor position while seamlessly scrolling content. This problem only occurs where its
// content mode is `.fitToBounds` and the tracking scroll content is smaller than the content view size.
// The reason why is because `bounces` prop change leads to the "content frame" change on `.fitToBounds`.
// See also https://github.com/scenee/FloatingPanel/issues/524.
} else {
scrollBounce = scrollView.bounces
scrollView.bounces = false
}
scrollView.isDirectionalLockEnabled = true
scrollView.showsVerticalScrollIndicator = false
}
private func unlockScrollView() {
guard let scrollView = scrollView, scrollView.isLocked else { return }
log.debug("unlock scroll view")
os_log(msg, log: devLog, type: .debug, "unlock scroll view")
scrollView.bounces = scrollBounce
scrollView.isDirectionalLockEnabled = false
scrollView.showsVerticalScrollIndicator = scrollIndictorVisible
}
private var shouldLooselyLockScrollView: Bool {
var isSmallScrollContentAndFitToBoundsMode: Bool {
if ownerVC?.contentMode == .fitToBounds, let scrollView = scrollView,
value(of: scrollView.contentSize) < value(of: scrollView.bounds.size) - min(layoutAdapter.offsetFromMostExpandedAnchor, 0) {
return true
}
return false
}
return isSmallScrollContentAndFitToBoundsMode
}
private func stopScrolling(at contentOffset: CGPoint) {
// Must use setContentOffset(_:animated) to force-stop deceleration
guard let scrollView = scrollView else { return }
@@ -1070,43 +1122,41 @@ class Core: NSObject, UIGestureRecognizerDelegate {
}
}
private func allowScrollPanGesture(for scrollView: UIScrollView) -> Bool {
guard state == layoutAdapter.mostExpandedState else { return false }
var offsetY: CGFloat = 0
private func allowScrollPanGesture(of scrollView: UIScrollView, condition: (_ offset: CGFloat) -> Bool) -> Bool {
var offset: CGFloat = 0
switch layoutAdapter.position {
case .top, .left:
offsetY = value(of: scrollView.fp_contentOffsetMax - scrollView.contentOffset)
offset = value(of: scrollView.fp_contentOffsetMax - scrollView.contentOffset)
case .bottom, .right:
offsetY = value(of: scrollView.contentOffset - contentOffsetForPinning(of: scrollView))
offset = value(of: scrollView.contentOffset - contentOffsetForPinning(of: scrollView))
}
return offsetY <= -30.0 || offsetY > 0
}
// MARK: - UIPanGestureRecognizer Intermediation
override func responds(to aSelector: Selector!) -> Bool {
return super.responds(to: aSelector) || panGestureRecognizer.delegateProxy?.responds(to: aSelector) == true
}
override func forwardingTarget(for aSelector: Selector!) -> Any? {
if panGestureRecognizer.delegateProxy?.responds(to: aSelector) == true {
return panGestureRecognizer.delegateProxy
}
return super.forwardingTarget(for: aSelector)
return condition(offset)
}
}
/// A gesture recognizer that looks for panning (dragging) gestures in a panel.
public final class FloatingPanelPanGestureRecognizer: UIPanGestureRecognizer {
fileprivate weak var floatingPanel: Core?
fileprivate var initialLocation: CGPoint = .zero
private weak var floatingPanel: Core! // Core has this gesture recognizer as non-optional
fileprivate func set(floatingPanel: Core) {
self.floatingPanel = floatingPanel
}
init() {
super.init(target: nil, action: nil)
if #available(iOS 11.0, *) {
name = "FloatingPanelPanGestureRecognizer"
}
}
public override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event)
initialLocation = touches.first?.location(in: view) ?? .zero
if floatingPanel?.transitionAnimator != nil || floatingPanel?.moveAnimator != nil {
if floatingPanel.transitionAnimator != nil || floatingPanel.moveAnimator != nil {
self.state = .began
}
}
/// The delegate of the gesture recognizer.
///
/// - Note: The delegate is used by FloatingPanel itself. If you set your own delegate object, an
@@ -1116,10 +1166,12 @@ public final class FloatingPanelPanGestureRecognizer: UIPanGestureRecognizer {
return super.delegate
}
set {
guard newValue is Core else {
let exception = NSException(name: .invalidArgumentException,
reason: "FloatingPanelController's built-in pan gesture recognizer must have its controller as its delegate. Use 'delegateProxy' property.",
userInfo: nil)
guard newValue is DelegateRouter else {
let exception = NSException(
name: .invalidArgumentException,
reason: "FloatingPanelController's built-in pan gesture recognizer must have its controller as its delegate. Use 'delegateProxy' property.",
userInfo: nil
)
exception.raise()
return
}
@@ -1127,12 +1179,44 @@ public final class FloatingPanelPanGestureRecognizer: UIPanGestureRecognizer {
}
}
/// An object to intercept the delegate of the gesture recognizer.
/// The default object implementing a set methods of the delegate of the gesture recognizer.
///
/// If an object adopting `UIGestureRecognizerDelegate` is set, the delegate methods are proxied to it.
/// Use this property with ``delegateProxy`` when you need to use the default gesture behaviors in a proxy implementation.
public var delegateOrigin: UIGestureRecognizerDelegate {
return floatingPanel
}
/// A proxy object to intercept the default behavior of the gesture recognizer.
///
/// `UIGestureRecognizerDelegate` methods implementing by this object are called instead of the default delegate,
/// ``delegateOrigin``.
public weak var delegateProxy: UIGestureRecognizerDelegate? {
didSet {
self.delegate = floatingPanel // Update the cached IMP
self.delegate = floatingPanel?.panGestureDelegateRouter // Update the cached IMP
}
}
final class DelegateRouter: NSObject, UIGestureRecognizerDelegate {
fileprivate unowned let panGestureRecognizer: FloatingPanelPanGestureRecognizer
init(panGestureRecognizer: FloatingPanelPanGestureRecognizer) {
self.panGestureRecognizer = panGestureRecognizer
super.init()
}
override func responds(to aSelector: Selector!) -> Bool {
return panGestureRecognizer.delegateProxy?.responds(to: aSelector) == true
|| panGestureRecognizer.delegateOrigin.responds(to: aSelector)
}
override func forwardingTarget(for aSelector: Selector!) -> Any? {
if panGestureRecognizer.delegateProxy?.responds(to: aSelector) == true {
return panGestureRecognizer.delegateProxy
}
if panGestureRecognizer.delegateOrigin.responds(to: aSelector) {
return panGestureRecognizer.delegateOrigin
}
return nil
}
}
}
@@ -1203,7 +1287,7 @@ private class NumericSpringAnimator: NSObject {
if isRunning {
return false
}
log.debug("startAnimation --", displayLink)
os_log(msg, log: devLog, type: .debug, "startAnimation -- \(displayLink)")
isRunning = true
displayLink.add(to: RunLoop.main, forMode: .common)
return true
@@ -1215,7 +1299,7 @@ private class NumericSpringAnimator: NSObject {
if locked { lock.unlock() }
}
log.debug("stopAnimation --", displayLink)
os_log(msg, log: devLog, type: .debug, "stopAnimation -- \(displayLink)")
isRunning = false
displayLink.invalidate()
if withoutFinishing {
+2 -2
View File
@@ -10,7 +10,7 @@ extension CGFloat {
return (self * displayScale).rounded(.toNearestOrAwayFromZero) / displayScale
}
func isEqual(to: CGFloat, on displayScale: CGFloat) -> Bool {
return self.rounded(by: displayScale) == to.rounded(by: displayScale)
return rounded(by: displayScale) == to.rounded(by: displayScale)
}
}
@@ -157,7 +157,7 @@ extension UIGestureRecognizer.State: CustomDebugStringConvertible {
extension UIScrollView {
var isLocked: Bool {
return !showsVerticalScrollIndicator && isDirectionalLockEnabled
return !showsVerticalScrollIndicator && !bounces && isDirectionalLockEnabled
}
var fp_contentInset: UIEdgeInsets {
if #available(iOS 11.0, *) {
+1 -1
View File
@@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>2.6.2</string>
<string>2.7.0</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
</dict>
+9 -6
View File
@@ -1,6 +1,7 @@
// 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 {
@@ -265,12 +266,14 @@ class LayoutAdapter {
}
var offsetFromMostExpandedAnchor: CGFloat {
let offset: CGFloat
switch position {
case .top, .left:
return edgePosition(surfaceView.presentationFrame) - position(for: mostExpandedState)
offset = edgePosition(surfaceView.presentationFrame) - position(for: mostExpandedState)
case .bottom, .right:
return position(for: mostExpandedState) - edgePosition(surfaceView.presentationFrame)
offset = position(for: mostExpandedState) - edgePosition(surfaceView.presentationFrame)
}
return offset.rounded(by: surfaceView.fp_displayScale)
}
private var hiddenAnchor: FloatingPanelLayoutAnchoring {
@@ -710,7 +713,7 @@ class LayoutAdapter {
func updateInteractiveEdgeConstraint(diff: CGFloat, scrollingContent: Bool, allowsRubberBanding: (UIRectEdge) -> Bool) {
defer {
log.debug("update surface location = \(surfaceLocation)")
os_log(msg, log: devLog, type: .debug, "update surface location = \(surfaceLocation)")
}
let minConst: CGFloat = position(for: leastCoordinateState)
@@ -750,9 +753,9 @@ class LayoutAdapter {
defer {
if forceLayout {
layoutSurfaceIfNeeded()
log.debug("activateLayout for \(state) -- surface.presentation = \(self.surfaceView.presentationFrame) surface.frame = \(self.surfaceView.frame)")
os_log(msg, log: devLog, type: .debug, "activateLayout for \(state) -- surface.presentation = \(self.surfaceView.presentationFrame) surface.frame = \(self.surfaceView.frame)")
} else {
log.debug("activateLayout for \(state)")
os_log(msg, log: devLog, type: .debug, "activateLayout for \(state)")
}
}
@@ -787,7 +790,7 @@ class LayoutAdapter {
if let constraints = stateConstraints[state] {
NSLayoutConstraint.activate(constraints)
} else {
log.error("Couldn't find any constraints for \(state)")
os_log(msg, log: sysLog, type: .fault, "Error: can not find any constraints for \(state)")
}
}
}
-100
View File
@@ -1,100 +0,0 @@
// 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
@@ -0,0 +1,17 @@
// 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() {}
}
+2 -1
View File
@@ -1,6 +1,7 @@
// 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)
@@ -318,7 +319,7 @@ public class SurfaceView: UIView {
public override func layoutSubviews() {
super.layoutSubviews()
log.debug("surface view frame = \(frame)")
os_log(msg, log: devLog, type: .debug, "surface view frame = \(frame)")
containerView.backgroundColor = appearance.backgroundColor
+23 -12
View File
@@ -1,5 +1,6 @@
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
import OSLog
import XCTest
@testable import FloatingPanel
@@ -7,19 +8,25 @@ class ControllerTests: XCTestCase {
override func setUp() {}
override func tearDown() {}
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()
}
#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")
}
let myVC = MyZombieViewController(nibName: nil, bundle: nil)
myVC.loadViewIfNeeded()
wait(for: [exp], timeout: 10)
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)
}
#endif
func test_addPanel() {
let rootVC = UIViewController()
@@ -34,8 +41,13 @@ class ControllerTests: XCTestCase {
XCTAssertEqual(fpc.surfaceLocation.y, fpc.surfaceLocation(for: .tip).y)
}
@available(iOS 12.0, *)
func test_updateLayout_willTransition() {
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")
}
class MyDelegate: FloatingPanelControllerDelegate {
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout {
if newCollection.userInterfaceStyle == .dark {
@@ -318,7 +330,6 @@ 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)
+41 -33
View File
@@ -7,27 +7,32 @@ 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)
@@ -35,6 +40,7 @@ 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)
@@ -42,11 +48,13 @@ 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() {
@@ -284,7 +292,7 @@ class CoreTests: XCTestCase {
}
}
func test_targetPosition_1positions() {
func test_targetState_1positions() {
class FloatingPanelLayout1Positions: FloatingPanelLayout {
let initialState: FloatingPanelState = .full
let position: FloatingPanelPosition = .bottom
@@ -301,7 +309,7 @@ class CoreTests: XCTestCase {
let fullPos = fpc.surfaceLocation(for: .full).y
fpc.move(to: .full, animated: false)
assertTargetPosition(fpc.floatingPanel, with: [
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
@@ -312,7 +320,7 @@ class CoreTests: XCTestCase {
])
}
func test_targetPosition_2positions() {
func test_targetState_2positions() {
class FloatingPanelLayout2Positions: FloatingPanelLayout {
let initialState: FloatingPanelState = .half
let position: FloatingPanelPosition = .bottom
@@ -332,7 +340,7 @@ class CoreTests: XCTestCase {
let halfPos = fpc.surfaceLocation(for: .half).y
fpc.move(to: .full, animated: false)
assertTargetPosition(fpc.floatingPanel, with: [
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),
@@ -349,7 +357,7 @@ class CoreTests: XCTestCase {
(#line, halfPos + 10.0, CGPoint(x: 0.0, y: -1000.0), .full), // project to full
])
fpc.move(to: .half, animated: false)
assertTargetPosition(fpc.floatingPanel, with: [
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),
@@ -367,7 +375,7 @@ class CoreTests: XCTestCase {
])
}
func test_targetPosition_2positionsWithHidden() {
func test_targetState_2positionsWithHidden() {
class FloatingPanelLayout2Positions: FloatingPanelLayout {
let initialState: FloatingPanelState = .hidden
let position: FloatingPanelPosition = .bottom
@@ -387,7 +395,7 @@ class CoreTests: XCTestCase {
let hiddenPos = fpc.surfaceLocation(for: .hidden).y
fpc.move(to: .full, animated: false)
assertTargetPosition(fpc.floatingPanel, with: [
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),
@@ -404,7 +412,7 @@ class CoreTests: XCTestCase {
(#line, hiddenPos + 10.0, CGPoint(x: 0.0, y: -1000.0), .full), // project to full
])
fpc.move(to: .hidden, animated: false)
assertTargetPosition(fpc.floatingPanel, with: [
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),
@@ -422,7 +430,7 @@ class CoreTests: XCTestCase {
])
}
func test_targetPosition_3positionsFromFull() {
func test_targetState_3positionsFromFull() {
let delegate = FloatingPanelTestDelegate()
let fpc = FloatingPanelController(delegate: delegate)
fpc.layout = FloatingPanelLayout3Positions()
@@ -434,7 +442,7 @@ class CoreTests: XCTestCase {
let tipPos = fpc.surfaceLocation(for: .tip).y
// From .full
fpc.move(to: .full, animated: false)
assertTargetPosition(fpc.floatingPanel, with: [
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
@@ -466,7 +474,7 @@ class CoreTests: XCTestCase {
])
}
func test_targetPosition_3positionsFromFull_bottomEdge() {
func test_targetState_3positionsFromFull_bottomEdge() {
let delegate = FloatingPanelTestDelegate()
let fpc = FloatingPanelController(delegate: delegate)
fpc.layout = FloatingPanelLayout3PositionsBottomEdge()
@@ -478,7 +486,7 @@ class CoreTests: XCTestCase {
let tipPos = fpc.surfaceLocation(for: .tip).y
// From .full
fpc.move(to: .full, animated: false)
assertTargetPosition(fpc.floatingPanel, with: [
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
@@ -510,7 +518,7 @@ class CoreTests: XCTestCase {
])
}
func test_targetPosition_3positionsFromHalf() {
func test_targetState_3positionsFromHalf() {
let delegate = FloatingPanelTestDelegate()
let fpc = FloatingPanelController(delegate: delegate)
fpc.layout = FloatingPanelLayout3Positions()
@@ -522,7 +530,7 @@ class CoreTests: XCTestCase {
let tipPos = fpc.surfaceLocation(for: .tip).y
// From .half
fpc.move(to: .half, animated: false)
assertTargetPosition(fpc.floatingPanel, with: [
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
@@ -552,7 +560,7 @@ class CoreTests: XCTestCase {
])
}
func test_targetPosition_3positionsFromHalf_bottomEdge() {
func test_targetState_3positionsFromHalf_bottomEdge() {
let delegate = FloatingPanelTestDelegate()
let fpc = FloatingPanelController(delegate: delegate)
fpc.layout = FloatingPanelLayout3PositionsBottomEdge()
@@ -564,7 +572,7 @@ class CoreTests: XCTestCase {
let tipPos = fpc.surfaceLocation(for: .tip).y
// From .half
fpc.move(to: .half, animated: false)
assertTargetPosition(fpc.floatingPanel, with: [
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
@@ -594,7 +602,7 @@ class CoreTests: XCTestCase {
])
}
func test_targetPosition_3positionsFromTip() {
func test_targetState_3positionsFromTip() {
let delegate = FloatingPanelTestDelegate()
let fpc = FloatingPanelController(delegate: delegate)
fpc.layout = FloatingPanelLayout3Positions()
@@ -607,7 +615,7 @@ class CoreTests: XCTestCase {
// From .tip
fpc.move(to: .tip, animated: false)
assertTargetPosition(fpc.floatingPanel, with: [
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
@@ -637,7 +645,7 @@ class CoreTests: XCTestCase {
])
}
func test_targetPosition_3positionsFromTip_bottomEdge() {
func test_targetState_3positionsFromTip_bottomEdge() {
let delegate = FloatingPanelTestDelegate()
let fpc = FloatingPanelController(delegate: delegate)
fpc.layout = FloatingPanelLayout3PositionsBottomEdge()
@@ -650,7 +658,7 @@ class CoreTests: XCTestCase {
// From .tip
fpc.move(to: .tip, animated: false)
assertTargetPosition(fpc.floatingPanel, with: [
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
@@ -680,7 +688,7 @@ class CoreTests: XCTestCase {
])
}
func test_targetPosition_3positionsAllProjection() {
func test_targetState_3positionsAllProjection() {
let delegate = FloatingPanelTestDelegate()
let fpc = FloatingPanelController(delegate: delegate)
fpc.layout = FloatingPanelLayout3Positions()
@@ -694,7 +702,7 @@ class CoreTests: XCTestCase {
// From .full
fpc.move(to: .full, animated: false)
assertTargetPosition(fpc.floatingPanel, with: [
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),
@@ -707,7 +715,7 @@ class CoreTests: XCTestCase {
// From .half
fpc.move(to: .tip, animated: false)
assertTargetPosition(fpc.floatingPanel, with: [
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),
@@ -716,7 +724,7 @@ class CoreTests: XCTestCase {
// From .tip
fpc.move(to: .tip, animated: false)
assertTargetPosition(fpc.floatingPanel, with: [
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),
@@ -728,7 +736,7 @@ class CoreTests: XCTestCase {
])
}
func test_targetPosition_3positionsWithHidden() {
func test_targetState_3positionsWithHidden() {
class FloatingPanelLayout3PositionsWithHidden: FloatingPanelLayout {
let initialState: FloatingPanelState = .hidden
let position: FloatingPanelPosition = .bottom
@@ -746,11 +754,11 @@ class CoreTests: XCTestCase {
XCTAssertEqual(fpc.state, .hidden)
fpc.move(to: .full, animated: false)
assertTargetPosition(fpc.floatingPanel, with: [
assertTargetState(fpc.floatingPanel, with: [
(#line, fpc.surfaceView.frame.minY, CGPoint(x: 0.0, y: 1000.0), .half),
])
fpc.move(to: .half, animated: false)
assertTargetPosition(fpc.floatingPanel, with: [
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),
@@ -758,7 +766,7 @@ class CoreTests: XCTestCase {
])
}
func test_targetPosition_3positionsWithHiddenWithoutFull() {
func test_targetState_3positionsWithHiddenWithoutFull() {
class FloatingPanelLayout3Positions: FloatingPanelLayout {
let initialState: FloatingPanelState = .hidden
let position: FloatingPanelPosition = .bottom
@@ -782,7 +790,7 @@ class CoreTests: XCTestCase {
//let hiddenPos = fpc.surfaceLocation(for: .hidden)
fpc.move(to: .half, animated: false)
assertTargetPosition(fpc.floatingPanel, with: [
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
@@ -798,7 +806,7 @@ class CoreTests: XCTestCase {
(#line, tipPos - 10.0, CGPoint(x: 0.0, y: 10.0), .tip), // redirection
])
fpc.move(to: .tip, animated: false)
assertTargetPosition(fpc.floatingPanel, with: [
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),
@@ -827,9 +835,9 @@ private class FloatingPanelLayout3PositionsBottomEdge: FloatingPanelTop2BottomTe
}
private typealias TestParameter = (UInt, CGFloat, CGPoint, FloatingPanelState)
private func assertTargetPosition(_ floatingPanel: Core, with params: [TestParameter]) {
private func assertTargetState(_ floatingPanel: Core, with params: [TestParameter]) {
params.forEach { (line, pos, velocity, result) in
floatingPanel.surfaceView.frame.origin.y = pos
XCTAssertEqual(floatingPanel.targetPosition(from: pos, with: velocity.y), result, line: line)
XCTAssertEqual(floatingPanel.targetState(from: pos, with: velocity.y), result, line: line)
}
}
+139
View File
@@ -0,0 +1,139 @@
// 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)
}
}
-1
View File
@@ -464,7 +464,6 @@ class LayoutTests: XCTestCase {
if #available(iOS 11, *) {
XCTAssertEqual(c.secondAnchor, prop.result.secondAnchor, line: UInt(prop.result.0))
}
print(c)
}
}
func test_layoutAnchor_bottomPosition() {
+1 -1
View File
@@ -73,7 +73,7 @@ class FloatingPanelTop2BottomTestLayout: FloatingPanelLayout {
}
class FloatingPanelProjectableBehavior: FloatingPanelBehavior {
func shouldProjectMomentum(_ fpc: FloatingPanelController, to proposedTargetPosition: FloatingPanelState) -> Bool {
func shouldProjectMomentum(_ fpc: FloatingPanelController, to proposedState: FloatingPanelState) -> Bool {
return true
}
}