Compare commits

...

29 Commits

Author SHA1 Message Date
Shin Yamamoto a225bf2cf1 Release v1.6.2 2019-07-10 18:55:09 +09:00
Shin Yamamoto 11a16092a7 Merge pull request #231 from SCENEE/prevent-found-nil-error
Prevent 'unexpectedly found nil' fatal error
2019-07-09 21:53:40 +09:00
Shin Yamamoto b9b7f940b9 Prevent 'unexpectedly found nil' fatal error
Use FloatingPanel.viewcontroller as an optional value instead of
an implicitly unwrapping optional one.
2019-07-09 20:06:16 +09:00
Shin Yamamoto 206475e6ab Merge pull request #232 from SCENEE/refactor-layout-adapter
Refactor layout adapter
2019-07-03 14:24:47 +09:00
Shin Yamamoto a4a68e5b39 Add test_surfaceView_constraintsUpdate() 2019-07-03 11:46:45 +09:00
Shin Yamamoto de7ab0e0cb Rename FloatingPanelViewTests to FloatingPanelSurfaceViewTests 2019-07-03 11:46:45 +09:00
Shin Yamamoto 5f7b5ce81c Add FloatingPanelLayoutTests & Utils 2019-07-03 11:46:45 +09:00
Shin Yamamoto 36d7ea5100 Improve testing speed 2019-07-03 11:34:21 +09:00
Shin Yamamoto 33f8cf3802 Modify FloatingPanel.distance(to:) 2019-07-03 11:34:21 +09:00
Shin Yamamoto f6da876fdf Add botomMostState prop 2019-07-03 11:34:21 +09:00
Shin Yamamoto 96c5dc7b74 Add FloatingPanelLayoutTests 2019-07-03 11:34:02 +09:00
Shin Yamamoto a37931b62d Merge pull request #230 from SCENEE/fix-scrollindicator
Fix the scroll indicator lock on a contentVC reset
2019-07-03 09:55:32 +09:00
Shin Yamamoto 5c848d9bf5 Fix the scroll indicator lock on a contentVC reset
The locking logic couldn't take care of the case where a content view
controller of a FloatingPanelController object is replaced.
2019-07-02 19:12:58 +09:00
Shin Yamamoto 265b805fa9 No more need FloatingPanel to conform UIScrollViewDelegate 2019-07-02 14:21:10 +09:00
Shin Yamamoto c4dfe33a5e Merge pull request #229 from SCENEE/release-1.6.1
Release v1.6.1
2019-06-29 09:31:17 +09:00
Shin Yamamoto 999eeb47ba Release v1.6.1 2019-06-29 08:33:24 +09:00
Shin Yamamoto a5bf02cfec Merge pull request #228 from SCENEE/fix-unexpected-layout-update
Fix an unexpected layout update on iOS13
2019-06-29 08:32:48 +09:00
Shin Yamamoto c10186e50a Prevent an unexpected layout update on iOS13
On iOS13, UITraitCollection.userInterfaceStyle can be changed
from .light to .dark when an app transitions to the background.
2019-06-29 07:41:52 +09:00
Shin Yamamoto 7a1cbf99d4 Rename setUpLayout to activateLayout 2019-06-28 20:23:10 +09:00
Shin Yamamoto c9c4000536 Merge pull request #225 from SCENEE/fix-seamless-scrolling
Remove workaround for tableView(_:didSelectRowAt:) issue
2019-06-19 10:34:57 +09:00
Shin Yamamoto 656bbc1b1c Remove workaround for tableView(_:didSelectRowAt:) issue
The workaround was added to avoid `tableView(_:didSelectRowAt:)` not
being called on first tap after the moving animation. However, it
doesn't only resolved the issue, but also has side effects.

For example, it affects the seamless scrolling in dragging up a panel from
half to full after bouncing it in the bottom buffer. The problem occurs
on "Tab2" sample of "Show Tab Bar".

Moreover the UITableView issue seems to be relieved on iOS 13.

