Compare commits

...

8 Commits

Author SHA1 Message Date
Shin Yamamoto 16fea625be Version 2.3.0 2021-02-23 13:45:46 +09:00
Shin Yamamoto 6c69694cfe Update readme 2021-02-23 13:45:46 +09:00
Shin Yamamoto 561d783479 Prevent a memory leak in a nested function (#441)
To fix a crash "Layout.swift: Fatal error: Attempted to read an unowned reference but the object was already deallocated"  which is reported at issue #440.
2021-02-23 09:37:35 +09:00
Shin Yamamoto 1bd2e60200 Enable to add custom panel states (#438)
* Support over 3 states in LayoutAdapter
* Allow to inherite FloatingPanelState
* Support a custome FloatingPanelState in ObjC
* Replace Menu enum with UseCase enum in Samples.app
* Rename UIExtensions to Extensions
* Add CustomState use case in Samples app
2021-02-15 21:05:12 +09:00
Shin Yamamoto ec0e1b2dad Remove class keyword
class keyword will be deprecated and in Swift 5 doesn't need it for a
protocol attributed by @objc.
2021-02-12 12:15:58 +09:00
Shin Yamamoto 9958fc5017 Prevent the potential memory leaks in the modal transition (#429)
This dismisses the frame 'FloatingPanel Core.move(from:to:animated:completion:)'
in the following memory leaks

> BoardServices -[BSXPCServiceConnectionEventHandler remoteTarget]
> BoardServices __63+[BSXPCServiceConnectionProxy createImplementationForProtocol:]_block_invoke

These leaks happens when a panel showes and hides using "Show Multi Panel Modal"
in the Samples app.
2021-02-06 09:16:11 +09:00
Shin Yamamoto 11dfc0d2f3 Fix workaround for bouncing a scroll content 2021-02-06 09:05:40 +09:00
Shin Yamamoto 34246d1f37 Merge pull request #435 from SCENEE/release-2.2.0
Prepare v2.2.0
2021-01-24 16:08:13 +09:00
14 changed files with 246 additions and 133 deletions
@@ -14,9 +14,11 @@
545DB9F821511E6400CA77B8 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 545DB9F621511E6400CA77B8 /* LaunchScreen.storyboard */; };
545DBA0321511E6400CA77B8 /* SampleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DBA0221511E6400CA77B8 /* SampleTests.swift */; };
545DBA0E21511E6400CA77B8 /* SampleUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DBA0D21511E6400CA77B8 /* SampleUITests.swift */; };
546341A125C6415100CA0596 /* UseCases.swift in Sources */ = {isa = PBXBuildFile; fileRef = 546341A025C6415100CA0596 /* UseCases.swift */; };
546341AC25C6426500CA0596 /* CustomState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 546341AB25C6426500CA0596 /* CustomState.swift */; };
549D23CB233C7779008EF4D7 /* FloatingPanel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 549D23CA233C7779008EF4D7 /* FloatingPanel.framework */; };
549D23CC233C7779008EF4D7 /* FloatingPanel.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 549D23CA233C7779008EF4D7 /* FloatingPanel.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
54B51116216AFE5F0033A6F3 /* UIExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B51115216AFE5F0033A6F3 /* UIExtensions.swift */; };
54B51116216AFE5F0033A6F3 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B51115216AFE5F0033A6F3 /* Extensions.swift */; };
54CDC5D8215BBE23007D205C /* UIComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CDC5D7215BBE23007D205C /* UIComponents.swift */; };
/* End PBXBuildFile section */
@@ -65,8 +67,10 @@
545DBA0921511E6400CA77B8 /* SamplesUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SamplesUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
545DBA0D21511E6400CA77B8 /* SampleUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SampleUITests.swift; sourceTree = "<group>"; };
545DBA0F21511E6400CA77B8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
546341A025C6415100CA0596 /* UseCases.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UseCases.swift; sourceTree = "<group>"; };
546341AB25C6426500CA0596 /* CustomState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomState.swift; sourceTree = "<group>"; };
549D23CA233C7779008EF4D7 /* FloatingPanel.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = FloatingPanel.framework; sourceTree = BUILT_PRODUCTS_DIR; };
54B51115216AFE5F0033A6F3 /* UIExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIExtensions.swift; sourceTree = "<group>"; };
54B51115216AFE5F0033A6F3 /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = "<group>"; };
54CDC5D7215BBE23007D205C /* UIComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIComponents.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
@@ -125,8 +129,9 @@
545DB9F121511E6300CA77B8 /* Main.storyboard */,
545DB9ED21511E6300CA77B8 /* AppDelegate.swift */,
545DB9EF21511E6300CA77B8 /* ViewController.swift */,
54B51115216AFE5F0033A6F3 /* UIExtensions.swift */,
54B51115216AFE5F0033A6F3 /* Extensions.swift */,
54CDC5D7215BBE23007D205C /* UIComponents.swift */,
546341AA25C6421000CA0596 /* UseCases */,
545DB9F921511E6400CA77B8 /* Info.plist */,
);
path = Sources;
@@ -150,6 +155,15 @@
path = UITests;
sourceTree = "<group>";
};
546341AA25C6421000CA0596 /* UseCases */ = {
isa = PBXGroup;
children = (
546341A025C6415100CA0596 /* UseCases.swift */,
546341AB25C6426500CA0596 /* CustomState.swift */,
);
path = UseCases;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@@ -304,7 +318,9 @@
buildActionMask = 2147483647;
files = (
54CDC5D8215BBE23007D205C /* UIComponents.swift in Sources */,
54B51116216AFE5F0033A6F3 /* UIExtensions.swift in Sources */,
54B51116216AFE5F0033A6F3 /* Extensions.swift in Sources */,
546341AC25C6426500CA0596 /* CustomState.swift in Sources */,
546341A125C6415100CA0596 /* UseCases.swift in Sources */,
545DB9F021511E6300CA77B8 /* ViewController.swift in Sources */,
545DB9EE21511E6300CA77B8 /* AppDelegate.swift in Sources */,
);
@@ -0,0 +1,22 @@
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
import Foundation
import FloatingPanel
extension FloatingPanelState {
static let lastQuart: FloatingPanelState = FloatingPanelState(rawValue: "lastQuart", order: 750)
static let firstQuart: FloatingPanelState = FloatingPanelState(rawValue: "firstQuart", order: 250)
}
class FloatingPanelLayoutWithCustomState: FloatingPanelBottomLayout {
override var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] {
return [
.full: FloatingPanelLayoutAnchor(absoluteInset: 0.0, edge: .top, referenceGuide: .safeArea),
.lastQuart: FloatingPanelLayoutAnchor(fractionalInset: 0.75, edge: .bottom, referenceGuide: .safeArea),
.half: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .safeArea),
.firstQuart: FloatingPanelLayoutAnchor(fractionalInset: 0.25, edge: .bottom, referenceGuide: .safeArea),
.tip: FloatingPanelLayoutAnchor(absoluteInset: 20.0, edge: .bottom, referenceGuide: .safeArea),
]
}
}
@@ -0,0 +1,78 @@
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
import Foundation
enum UseCases: Int, CaseIterable {
case trackingTableView
case trackingTextView
case showDetail
case showModal
case showPanelModal
case showMultiPanelModal
case showPanelInSheetModal
case showTabBar
case showPageView
case showPageContentView
case showNestedScrollView
case showRemovablePanel
case showIntrinsicView
case showContentInset
case showContainerMargins
case showNavigationController
case showTopPositionedPanel
case showAdaptivePanel
case showAdaptivePanelWithCustomGuide
case showCustomStatePanel
var name: String {
switch self {
case .trackingTableView: return "Scroll tracking(TableView)"
case .trackingTextView: return "Scroll tracking(TextView)"
case .showDetail: return "Show Detail Panel"
case .showModal: return "Show Modal"
case .showPanelModal: return "Show Panel Modal"
case .showMultiPanelModal: return "Show Multi Panel Modal"
case .showPanelInSheetModal: return "Show Panel in Sheet Modal"
case .showTabBar: return "Show Tab Bar"
case .showPageView: return "Show Page View"
case .showPageContentView: return "Show Page Content View"
case .showNestedScrollView: return "Show Nested ScrollView"
case .showRemovablePanel: return "Show Removable Panel"
case .showIntrinsicView: return "Show Intrinsic View"
case .showContentInset: return "Show with ContentInset"
case .showContainerMargins: return "Show with ContainerMargins"
case .showNavigationController: return "Show Navigation Controller"
case .showTopPositionedPanel: return "Show Top Positioned Panel"
case .showAdaptivePanel: return "Show Adaptive Panel"
case .showAdaptivePanelWithCustomGuide: return "Show Adaptive Panel (Custom Layout Guide)"
case .showCustomStatePanel: return "Show Panel with Custom state"
}
}
var storyboardID: String? {
switch self {
case .trackingTableView: return nil
case .trackingTextView: return "ConsoleViewController"
case .showDetail: return "DetailViewController"
case .showModal: return "ModalViewController"
case .showMultiPanelModal: return nil
case .showPanelInSheetModal: return nil
case .showPanelModal: return nil
case .showTabBar: return "TabBarViewController"
case .showPageView: return nil
case .showPageContentView: return nil
case .showNestedScrollView: return "NestedScrollViewController"
case .showRemovablePanel: return "DetailViewController"
case .showIntrinsicView: return "IntrinsicViewController"
case .showContentInset: return nil
case .showContainerMargins: return nil
case .showNavigationController: return "RootNavigationController"
case .showTopPositionedPanel: return nil
case .showAdaptivePanel,
.showAdaptivePanelWithCustomGuide:
return "ImageViewController"
case .showCustomStatePanel:
return nil
}
}
}
+10 -79
View File
@@ -6,78 +6,7 @@ import FloatingPanel
class SampleListViewController: UIViewController {
@IBOutlet weak var tableView: UITableView!
enum Menu: Int, CaseIterable {
case trackingTableView
case trackingTextView
case showDetail
case showModal
case showPanelModal
case showMultiPanelModal
case showPanelInSheetModal
case showTabBar
case showPageView
case showPageContentView
case showNestedScrollView
case showRemovablePanel
case showIntrinsicView
case showContentInset
case showContainerMargins
case showNavigationController
case showTopPositionedPanel
case showAdaptivePanel
case showAdaptivePanelWithCustomGuide
var name: String {
switch self {
case .trackingTableView: return "Scroll tracking(TableView)"
case .trackingTextView: return "Scroll tracking(TextView)"
case .showDetail: return "Show Detail Panel"
case .showModal: return "Show Modal"
case .showPanelModal: return "Show Panel Modal"
case .showMultiPanelModal: return "Show Multi Panel Modal"
case .showPanelInSheetModal: return "Show Panel in Sheet Modal"
case .showTabBar: return "Show Tab Bar"
case .showPageView: return "Show Page View"
case .showPageContentView: return "Show Page Content View"
case .showNestedScrollView: return "Show Nested ScrollView"
case .showRemovablePanel: return "Show Removable Panel"
case .showIntrinsicView: return "Show Intrinsic View"
case .showContentInset: return "Show with ContentInset"
case .showContainerMargins: return "Show with ContainerMargins"
case .showNavigationController: return "Show Navigation Controller"
case .showTopPositionedPanel: return "Show Top Positioned Panel"
case .showAdaptivePanel: return "Show Adaptive Panel"
case .showAdaptivePanelWithCustomGuide: return "Show Adaptive Panel (Custom Layout Guide)"
}
}
var storyboardID: String? {
switch self {
case .trackingTableView: return nil
case .trackingTextView: return "ConsoleViewController"
case .showDetail: return "DetailViewController"
case .showModal: return "ModalViewController"
case .showMultiPanelModal: return nil
case .showPanelInSheetModal: return nil
case .showPanelModal: return nil
case .showTabBar: return "TabBarViewController"
case .showPageView: return nil
case .showPageContentView: return nil
case .showNestedScrollView: return "NestedScrollViewController"
case .showRemovablePanel: return "DetailViewController"
case .showIntrinsicView: return "IntrinsicViewController"
case .showContentInset: return nil
case .showContainerMargins: return nil
case .showNavigationController: return "RootNavigationController"
case .showTopPositionedPanel: return nil
case .showAdaptivePanel,
.showAdaptivePanelWithCustomGuide:
return "ImageViewController"
}
}
}
var currentMenu: Menu = .trackingTableView
var currentMenu: UseCases = .trackingTableView
var mainPanelVC: FloatingPanelController!
var detailPanelVC: FloatingPanelController!
@@ -254,19 +183,19 @@ extension SampleListViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if #available(iOS 11.0, *) {
if navigationController?.navigationBar.prefersLargeTitles == true {
return Menu.allCases.count + 30
return UseCases.allCases.count + 30
} else {
return Menu.allCases.count
return UseCases.allCases.count
}
} else {
return Menu.allCases.count
return UseCases.allCases.count
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
if Menu.allCases.count > indexPath.row {
let menu = Menu.allCases[indexPath.row]
if UseCases.allCases.count > indexPath.row {
let menu = UseCases.allCases[indexPath.row]
cell.textLabel?.text = menu.name
} else {
cell.textLabel?.text = "\(indexPath.row) row"
@@ -277,8 +206,8 @@ extension SampleListViewController: UITableViewDataSource {
extension SampleListViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard Menu.allCases.count > indexPath.row else { return }
let menu = Menu.allCases[indexPath.row]
guard UseCases.allCases.count > indexPath.row else { return }
let menu = UseCases.allCases[indexPath.row]
let contentVC: UIViewController = {
guard let storyboardID = menu.storyboardID else { return DebugTableViewController() }
guard let vc = self.storyboard?.instantiateViewController(withIdentifier: storyboardID) else { fatalError() }
@@ -450,6 +379,8 @@ extension SampleListViewController: FloatingPanelControllerDelegate {
fallthrough
case .showContentInset:
return FloatingPanelBottomLayout()
case .showCustomStatePanel:
return FloatingPanelLayoutWithCustomState()
default:
return (newCollection.verticalSizeClass == .compact) ? FloatingPanelBottomLayout() : self
}
@@ -3,6 +3,22 @@
#import "ViewController.h"
@import FloatingPanel;
// Defining a custom FloatingPanelState
@interface FloatingPanelState(Extended)
+ (FloatingPanelState *)LastQuart;
@end
@implementation FloatingPanelState(Extended)
static FloatingPanelState *_lastQuart;
+ (FloatingPanelState *)LastQuart {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_lastQuart = [[FloatingPanelState alloc] initWithRawValue:@"lastquart" order:750];
});
return _lastQuart;
}
@end
@interface ViewController()<FloatingPanelControllerDelegate>
@end
@@ -59,6 +75,9 @@
}
- (NSDictionary<FloatingPanelState *, id<FloatingPanelLayoutAnchoring>> *)anchors {
return @{
FloatingPanelState.LastQuart: [[FloatingPanelLayoutAnchor alloc] initWithFractionalInset:0.25
edge:FloatingPanelReferenceEdgeTop
referenceGuide:FloatingPanelLayoutReferenceGuideSafeArea],
FloatingPanelState.Half: [[FloatingPanelLayoutAnchor alloc] initWithFractionalInset:0.5
edge:FloatingPanelReferenceEdgeTop
referenceGuide:FloatingPanelLayoutReferenceGuideSafeArea],
+1 -1
View File
@@ -1,7 +1,7 @@
Pod::Spec.new do |s|
s.name = "FloatingPanel"
s.version = "2.2.0"
s.version = "2.3.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.
+25 -1
View File
@@ -36,6 +36,7 @@ The new interface displays the related contents and utilities in parallel as a u
- [Use the intrinsic size of a content in your panel layout](#use-the-intrinsic-size-of-a-content-in-your-panel-layout)
- [Specify an anchor for each state by an inset of the `FloatingPanelController.view` frame](#specify-an-anchor-for-each-state-by-an-inset-of-the-floatingpanelcontrollerview-frame)
- [Change the backdrop alpha](#change-the-backdrop-alpha)
- [Using custome panel states](#using-custome-panel-states)
- [Customize the behavior with `FloatingPanelBehavior` protocol](#customize-the-behavior-with-floatingpanelbehavior-protocol)
- [Modify your floating panel's interaction](#modify-your-floating-panels-interaction)
- [Activate the rubber-band effect on panel edges](#activate-the-rubber-band-effect-on-panel-edges)
@@ -72,7 +73,7 @@ The new interface displays the related contents and utilities in parallel as a u
- [x] Multi panel support
- [x] Modal presentation
- [x] 4 positioning support(top, left, bottom, right)
- [x] 1~3 magnetic anchors(full, half, tip)
- [x] 1 or more magnetic anchors(full, half, tip and more)
- [x] Layout support for all trait environments(i.e. Landscape orientation)
- [x] Common UI elements: surface, backdrop and grabber handle
- [x] Free from common issues of Auto Layout and gesture handling
@@ -388,6 +389,29 @@ class MyPanelLayout: FloatingPanelLayout {
}
```
#### Using custome panel states
You're able to define custom panel states and use them as the following example.
```swift
extension FloatingPanelState {
static let lastQuart: FloatingPanelState = FloatingPanelState(rawValue: "lastQuart", order: 750)
static let firstQuart: FloatingPanelState = FloatingPanelState(rawValue: "firstQuart", order: 250)
}
class FloatingPanelLayoutWithCustomState: FloatingPanelBottomLayout {
override var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] {
return [
.full: FloatingPanelLayoutAnchor(absoluteInset: 0.0, edge: .top, referenceGuide: .safeArea),
.lastQuart: FloatingPanelLayoutAnchor(fractionalInset: 0.75, edge: .bottom, referenceGuide: .safeArea),
.half: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .safeArea),
.firstQuart: FloatingPanelLayoutAnchor(fractionalInset: 0.25, edge: .bottom, referenceGuide: .safeArea),
.tip: FloatingPanelLayoutAnchor(absoluteInset: 20.0, edge: .bottom, referenceGuide: .safeArea),
]
}
}
```
### Customize the behavior with `FloatingPanelBehavior` protocol
#### Modify your floating panel's interaction
+1 -1
View File
@@ -5,7 +5,7 @@ import UIKit
/// A set of methods implemented by the delegate of a panel controller allows the adopting delegate to respond to
/// messages from the FloatingPanelController class and thus respond to, and in some affect, operations such as
/// dragging, attracting a panel, layout of a panel and the content, and transition animations.
@objc public protocol FloatingPanelControllerDelegate: class {
@objc public protocol FloatingPanelControllerDelegate {
/// Returns a FloatingPanelLayout object. If you use the default one, you can use a `FloatingPanelBottomLayout` object.
@objc(floatingPanel:layoutForTraitCollection:) optional
func floatingPanel(_ fpc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout
+23 -9
View File
@@ -32,7 +32,8 @@ class Core: NSObject, UIGestureRecognizerDelegate {
let panGestureRecognizer: FloatingPanelPanGestureRecognizer
var isRemovalInteractionEnabled: Bool = false
fileprivate var animator: UIViewPropertyAnimator?
fileprivate var isSuspended: Bool = false // Prevent a memory leak in the modal transition
fileprivate var transitionAnimator: UIViewPropertyAnimator?
fileprivate var moveAnimator: NumericSpringAnimator?
private var initialSurfaceLocation: CGPoint = .zero
@@ -114,7 +115,8 @@ class Core: NSObject, UIGestureRecognizerDelegate {
interruptAnimationIfNeeded()
if animated {
func updateScrollView() {
let updateScrollView: () -> Void = { [weak self] in
guard let self = self else { return }
if self.state == self.layoutAdapter.edgeMostState, abs(self.layoutAdapter.offsetFromEdgeMost) <= 1.0 {
self.unlockScrollView()
} else {
@@ -158,12 +160,15 @@ class Core: NSObject, UIGestureRecognizerDelegate {
animator.addCompletion { [weak self] _ in
guard let self = self else { return }
self.animator = nil
self.transitionAnimator = nil
updateScrollView()
self.ownerVC?.notifyDidMove()
completion?()
}
self.animator = animator
self.transitionAnimator = animator
if isSuspended {
return
}
animator.startAnimation()
} else {
self.state = to
@@ -376,7 +381,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
if interactionInProgress {
lockScrollView()
} else {
if state == layoutAdapter.edgeMostState, self.animator == nil {
if state == layoutAdapter.edgeMostState, self.transitionAnimator == nil {
switch layoutAdapter.position {
case .top, .left:
if offsetDiff < 0 && velocity > 0 {
@@ -496,7 +501,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
animator.stopAnimation(true)
endAttraction(false)
}
if let animator = self.animator {
if let animator = self.transitionAnimator {
guard 0 >= layoutAdapter.offsetFromEdgeMost else { return }
log.debug("a panel animation(interruptible: \(animator.isInterruptible)) interrupted!!!")
if animator.isInterruptible {
@@ -710,14 +715,14 @@ class Core: NSObject, UIGestureRecognizerDelegate {
// Workaround: Disable a tracking scroll to prevent bouncing a scroll content in a panel animating
let isScrollEnabled = scrollView?.isScrollEnabled
if let scrollView = scrollView, targetPosition != .full {
if let scrollView = scrollView, targetPosition != layoutAdapter.edgeMostState {
scrollView.isScrollEnabled = false
}
startAttraction(to: targetPosition, with: velocity)
// Workaround: Reset `self.scrollView.isScrollEnabled`
if let scrollView = scrollView, targetPosition != .full,
if let scrollView = scrollView, targetPosition != layoutAdapter.edgeMostState,
let isScrollEnabled = isScrollEnabled {
scrollView.isScrollEnabled = isScrollEnabled
}
@@ -1038,7 +1043,7 @@ public final class FloatingPanelPanGestureRecognizer: UIPanGestureRecognizer {
public override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event)
initialLocation = touches.first?.location(in: view) ?? .zero
if floatingPanel?.animator != nil || floatingPanel?.moveAnimator != nil {
if floatingPanel?.transitionAnimator != nil || floatingPanel?.moveAnimator != nil {
self.state = .began
}
}
@@ -1199,3 +1204,12 @@ private class NumericSpringAnimator: NSObject {
v = (v + h * o2 * (xt - x)) / det
}
}
extension FloatingPanelController {
func suspendTransitionAnimator(_ suspended: Bool) {
self.floatingPanel.isSuspended = suspended
}
var transitionAnimator: UIViewPropertyAnimator? {
return self.floatingPanel.transitionAnimator
}
}
+1 -1
View File
@@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>2.2.0</string>
<string>2.3.0</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
</dict>
+17 -31
View File
@@ -82,9 +82,8 @@ class LayoutAdapter {
private var initialConst: CGFloat = 0.0
private var fixedConstraints: [NSLayoutConstraint] = []
private var fullConstraints: [NSLayoutConstraint] = []
private var halfConstraints: [NSLayoutConstraint] = []
private var tipConstraints: [NSLayoutConstraint] = []
private var stateConstraints: [FloatingPanelState: [NSLayoutConstraint]] = [:]
private var offConstraints: [NSLayoutConstraint] = []
private var fitToBoundsConstraint: NSLayoutConstraint?
@@ -452,25 +451,13 @@ class LayoutAdapter {
}
private func updateStateConstraints() {
NSLayoutConstraint.deactivate(fullConstraints + halfConstraints + tipConstraints + offConstraints)
if let fullAnchor = layout.anchors[.full] {
fullConstraints = fullAnchor.layoutConstraints(vc, for: position)
fullConstraints.forEach {
$0.identifier = "FloatingPanel-full-constraint"
}
}
if let halfAnchor = layout.anchors[.half] {
halfConstraints = halfAnchor.layoutConstraints(vc, for: position)
halfConstraints.forEach {
$0.identifier = "FloatingPanel-half-constraint"
}
}
if let tipAnchors = layout.anchors[.tip] {
tipConstraints = tipAnchors.layoutConstraints(vc, for: position)
tipConstraints.forEach {
$0.identifier = "FloatingPanel-tip-constraint"
}
let allStateConstraints = stateConstraints.flatMap { $1 }
NSLayoutConstraint.deactivate(allStateConstraints + offConstraints)
stateConstraints.removeAll()
for state in layout.anchors.keys {
stateConstraints[state] = layout.anchors[state]?
.layoutConstraints(vc, for: position)
.map{ $0.identifier = "FloatingPanel-\(state)-constraint"; return $0 }
}
let hiddenAnchor = layout.anchors[.hidden] ?? self.hiddenAnchor
offConstraints = hiddenAnchor.layoutConstraints(vc, for: position)
@@ -487,7 +474,7 @@ class LayoutAdapter {
tearDownAttraction()
NSLayoutConstraint.deactivate(fullConstraints + halfConstraints + tipConstraints + offConstraints)
NSLayoutConstraint.deactivate(stateConstraints.flatMap { $1 } + offConstraints)
initialConst = edgePosition(surfaceView.frame) + offset.y
@@ -527,7 +514,7 @@ class LayoutAdapter {
let anchor = layout.anchors[state] ?? self.hiddenAnchor
NSLayoutConstraint.deactivate(fullConstraints + halfConstraints + tipConstraints + offConstraints)
NSLayoutConstraint.deactivate(stateConstraints.flatMap { $1 } + offConstraints)
NSLayoutConstraint.deactivate(constraint: interactionConstraint)
interactionConstraint = nil
@@ -768,17 +755,16 @@ class LayoutAdapter {
// on-screen and off-screen view which includes
// UIStackView(i.e. Settings view in Samples.app)
updateStateConstraints()
switch state {
case .full:
NSLayoutConstraint.activate(fullConstraints)
case .half:
NSLayoutConstraint.activate(halfConstraints)
case .tip:
NSLayoutConstraint.activate(tipConstraints)
case .hidden:
NSLayoutConstraint.activate(offConstraints)
default:
break
if let constraints = stateConstraints[state] {
NSLayoutConstraint.activate(constraints)
} else {
log.error("Couldn't find any constraints for \(state)")
}
}
}
+3 -2
View File
@@ -4,7 +4,7 @@ import Foundation
/// An object that represents the display state of a panel in a screen.
@objc
public class FloatingPanelState: NSObject, NSCopying, RawRepresentable {
open class FloatingPanelState: NSObject, NSCopying, RawRepresentable {
public typealias RawValue = String
required public init?(rawValue: RawValue) {
@@ -13,6 +13,7 @@ public class FloatingPanelState: NSObject, NSCopying, RawRepresentable {
super.init()
}
@objc
public init(rawValue: RawValue, order: Int) {
self.rawValue = rawValue
self.order = order
@@ -33,7 +34,7 @@ public class FloatingPanelState: NSObject, NSCopying, RawRepresentable {
}
public override var debugDescription: String {
return description
return "<FloatingPanel.FloatingPanelState: \(Unmanaged.passUnretained(self).toOpaque())>"
}
/// A panel state indicates the entire panel is shown.
+26 -4
View File
@@ -90,14 +90,25 @@ class ModalPresentTransition: NSObject, UIViewControllerAnimatedTransitioning {
return TimeInterval(animator.duration)
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
func interruptibleAnimator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating {
guard
let fpc = transitionContext.viewController(forKey: .to) as? FloatingPanelController
else { fatalError() }
fpc.show(animated: true) {
if let animator = fpc.transitionAnimator {
return animator
}
fpc.suspendTransitionAnimator(true)
fpc.show(animated: true) { [weak fpc] in
fpc?.suspendTransitionAnimator(false)
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
return fpc.transitionAnimator!
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
self.interruptibleAnimator(using: transitionContext).startAnimation()
}
}
@@ -111,14 +122,25 @@ class ModalDismissTransition: NSObject, UIViewControllerAnimatedTransitioning {
return TimeInterval(animator.duration)
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
func interruptibleAnimator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating {
guard
let fpc = transitionContext.viewController(forKey: .from) as? FloatingPanelController
else { fatalError() }
fpc.hide(animated: true) {
if let animator = fpc.transitionAnimator {
return animator
}
fpc.suspendTransitionAnimator(true)
fpc.hide(animated: true) { [weak fpc] in
fpc?.suspendTransitionAnimator(false)
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
return fpc.transitionAnimator!
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
self.interruptibleAnimator(using: transitionContext).startAnimation()
}
}