Therefore I remove the workaround.
2019-06-19 09:39:56 +09:00
Shin Yamamoto 3815a08af5 Merge pull request #221 from SCENEE/fix-closing-panel-in-bounce
Fix closing panel during internal scroll view bounce
2019-06-17 08:04:56 +09:00
Shin Yamamoto 404fdb6496 Fix flushing a scroll indicator
1. A scroll indicator flushed at the first time when a tacking scroll view's
offset is zero and a user swipes down a panel at the top most position
2. A scroll indicator flushed at the first time when a tacking scroll view's
offset is zero and a user swipes up a panel at non top most position
2019-06-16 21:33:37 +09:00
Shin Yamamoto 573f355c15 Remove unnecessary code
There is not reason why the code is needed because the scroll tracking
logic is working well without it.
2019-06-16 21:32:35 +09:00
Shin Yamamoto bd0c891795 Fix closing panel during internal scroll view bounce
Now the scroll tracking is working well without the scroll offset handling
at the top most position in the callback of a scroll pan gesture.
2019-06-14 14:00:55 +09:00
Robbie Trencheny f4857a3da9 Add Swift Package Manager support (#219)
* Add Package.swift
2019-06-13 07:59:12 +09:00
Shin Yamamoto e074c3caf1 Merge pull request #220 from SCENEE/fix-removal-crash
Fix the crash while closeing via dragging
2019-06-12 08:56:31 +09:00
Shin Yamamoto 0f4c7503b1 Fix the crash while closeing via dragging
While closing the viewcontroller via dragging, calling floatPanelController's hide() will cause a crash.
2019-06-11 08:26:16 +09:00
Shin Yamamoto 2cb142a31f Merge pull request #213 from SCENEE/release-1.6.0
Release v1.6.0
2019-06-03 22:12:36 +09:00
14 changed files with 335 additions and 125 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
Pod::Spec.new do |s|
s.name = "FloatingPanel"
s.version = "1.6.0"
s.version = "1.6.2"
s.summary = "FloatingPanel is a clean and easy-to-use UI component of a floating panel interface."
s.description = <<-DESC
FloatingPanel is a clean and easy-to-use UI component for a new interface introduced in Apple Maps, Shortcuts and Stocks app.
@@ -7,6 +7,8 @@
objects = {
/* Begin PBXBuildFile section */
542753C622C49A6E00D17955 /* FloatingPanelLayoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 542753C522C49A6E00D17955 /* FloatingPanelLayoutTests.swift */; };
542753C822C49A8F00D17955 /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 542753C722C49A8F00D17955 /* Utils.swift */; };
54352E9621A51A2500CBCA08 /* FloatingPanelTransitioning.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54352E9521A51A2500CBCA08 /* FloatingPanelTransitioning.swift */; };
54352E9821A521CA00CBCA08 /* FloatingPanelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54352E9721A521CA00CBCA08 /* FloatingPanelView.swift */; };
5450EEE421646DF500135936 /* FloatingPanelBehavior.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5450EEE321646DF500135936 /* FloatingPanelBehavior.swift */; };
@@ -18,7 +20,7 @@
545DBA2B2152383100CA77B8 /* GrabberHandleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DBA2A2152383100CA77B8 /* GrabberHandleView.swift */; };
54A6B6B122968B530077F348 /* FloatingPanelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54A6B6B022968B530077F348 /* FloatingPanelTests.swift */; };
54A6B6B622968F710077F348 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 54A6B6B522968F710077F348 /* LaunchScreen.storyboard */; };
54A6B6B82296A8520077F348 /* FloatingPanelViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54A6B6B72296A8520077F348 /* FloatingPanelViewTests.swift */; };
54A6B6B82296A8520077F348 /* FloatingPanelSurfaceViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54A6B6B72296A8520077F348 /* FloatingPanelSurfaceViewTests.swift */; };
54ABD7AF216CCFF7002E6C13 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54ABD7AE216CCFF7002E6C13 /* Logger.swift */; };
54CDC5D3215B6D5A007D205C /* FloatingPanelSurfaceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CDC5D2215B6D5A007D205C /* FloatingPanelSurfaceView.swift */; };
54CDC5D5215B6D8D007D205C /* FloatingPanelBackdropView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CDC5D4215B6D8D007D205C /* FloatingPanelBackdropView.swift */; };
@@ -45,6 +47,8 @@
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
542753C522C49A6E00D17955 /* FloatingPanelLayoutTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelLayoutTests.swift; sourceTree = "<group>"; };
542753C722C49A8F00D17955 /* Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utils.swift; sourceTree = "<group>"; };
54352E9521A51A2500CBCA08 /* FloatingPanelTransitioning.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelTransitioning.swift; sourceTree = "<group>"; };
54352E9721A521CA00CBCA08 /* FloatingPanelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelView.swift; sourceTree = "<group>"; };
5450EEE321646DF500135936 /* FloatingPanelBehavior.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelBehavior.swift; sourceTree = "<group>"; };
@@ -59,7 +63,7 @@
545DBA2A2152383100CA77B8 /* GrabberHandleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GrabberHandleView.swift; sourceTree = "<group>"; };
54A6B6B022968B530077F348 /* FloatingPanelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelTests.swift; sourceTree = "<group>"; };
54A6B6B522968F710077F348 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = "<group>"; };
54A6B6B72296A8520077F348 /* FloatingPanelViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelViewTests.swift; sourceTree = "<group>"; };
54A6B6B72296A8520077F348 /* FloatingPanelSurfaceViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelSurfaceViewTests.swift; sourceTree = "<group>"; };
54ABD7AE216CCFF7002E6C13 /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; };
54CDC5D2215B6D5A007D205C /* FloatingPanelSurfaceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelSurfaceView.swift; sourceTree = "<group>"; };
54CDC5D4215B6D8D007D205C /* FloatingPanelBackdropView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelBackdropView.swift; sourceTree = "<group>"; };
@@ -140,8 +144,10 @@
isa = PBXGroup;
children = (
54A6B6B022968B530077F348 /* FloatingPanelTests.swift */,
54A6B6B72296A8520077F348 /* FloatingPanelViewTests.swift */,
542753C522C49A6E00D17955 /* FloatingPanelLayoutTests.swift */,
54A6B6B72296A8520077F348 /* FloatingPanelSurfaceViewTests.swift */,
545DB9CF2151169500CA77B8 /* FloatingPanelControllerTests.swift */,
542753C722C49A8F00D17955 /* Utils.swift */,
545DB9D12151169500CA77B8 /* Info.plist */,
);
path = Tests;
@@ -309,6 +315,7 @@
545DBA2B2152383100CA77B8 /* GrabberHandleView.swift in Sources */,
54352E9621A51A2500CBCA08 /* FloatingPanelTransitioning.swift in Sources */,
545DB9DE215118C800CA77B8 /* UIExtensions.swift in Sources */,
542753C822C49A8F00D17955 /* Utils.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -318,7 +325,8 @@
files = (
54A6B6B122968B530077F348 /* FloatingPanelTests.swift in Sources */,
545DB9D02151169500CA77B8 /* FloatingPanelControllerTests.swift in Sources */,
54A6B6B82296A8520077F348 /* FloatingPanelViewTests.swift in Sources */,
542753C622C49A6E00D17955 /* FloatingPanelLayoutTests.swift in Sources */,
54A6B6B82296A8520077F348 /* FloatingPanelSurfaceViewTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -29,7 +29,9 @@
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
skipped = "NO"
parallelizable = "YES"
testExecutionOrdering = "random">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "545DB9C92151169500CA77B8"
+88 -84
View File
@@ -8,9 +8,9 @@ import UIKit.UIGestureRecognizerSubclass // For Xcode 9.4.1
///
/// FloatingPanel presentation model
///
class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate {
class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
// MUST be a weak reference to prevent UI freeze on the presentation modally
weak var viewcontroller: FloatingPanelController!
weak var viewcontroller: FloatingPanelController?
let surfaceView: FloatingPanelSurfaceView
let backdropView: FloatingPanelBackdropView
@@ -25,7 +25,11 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
}
private(set) var state: FloatingPanelPosition = .hidden {
didSet { viewcontroller.delegate?.floatingPanelDidChangePosition(viewcontroller) }
didSet {
if let vc = viewcontroller {
vc.delegate?.floatingPanelDidChangePosition(vc)
}
}
}
private var isBottomState: Bool {
@@ -36,15 +40,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
let panGestureRecognizer: FloatingPanelPanGestureRecognizer
var isRemovalInteractionEnabled: Bool = false
fileprivate var animator: UIViewPropertyAnimator? {
didSet {
// This intends to avoid `tableView(_:didSelectRowAt:)` not being
// called on first tap after the moving animation, but it doesn't
// seem to be enough. The same issue happens on Apple Maps so it
// might be an issue in `UITableView`.
scrollView?.isUserInteractionEnabled = (animator == nil)
}
}
fileprivate var animator: UIViewPropertyAnimator?
private var initialFrame: CGRect = .zero
private var initialTranslationY: CGFloat = 0
@@ -60,8 +56,6 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
private var scrollBouncable = false
private var scrollIndictorVisible = false
private var isScrollLocked: Bool = false
// MARK: - Interface
init(_ vc: FloatingPanelController, layout: FloatingPanelLayout, behavior: FloatingPanelBehavior) {
@@ -99,6 +93,10 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
}
private func move(from: FloatingPanelPosition, to: FloatingPanelPosition, animated: Bool, completion: (() -> Void)? = nil) {
guard let vc = viewcontroller else {
completion?()
return
}
if state != layoutAdapter.topMostState {
lockScrollView()
}
@@ -108,11 +106,11 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
let animator: UIViewPropertyAnimator
switch (from, to) {
case (.hidden, let to):
animator = behavior.addAnimator(self.viewcontroller, to: to)
animator = behavior.addAnimator(vc, to: to)
case (let from, .hidden):
animator = behavior.removeAnimator(self.viewcontroller, from: from)
animator = behavior.removeAnimator(vc, from: from)
case (let from, let to):
animator = behavior.moveAnimator(self.viewcontroller, from: from, to: to)
animator = behavior.moveAnimator(vc, from: from, to: to)
}
animator.addAnimations { [weak self] in
@@ -124,7 +122,11 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
animator.addCompletion { [weak self] _ in
guard let `self` = self else { return }
self.animator = nil
self.unlockScrollView()
if self.state == self.layoutAdapter.topMostState {
self.unlockScrollView()
} else {
self.lockScrollView()
}
completion?()
}
self.animator = animator
@@ -132,7 +134,11 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
} else {
self.state = to
self.updateLayout(to: to)
self.unlockScrollView()
if self.state == self.layoutAdapter.topMostState {
self.unlockScrollView()
} else {
self.lockScrollView()
}
completion?()
}
}
@@ -169,7 +175,8 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
/* log.debug("shouldRecognizeSimultaneouslyWith", otherGestureRecognizer) */
if viewcontroller.delegate?.floatingPanel(viewcontroller, shouldRecognizeSimultaneouslyWith: otherGestureRecognizer) ?? false {
if let vc = viewcontroller,
vc.delegate?.floatingPanel(vc, shouldRecognizeSimultaneouslyWith: otherGestureRecognizer) ?? false {
return true
}
@@ -216,11 +223,11 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
}
}
if viewcontroller.delegate?.floatingPanel(viewcontroller, shouldRecognizeSimultaneouslyWith: otherGestureRecognizer) ?? false {
if let vc = viewcontroller,
vc.delegate?.floatingPanel(vc, shouldRecognizeSimultaneouslyWith: otherGestureRecognizer) ?? false {
return false
}
switch otherGestureRecognizer {
case is UIPanGestureRecognizer,
is UISwipeGestureRecognizer,
@@ -271,13 +278,6 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
if grabberAreaFrame.contains(location) {
// Preserve the current content offset in moving from full.
scrollView.setContentOffset(initialScrollOffset, animated: false)
} else {
let offset = scrollView.contentOffset.y - scrollView.contentOffsetZero.y
if offset < 0 {
fitToBounds(scrollView: scrollView)
let translation = panGesture.translation(in: panGestureRecognizer.view!.superview)
startInteraction(with: translation, at: location)
}
}
}
} else {
@@ -289,15 +289,17 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
lockScrollView()
}
} else {
let offset = scrollView.contentOffset.y - scrollView.contentOffsetZero.y
// Always show a scroll indicator at the top.
if interactionInProgress {
unlockScrollView()
if offset > 0 {
unlockScrollView()
}
} else {
let offset = scrollView.contentOffset.y - scrollView.contentOffsetZero.y
if state == layoutAdapter.topMostState, offset < 0, velocity.y > 0 {
fitToBounds(scrollView: scrollView)
let translation = panGesture.translation(in: panGestureRecognizer.view!.superview)
startInteraction(with: translation, at: location)
// Hide a scroll indicator just before starting an interaction by swiping a panel down.
if state == layoutAdapter.topMostState,
offset < 0, velocity.y > 0 {
lockScrollView()
}
}
}
@@ -323,7 +325,8 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
}
if interactionInProgress == false,
viewcontroller.delegate?.floatingPanelShouldBeginDragging(viewcontroller) == false {
let vc = viewcontroller,
vc.delegate?.floatingPanelShouldBeginDragging(vc) == false {
return
}
@@ -436,7 +439,9 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
let didMove = (pre != surfaceView.frame.minY)
guard didMove else { return }
viewcontroller.delegate?.floatingPanelDidMove(viewcontroller)
if let vc = viewcontroller {
vc.delegate?.floatingPanelDidMove(vc)
}
}
private func allowsTopBuffer(for translationY: CGFloat) -> Bool {
@@ -454,11 +459,12 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
// Prevent stretching a view having a constraint to SafeArea.bottom in an overflow
// from the full position because SafeArea is global in a screen.
private func preserveContentVCLayoutIfNeeded() {
guard let vc = viewcontroller else { return }
// Must include topY
if (surfaceView.frame.minY <= layoutAdapter.topY) {
if !disabledBottomAutoLayout {
viewcontroller.contentViewController?.view?.constraints.forEach({ (const) in
switch viewcontroller.contentViewController?.layoutGuide.bottomAnchor {
vc.contentViewController?.view?.constraints.forEach({ (const) in
switch vc.contentViewController?.layoutGuide.bottomAnchor {
case const.firstAnchor:
(const.secondItem as? UIView)?.disableAutoLayout()
const.isActive = false
@@ -473,8 +479,8 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
disabledBottomAutoLayout = true
} else {
if disabledBottomAutoLayout {
viewcontroller.contentViewController?.view?.constraints.forEach({ (const) in
switch viewcontroller.contentViewController?.layoutGuide.bottomAnchor {
vc.contentViewController?.view?.constraints.forEach({ (const) in
switch vc.contentViewController?.layoutGuide.bottomAnchor {
case const.firstAnchor:
(const.secondItem as? UIView)?.enableAutoLayout()
const.isActive = true
@@ -515,21 +521,18 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
let velocityVector = (distance != 0) ? CGVector(dx: 0,
dy: min(abs(velocity.y)/distance, behavior.removalVelocity)) : .zero
if shouldStartRemovalAnimation(with: velocityVector) {
viewcontroller.delegate?.floatingPanelDidEndDraggingToRemove(viewcontroller, withVelocity: velocity)
self.startRemovalAnimation(with: velocityVector) { [weak self] in
guard let `self` = self else { return }
self.viewcontroller.dismiss(animated: false, completion: { [weak self] in
guard let `self` = self else { return }
self.viewcontroller.delegate?.floatingPanelDidEndRemove(self.viewcontroller)
})
if shouldStartRemovalAnimation(with: velocityVector), let vc = viewcontroller {
vc.delegate?.floatingPanelDidEndDraggingToRemove(vc, withVelocity: velocity)
startRemovalAnimation(vc, with: velocityVector) { [weak self] in
self?.finishRemovalAnimation()
}
return
}
}
viewcontroller.delegate?.floatingPanelDidEndDragging(viewcontroller, withVelocity: velocity, targetPosition: targetPosition)
if let vc = viewcontroller {
vc.delegate?.floatingPanelDidEndDragging(vc, withVelocity: velocity, targetPosition: targetPosition)
}
// Workaround: Disable a tracking scroll to prevent bouncing a scroll content in a panel animating
let isScrollEnabled = scrollView?.isScrollEnabled
@@ -562,8 +565,8 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
return true
}
private func startRemovalAnimation(with velocityVector: CGVector, completion: (() -> Void)?) {
let animator = self.behavior.removalInteractionAnimator(self.viewcontroller, with: velocityVector)
private func startRemovalAnimation(_ vc: FloatingPanelController, with velocityVector: CGVector, completion: (() -> Void)?) {
let animator = behavior.removalInteractionAnimator(vc, with: velocityVector)
animator.addAnimations { [weak self] in
self?.updateLayout(to: .hidden)
@@ -576,6 +579,13 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
animator.startAnimation()
}
private func finishRemovalAnimation() {
viewcontroller?.dismiss(animated: false) { [weak self] in
guard let vc = self?.viewcontroller else { return }
vc.delegate?.floatingPanelDidEndRemove(vc)
}
}
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 = \(translation.y), location = \(location.y)")
@@ -586,6 +596,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
if grabberAreaFrame.contains(location) {
initialScrollOffset = scrollView.contentOffset
} else {
fitToBounds(scrollView: scrollView)
settle(scrollView: scrollView)
initialScrollOffset = scrollView.contentOffsetZero
}
@@ -594,7 +605,9 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
initialTranslationY = translation.y
viewcontroller.delegate?.floatingPanelWillBeginDragging(viewcontroller)
if let vc = viewcontroller {
vc.delegate?.floatingPanelWillBeginDragging(vc)
}
layoutAdapter.startInteraction(at: state)
@@ -626,12 +639,14 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
private func startAnimation(to targetPosition: FloatingPanelPosition, at distance: CGFloat, with velocity: CGPoint) {
log.debug("startAnimation to \(targetPosition) -- distance = \(distance), velocity = \(velocity.y)")
guard let vc = viewcontroller else { return }
isDecelerating = true
viewcontroller.delegate?.floatingPanelWillBeginDecelerating(viewcontroller)
vc.delegate?.floatingPanelWillBeginDecelerating(vc)
let velocityVector = (distance != 0) ? CGVector(dx: 0, dy: min(abs(velocity.y)/distance, 30.0)) : .zero
let animator = behavior.interactionAnimator(self.viewcontroller, to: targetPosition, with: velocityVector)
let animator = behavior.interactionAnimator(vc, to: targetPosition, with: velocityVector)
animator.addAnimations { [weak self] in
guard let `self` = self else { return }
self.state = targetPosition
@@ -651,7 +666,9 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
self.isDecelerating = false
self.animator = nil
self.viewcontroller.delegate?.floatingPanelDidEndDecelerating(self.viewcontroller)
if let vc = viewcontroller {
vc.delegate?.floatingPanelDidEndDecelerating(vc)
}
if let scrollView = scrollView {
log.debug("finishAnimation -- scroll offset = \(scrollView.contentOffset)")
@@ -665,21 +682,9 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
}
private func distance(to targetPosition: FloatingPanelPosition) -> CGFloat {
let topY = layoutAdapter.topY
let middleY = layoutAdapter.middleY
let bottomY = layoutAdapter.bottomY
let currentY = surfaceView.frame.minY
switch targetPosition {
case .full:
return CGFloat(abs(currentY - topY))
case .half:
return CGFloat(abs(currentY - middleY))
case .tip:
return CGFloat(abs(currentY - bottomY))
case .hidden:
fatalError("Now .hidden must not be used for a user interaction")
}
let targetY = layoutAdapter.positionY(for: targetPosition)
return CGFloat(abs(currentY - targetY))
}
private func directionalPosition(at currentY: CGFloat, with translation: CGPoint) -> FloatingPanelPosition {
@@ -720,6 +725,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
}
private func targetPosition(with velocity: CGPoint) -> (FloatingPanelPosition) {
guard let vc = viewcontroller else { return state }
let currentY = surfaceView.frame.minY
let supportedPositions = layoutAdapter.supportedPositions
@@ -762,7 +768,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
fatalError("Now .hidden must not be used for a user interaction")
}
let redirectionalProgress = max(min(behavior.redirectionalProgress(viewcontroller, from: state, to: nextState), 1.0), 0.0)
let redirectionalProgress = max(min(behavior.redirectionalProgress(vc, from: state, to: nextState), 1.0), 0.0)
let th1: CGFloat
let th2: CGFloat
@@ -775,7 +781,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
th2 = bottomY - (bottomY - middleY) * redirectionalProgress
}
let decelerationRate = behavior.momentumProjectionRate(viewcontroller)
let decelerationRate = behavior.momentumProjectionRate(vc)
let baseY = abs(bottomY - topY)
let vecY = velocity.y / baseY
@@ -785,7 +791,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
case ..<th1:
switch pY {
case bottomY...:
return behavior.shouldProjectMomentum(viewcontroller, for: .tip) ? .tip : .half
return behavior.shouldProjectMomentum(vc, for: .tip) ? .tip : .half
case middleY...:
return .half
case topY...:
@@ -796,7 +802,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
case ...middleY:
switch pY {
case bottomY...:
return behavior.shouldProjectMomentum(viewcontroller, for: .tip) ? .tip : .half
return behavior.shouldProjectMomentum(vc, for: .tip) ? .tip : .half
case middleY...:
return .half
case topY...:
@@ -813,7 +819,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
case topY...:
return .half
default:
return behavior.shouldProjectMomentum(viewcontroller, for: .full) ? .full : .half
return behavior.shouldProjectMomentum(vc, for: .full) ? .full : .half
}
default:
switch pY {
@@ -824,13 +830,14 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
case topY...:
return .half
default:
return behavior.shouldProjectMomentum(viewcontroller, for: .full) ? .full : .half
return behavior.shouldProjectMomentum(vc, for: .full) ? .full : .half
}
}
}
}
private func targetPosition(from positions: [FloatingPanelPosition], at currentY: CGFloat, velocity: CGPoint) -> FloatingPanelPosition {
guard let vc = viewcontroller else { return state }
assert(positions.count == 2)
let top = positions[0]
@@ -840,11 +847,11 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
let bottomY = layoutAdapter.positionY(for: bottom)
let target = top == state ? bottom : top
let redirectionalProgress = max(min(behavior.redirectionalProgress(viewcontroller, from: state, to: target), 1.0), 0.0)
let redirectionalProgress = max(min(behavior.redirectionalProgress(vc, from: state, to: target), 1.0), 0.0)
let th = topY + (bottomY - topY) * redirectionalProgress
let decelerationRate = behavior.momentumProjectionRate(viewcontroller)
let decelerationRate = behavior.momentumProjectionRate(vc)
let pY = project(initialVelocity: velocity.y, decelerationRate: decelerationRate) + currentY
switch currentY {
@@ -868,11 +875,10 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
private func lockScrollView() {
guard let scrollView = scrollView else { return }
if isScrollLocked {
if scrollView.isLocked {
log.debug("Already scroll locked.")
return
}
isScrollLocked = true
scrollBouncable = scrollView.bounces
scrollIndictorVisible = scrollView.showsVerticalScrollIndicator
@@ -883,9 +889,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
}
private func unlockScrollView() {
guard let scrollView = scrollView, isScrollLocked else { return }
isScrollLocked = false
guard let scrollView = scrollView, scrollView.isLocked else { return }
scrollView.isDirectionalLockEnabled = false
scrollView.bounces = scrollBouncable
+22 -14
View File
@@ -68,6 +68,10 @@ public enum FloatingPanelPosition: Int {
case half
case tip
case hidden
static var allCases: [FloatingPanelPosition] {
return [.full, .half, .tip, .hidden]
}
}
///
@@ -145,7 +149,7 @@ open class FloatingPanelController: UIViewController, UIScrollViewDelegate, UIGe
}
private var _contentViewController: UIViewController?
private var floatingPanel: FloatingPanel!
private(set) var floatingPanel: FloatingPanel!
private var preSafeAreaInsets: UIEdgeInsets = .zero // Capture the latest one
private var safeAreaInsetsObservation: NSKeyValueObservation?
private let modalTransition = FloatingPanelModalTransition()
@@ -196,7 +200,7 @@ open class FloatingPanelController: UIViewController, UIScrollViewDelegate, UIGe
self.view = view as UIView
}
open override func viewDidLayoutSubviews() {
open override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if #available(iOS 11.0, *) {}
else {
@@ -207,7 +211,7 @@ open class FloatingPanelController: UIViewController, UIScrollViewDelegate, UIGe
}
}
open override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
open override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
if view.translatesAutoresizingMaskIntoConstraints {
@@ -216,14 +220,9 @@ open class FloatingPanelController: UIViewController, UIScrollViewDelegate, UIGe
}
}
open override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) {
open override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) {
super.willTransition(to: newCollection, with: coordinator)
// Change layout for a new trait collection
reloadLayout(for: newCollection)
setUpLayout()
floatingPanel.behavior = fetchBehavior(for: newCollection)
self.prepare(for: newCollection)
}
open override func viewWillDisappear(_ animated: Bool) {
@@ -231,6 +230,15 @@ open class FloatingPanelController: UIViewController, UIScrollViewDelegate, UIGe
safeAreaInsetsObservation = nil
}
// MARK:- Internals
func prepare(for newCollection: UITraitCollection) {
guard newCollection.shouldUpdateLayout(from: traitCollection) else { return }
// Change a layout & behavior for a new trait collection
reloadLayout(for: newCollection)
activateLayout()
floatingPanel.behavior = fetchBehavior(for: newCollection)
}
// MARK:- Privates
private func fetchLayout(for traitCollection: UITraitCollection) -> FloatingPanelLayout {
@@ -257,7 +265,7 @@ open class FloatingPanelController: UIViewController, UIScrollViewDelegate, UIGe
// Prevent an infinite loop on iOS 10: setUpLayout() -> viewDidLayoutSubviews() -> setUpLayout()
preSafeAreaInsets = safeAreaInsets
setUpLayout()
activateLayout()
switch contentInsetAdjustmentBehavior {
case .always:
@@ -282,7 +290,7 @@ open class FloatingPanelController: UIViewController, UIScrollViewDelegate, UIGe
}
}
private func setUpLayout() {
private func activateLayout() {
// preserve the current content offset
let contentOffset = scrollView?.contentOffset
@@ -298,7 +306,7 @@ open class FloatingPanelController: UIViewController, UIScrollViewDelegate, UIGe
public func show(animated: Bool = false, completion: (() -> Void)? = nil) {
// Must apply the current layout here
reloadLayout(for: traitCollection)
setUpLayout()
activateLayout()
if #available(iOS 11.0, *) {
// Must track the safeAreaInsets of `self.view` to update the layout.
@@ -513,7 +521,7 @@ open class FloatingPanelController: UIViewController, UIScrollViewDelegate, UIGe
/// animation block.
public func updateLayout() {
reloadLayout(for: traitCollection)
setUpLayout()
activateLayout()
}
/// Returns the y-coordinate of the point at the origin of the surface view.
+5 -7
View File
@@ -181,13 +181,11 @@ class FloatingPanelLayoutAdapter {
}
var topMostState: FloatingPanelPosition {
if supportedPositions.contains(.full) {
return .full
}
if supportedPositions.contains(.half) {
return .half
}
return .tip
return supportedPositions.sorted(by: { $0.rawValue < $1.rawValue }).first ?? .hidden
}
var bottomMostState: FloatingPanelPosition {
return supportedPositions.sorted(by: { $0.rawValue < $1.rawValue }).last ?? .hidden
}
var topY: CGFloat {
+1 -1
View File
@@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.6.0</string>
<string>1.6.2</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
</dict>
+12
View File
@@ -107,6 +107,9 @@ extension UIScrollView {
var contentOffsetZero: CGPoint {
return CGPoint(x: 0.0, y: 0.0 - contentInset.top)
}
var isLocked: Bool {
return !showsVerticalScrollIndicator && !bounces && isDirectionalLockEnabled
}
}
extension UISpringTimingParameters {
@@ -124,3 +127,12 @@ extension CGPoint {
y: CGFloat.nan)
}
}
extension UITraitCollection {
func shouldUpdateLayout(from previous: UITraitCollection) -> Bool {
return previous.horizontalSizeClass != horizontalSizeClass
|| previous.verticalSizeClass != verticalSizeClass
|| previous.preferredContentSizeCategory != preferredContentSizeCategory
|| previous.layoutDirection != layoutDirection
}
}
@@ -28,24 +28,33 @@ class FloatingPanelControllerTests: XCTestCase {
func test_addPanel() {
guard let rootVC = UIApplication.shared.keyWindow?.rootViewController else { fatalError() }
let fpc = FloatingPanelController()
fpc.addPanel(toParent: rootVC)
waitRunLoop(secs: 1.0)
XCTAssert(fpc.surfaceView.frame.minY == (fpc.view.bounds.height - fpc.layoutInsets.bottom) - fpc.layout.insetFor(position: .half)!)
fpc.move(to: .tip, animated: true)
waitRunLoop(secs: 1.0)
fpc.move(to: .tip, animated: false)
XCTAssert(fpc.surfaceView.frame.minY == (fpc.view.bounds.height - fpc.layoutInsets.bottom) - fpc.layout.insetFor(position: .tip)!)
}
@available(iOS 12.0, *)
func test_updateLayout_willTransition() {
class MyDelegate: FloatingPanelControllerDelegate {
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? {
if newCollection.userInterfaceStyle == .dark {
XCTFail()
}
return nil
}
}
let myDelegate = MyDelegate()
let fpc = FloatingPanelController(delegate: myDelegate)
let traitCollection = UITraitCollection(traitsFrom: [fpc.traitCollection,
UITraitCollection(userInterfaceStyle: .dark)])
XCTAssertEqual(traitCollection.userInterfaceStyle, .dark)
fpc.prepare(for: traitCollection)
}
}
func waitRunLoop(secs: TimeInterval = 0) {
RunLoop.main.run(until: Date(timeIntervalSinceNow: secs))
}
class MyZombieViewController: UIViewController, FloatingPanelLayout, FloatingPanelBehavior, FloatingPanelControllerDelegate {
private class MyZombieViewController: UIViewController, FloatingPanelLayout, FloatingPanelBehavior, FloatingPanelControllerDelegate {
var fpc: FloatingPanelController?
override func viewDidLoad() {
fpc = FloatingPanelController(delegate: self)
@@ -0,0 +1,41 @@
//
// Created by Shin Yamamoto on 2019/06/27.
// Copyright © 2019 scenee. All rights reserved.
//
import XCTest
@testable import FloatingPanel
class FloatingPanelLayoutTests: XCTestCase {
override func setUp() {}
override func tearDown() {}
func test_layoutAdapter_topAndBottomMostState() {
let fpc = FloatingPanelController(delegate: nil)
fpc.loadViewIfNeeded()
fpc.view.frame = CGRect(x: 0, y: 0, width: 375, height: 667)
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.topMostState, .full)
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.bottomMostState, .tip)
class FloatingPanelLayoutWithHidden: FloatingPanelLayout {
func insetFor(position: FloatingPanelPosition) -> CGFloat? { return nil }
let initialPosition: FloatingPanelPosition = .hidden
let supportedPositions: Set<FloatingPanelPosition> = [.hidden, .half, .full]
}
class FloatingPanelLayout2Positions: FloatingPanelLayout {
func insetFor(position: FloatingPanelPosition) -> CGFloat? { return nil }
let initialPosition: FloatingPanelPosition = .tip
let supportedPositions: Set<FloatingPanelPosition> = [.tip, .half]
}
let delegate = FloatingPanelTestDelegate()
delegate.layout = FloatingPanelLayoutWithHidden()
fpc.delegate = delegate
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.topMostState, .full)
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.bottomMostState, .half) // Will fixed on fix-hidden-position branch
delegate.layout = FloatingPanelLayout2Positions()
fpc.delegate = delegate
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.topMostState, .half)
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.bottomMostState, .tip)
}
}
@@ -6,8 +6,7 @@
import XCTest
@testable import FloatingPanel
class FloatingPanelViewTests: XCTestCase {
class FloatingPanelSurfaceViewTests: XCTestCase {
override func setUp() {}
override func tearDown() {}
@@ -24,6 +23,26 @@ class FloatingPanelViewTests: XCTestCase {
XCTAssert(surface.backgroundColor == surface.containerView.backgroundColor)
}
func test_surfaceView_constraintsUpdate() {
let window = UIWindow()
let surface = FloatingPanelSurfaceView(frame: CGRect(x: 0.0, y: 0.0, width: 320.0, height: 480.0))
window.addSubview(surface)
window.makeKeyAndVisible()
XCTAssert(surface.contentView == nil)
surface.layoutIfNeeded()
XCTAssert(surface.grabberHandle.frame.minY == 6.0)
XCTAssert(surface.grabberHandle.frame.width == surface.grabberHandleWidth)
XCTAssert(surface.grabberHandle.frame.height == surface.grabberHandleHeight)
surface.grabberHandleWidth = 44.0
surface.grabberHandleHeight = 12.0
surface.layoutIfNeeded()
waitRunLoop(secs: 0.000_001)
XCTAssert(surface.grabberHandle.frame.width == surface.grabberHandleWidth, "\(surface.grabberHandle.frame.width) == \(surface.grabberHandleWidth)")
XCTAssert(surface.grabberHandle.frame.height == surface.grabberHandleHeight, "\(surface.grabberHandle.frame.height) == \(surface.grabberHandleHeight)")
window.resignKey()
}
func test_surfaceView_cornderRaduis() {
let surface = FloatingPanelSurfaceView(frame: CGRect(x: 0.0, y: 0.0, width: 320.0, height: 480.0))
XCTAssert(surface.cornerRadius == 0.0)
+64
View File
@@ -4,6 +4,7 @@
//
import XCTest
@testable import FloatingPanel
class FloatingPanelTests: XCTestCase {
@@ -11,4 +12,67 @@ class FloatingPanelTests: XCTestCase {
override func tearDown() {}
func test_scrolllock() {
let fpc = FloatingPanelController()
fpc.loadViewIfNeeded()
fpc.view.frame = CGRect(x: 0, y: 0, width: 375, height: 667)
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.show(animated: false, completion: nil) // half
XCTAssertEqual(fpc.position, .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)
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)
// 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.position, .half)
XCTAssertEqual(contentVC2.tableView.showsVerticalScrollIndicator, false)
XCTAssertEqual(contentVC2.tableView.bounces, false)
}
}
private protocol FloatingPanelTestLayout: FloatingPanelLayout {}
private extension FloatingPanelTestLayout {
func insetFor(position: FloatingPanelPosition) -> CGFloat? {
switch position {
case .full: return 18.0
case .half: return 262.0
case .tip: return 69.0
default: return nil
}
}
}
+18
View File
@@ -0,0 +1,18 @@
//
// Created by Shin Yamamoto on 2019/06/27.
// Copyright © 2019 scenee. All rights reserved.
//
import Foundation
@testable import FloatingPanel
func waitRunLoop(secs: TimeInterval = 0) {
RunLoop.main.run(until: Date(timeIntervalSinceNow: secs))
}
class FloatingPanelTestDelegate: FloatingPanelControllerDelegate {
var layout: FloatingPanelLayout?
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? {
return layout
}
}
+27
View File
@@ -0,0 +1,27 @@
// swift-tools-version:5.0
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "FloatingPanel",
platforms: [
.iOS(.v10)
],
products: [
// Products define the executables and libraries produced by a package, and make them visible to other packages.
.library(
name: "FloatingPanel",
targets: ["FloatingPanel"]),
],
dependencies: [
// Dependencies declare other packages that this package depends on.
// .package(url: /* package url */, from: "1.0.0"),
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages which this package depends on.
.target(name: "FloatingPanel", path: "Framework/Sources"),
],
swiftLanguageVersions: [.version("5")]
)