Compare commits

...

12 Commits

Author SHA1 Message Date
Shin Yamamoto 5325e707e6 Release v1.6.5 2019-08-31 12:51:01 +09:00
Nikolay Derkach 1dc0a6b76a Support bottom content inset for container view (#257)
and also fix height of a content view resized by the inset
Fix #256
2019-08-31 12:49:59 +09:00
David Hart 2689d68bab Improve floatingPanelDidChangePosition and tigger it on removal 2019-08-31 12:49:57 +09:00
Shin Yamamoto 218a12962f Don't unregister safeAreaInsetsObservation in hide()
Because the observation can't be active when a user makes a panel visible
by move(to:) instead of show().
2019-08-24 15:37:15 +09:00
Shin Yamamoto 916d2ec76a Add move-to-hidden tests 2019-08-24 15:37:15 +09:00
Shin Yamamoto 1b1ba5deef Update README for UISearchController issue 2019-08-24 15:37:15 +09:00
Shin Yamamoto 58b2df4996 Fix UISearchBar's _searchField access 2019-08-24 09:54:54 +09:00
Shin Yamamoto 3b812be84e Return true for FloatingPanelSurfaceView.requiresConstraintBasedLayout 2019-08-13 15:27:27 +09:00
Shin Yamamoto 5d86bd5d02 Release v1.6.4 2019-08-09 09:55:04 +09:00
Shin Yamamoto 3b6271c4f4 Fix stopping a panel b/w anchors after an interruption
The panel(surface view) could stop b/w anchors if the pan gesture doesn't
pass through `.changed` state after an interruptible animator is interrupted.

The possible reason is the constraints have never changed since the last animation
is committed so that `surfaceView.superview!.layoutIfNeeded()` doesn't trigger
a layout update by the constraint-based layout system in
`FloatingPanelLayoutAdapter.activateLayout(of:)`.

Thus the inserted code changes a panel interactive constraint by the least
positive number. It allows the constraint-based layout system to update the
surface layout expectedly.
2019-08-08 10:52:51 +09:00
Shin Yamamoto 1671a3d50f Always call startInteraction before endInteraction 2019-08-07 22:27:13 +09:00
Shin Yamamoto 0ab318e804 Fix not calling floatingPanelDidEndDecelerating delegate after interruption
If the decelerating animation is interrupted and
floatingPanelShouldBeginDragging delegate method returns false,
floatingPanelDidEndDecelerating delegate method will not be called
after calling floatingPanelWillBeginDecelerating method.

A panel have to run an animation after the interruption and also
floatingPanelDidEndDecelerating(_:) delegate should be called always
after calling floatingPanelWillBeginDecelerating method.

Therefore floatingPanelShouldBeginDragging delegate method shouldn't be
called in the panel decelerating.
2019-08-07 22:07:35 +09:00
13 changed files with 107 additions and 26 deletions
+13 -3
View File
@@ -148,11 +148,9 @@ class SearchPanelViewController: UIViewController, UITableViewDataSource, UITabl
tableView.dataSource = self
tableView.delegate = self
searchBar.placeholder = "Search for a place or address"
let textField = searchBar.value(forKey: "_searchField") as! UITextField
textField.font = UIFont(name: textField.font!.fontName, size: 15.0)
searchBar.setSearchText(fontSize: 15.0)
hideHeader()
}
override func viewDidLayoutSubviews() {
@@ -274,3 +272,15 @@ class SearchHeaderView: UIView {
self.clipsToBounds = true
}
}
extension UISearchBar {
func setSearchText(fontSize: CGFloat) {
#if swift(>=5.1) // Xcode 11 or later
let font = searchTextField.font
searchTextField.font = font?.withSize(fontSize)
#else
let textField = value(forKey: "_searchField") as! UITextField
textField.font = textField.font?.withSize(fontSize)
#endif
}
}
@@ -324,7 +324,7 @@
</connections>
</button>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="top" spacing="44" translatesAutoresizingMaskIntoConstraints="NO" id="9p4-06-y2T">
<rect key="frame" x="139.66666666666666" y="132" width="96" height="252"/>
<rect key="frame" x="134.66666666666666" y="132" width="106" height="326"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="i9x-x5-n1q">
<rect key="frame" x="0.0" y="0.0" width="80" height="30"/>
@@ -347,8 +347,15 @@
<action selector="moveToTipWithSender:" destination="bYI-y3-Rzb" eventType="touchUpInside" id="BmL-91-9ai"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="swr-XM-GzZ">
<rect key="frame" x="0.0" y="222" width="106" height="30"/>
<state key="normal" title="Move to hidden"/>
<connections>
<action selector="moveToHiddenWithSender:" destination="bYI-y3-Rzb" eventType="touchUpInside" id="jfJ-0f-fdk"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="szf-HE-QTk">
<rect key="frame" x="0.0" y="222" width="96" height="30"/>
<rect key="frame" x="0.0" y="296" width="96" height="30"/>
<state key="normal" title="Update layout"/>
<connections>
<action selector="updateLayout:" destination="bYI-y3-Rzb" eventType="touchUpInside" id="Woz-a7-YMJ"/>
+31 -4
View File
@@ -306,8 +306,8 @@ extension SampleListViewController: UITableViewDelegate {
let fpc = FloatingPanelController()
fpc.set(contentViewController: contentViewController)
fpc.surfaceView.contentInsets = .init(top: 20, left: 20, bottom: 0, right: 20)
fpc.surfaceView.contentInsets = .init(top: 20, left: 20, bottom: 20, right: 20)
fpc.delegate = self
fpc.isRemovalInteractionEnabled = true
self.present(fpc, animated: true, completion: nil)
@@ -340,8 +340,10 @@ extension SampleListViewController: FloatingPanelControllerDelegate {
return ModalPanelLayout()
}
fallthrough
case .showContentInset:
return NoInteractionBufferPanelLayout()
default:
return (newCollection.verticalSizeClass == .compact) ? nil : self
return (newCollection.verticalSizeClass == .compact) ? nil : self
}
}
@@ -406,6 +408,29 @@ extension SampleListViewController: UIPageViewControllerDataSource {
class IntrinsicPanelLayout: FloatingPanelIntrinsicLayout { }
class NoInteractionBufferPanelLayout: FloatingPanelLayout {
var initialPosition: FloatingPanelPosition {
return .full
}
func insetFor(position: FloatingPanelPosition) -> CGFloat? {
switch position {
case .full: return 0
case .half: return 216
case .tip: return 60
case .hidden: return nil
}
}
var topInteractionBuffer: CGFloat {
return 0.0
}
var bottomInteractionBuffer: CGFloat {
return 0.0
}
}
class RemovablePanelLayout: FloatingPanelIntrinsicLayout {
var supportedPositions: Set<FloatingPanelPosition> {
return [.full, .half]
@@ -799,7 +824,9 @@ class ModalViewController: UIViewController, FloatingPanelControllerDelegate {
@IBAction func moveToTip(sender: UIButton) {
fpc.move(to: .tip, animated: true)
}
@IBAction func moveToHidden(sender: UIButton) {
fpc.move(to: .hidden, animated: true)
}
@IBAction func updateLayout(_ sender: Any) {
isNewlayout = !isNewlayout
UIView.animate(withDuration: 0.5) {
+1 -1
View File
@@ -1,7 +1,7 @@
Pod::Spec.new do |s|
s.name = "FloatingPanel"
s.version = "1.6.3"
s.version = "1.6.5"
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.
+16 -7
View File
@@ -337,9 +337,14 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
log.debug("panel gesture(\(state):\(panGesture.state)) --",
"translation = \(translation.y), location = \(location.y), velocity = \(velocity.y)")
if interactionInProgress == false, isDecelerating == false,
let vc = viewcontroller, vc.delegate?.floatingPanelShouldBeginDragging(vc) == false {
return
}
if let animator = self.animator {
guard surfaceView.presentationFrame.minY >= layoutAdapter.topMaxY else { return }
log.debug("panel animation interrupted!!!")
log.debug("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
@@ -354,12 +359,6 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
}
}
if interactionInProgress == false,
let vc = viewcontroller,
vc.delegate?.floatingPanelShouldBeginDragging(vc) == false {
return
}
if panGesture.state == .began {
panningBegan(at: location)
return
@@ -376,6 +375,15 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
}
panningChange(with: translation)
case .ended, .cancelled, .failed:
if interactionInProgress == false {
startInteraction(with: translation, at: location)
// Workaround: Prevent stopping the surface view b/w anchors if the pan gesture
// doesn't pass through .changed state after an interruptible animator is interrupted.
let dy = translation.y - .leastNonzeroMagnitude
layoutAdapter.updateInteractiveTopConstraint(diff: dy,
allowsTopBuffer: true,
with: behavior)
}
panningEnd(with: translation, velocity: velocity)
default:
break
@@ -615,6 +623,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
let animator = behavior.removalInteractionAnimator(vc, with: velocityVector)
animator.addAnimations { [weak self] in
self?.state = .hidden
self?.updateLayout(to: .hidden)
}
animator.addCompletion({ _ in
@@ -12,7 +12,9 @@ public protocol FloatingPanelControllerDelegate: class {
// if it returns nil, FloatingPanelController uses the default behavior
func floatingPanel(_ vc: FloatingPanelController, behaviorFor newCollection: UITraitCollection) -> FloatingPanelBehavior?
func floatingPanelDidChangePosition(_ vc: FloatingPanelController) // changed the settled position in the model layer
/// Called when the floating panel has changed to a new position. Can be called inside an animation block, so any
/// view properties set inside this function will be automatically animated alongside the panel.
func floatingPanelDidChangePosition(_ vc: FloatingPanelController)
/// Asks the delegate if dragging should begin by the pan gesture recognizer.
func floatingPanelShouldBeginDragging(_ vc: FloatingPanelController) -> Bool
@@ -361,7 +363,6 @@ open class FloatingPanelController: UIViewController, UIScrollViewDelegate, UIGe
/// Hides the surface view to the hidden position
public func hide(animated: Bool = false, completion: (() -> Void)? = nil) {
safeAreaInsetsObservation = nil
move(to: .hidden,
animated: animated,
completion: completion)
@@ -454,6 +454,7 @@ class FloatingPanelLayoutAdapter {
func activateLayout(of state: FloatingPanelPosition) {
defer {
surfaceView.superview!.layoutIfNeeded()
log.debug("activateLayout -- surface.presentation = \(self.surfaceView.presentationFrame) surface.frame = \(self.surfaceView.frame)")
}
var state = state
@@ -36,8 +36,6 @@ public class FloatingPanelSurfaceView: UIView {
public weak var contentView: UIView!
/// The content insets specifying the insets around the content view.
///
/// - important: Currently the `bottom` inset is ignored.
public var contentInsets: UIEdgeInsets = .zero {
didSet {
// Needs update constraints
@@ -115,6 +113,8 @@ public class FloatingPanelSurfaceView: UIView {
private lazy var grabberHandleHeightConstraint: NSLayoutConstraint = grabberHandle.heightAnchor.constraint(equalToConstant: grabberHandleHeight)
private lazy var grabberHandleTopConstraint: NSLayoutConstraint = grabberHandle.topAnchor.constraint(equalTo: topAnchor, constant: grabberTopPadding)
public override class var requiresConstraintBasedLayout: Bool { return true }
override init(frame: CGRect) {
super.init(frame: frame)
addSubViews()
@@ -155,7 +155,7 @@ public class FloatingPanelSurfaceView: UIView {
contentViewTopConstraint?.constant = contentInsets.top
contentViewLeftConstraint?.constant = contentInsets.left
contentViewRightConstraint?.constant = contentInsets.right
contentViewHeightConstraint?.constant = -containerTopInset
contentViewHeightConstraint?.constant = -(containerTopInset + contentInsets.top + contentInsets.bottom)
grabberHandleTopConstraint.constant = grabberTopPadding
grabberHandleWidthConstraint.constant = grabberHandleWidth
@@ -221,7 +221,7 @@ public class FloatingPanelSurfaceView: UIView {
let topConstraint = contentView.topAnchor.constraint(equalTo: topAnchor, constant: contentInsets.top)
let leftConstraint = contentView.leftAnchor.constraint(equalTo: leftAnchor, constant: contentInsets.left)
let rightConstraint = rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: contentInsets.right)
let heightConstraint = contentView.heightAnchor.constraint(equalTo: heightAnchor, constant: -containerTopInset)
let heightConstraint = contentView.heightAnchor.constraint(equalTo: heightAnchor, constant: -(containerTopInset + contentInsets.top + contentInsets.bottom))
NSLayoutConstraint.activate([
topConstraint,
leftConstraint,
+1 -1
View File
@@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.6.3</string>
<string>1.6.5</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
</dict>
@@ -52,44 +52,58 @@ class FloatingPanelControllerTests: XCTestCase {
}
func test_moveTo() {
let fpc = FloatingPanelController(delegate: nil)
let delegate = FloatingPanelTestDelegate()
let fpc = FloatingPanelController(delegate: delegate)
XCTAssertEqual(delegate.position, .hidden)
fpc.showForTest()
XCTAssertEqual(delegate.position, .half)
fpc.hide()
XCTAssertEqual(delegate.position, .hidden)
fpc.move(to: .full, animated: false)
XCTAssertEqual(fpc.position, .full)
XCTAssertEqual(delegate.position, .full)
XCTAssertEqual(fpc.surfaceView.frame.minY, fpc.originYOfSurface(for: .full))
fpc.move(to: .half, animated: false)
XCTAssertEqual(fpc.position, .half)
XCTAssertEqual(delegate.position, .half)
XCTAssertEqual(fpc.surfaceView.frame.minY, fpc.originYOfSurface(for: .half))
fpc.move(to: .tip, animated: false)
XCTAssertEqual(fpc.position, .tip)
XCTAssertEqual(delegate.position, .tip)
XCTAssertEqual(fpc.surfaceView.frame.minY, fpc.originYOfSurface(for: .tip))
fpc.move(to: .hidden, animated: false)
XCTAssertEqual(fpc.position, .hidden)
XCTAssertEqual(delegate.position, .hidden)
XCTAssertEqual(fpc.surfaceView.frame.minY, fpc.originYOfSurface(for: .hidden))
fpc.move(to: .full, animated: true)
waitRunLoop(secs: 0.3)
XCTAssertEqual(fpc.position, .full)
XCTAssertEqual(delegate.position, .full)
XCTAssertEqual(fpc.surfaceView.frame.minY, fpc.originYOfSurface(for: .full))
fpc.move(to: .half, animated: true)
waitRunLoop(secs: 0.3)
XCTAssertEqual(fpc.position, .half)
XCTAssertEqual(delegate.position, .half)
XCTAssertEqual(fpc.surfaceView.frame.minY, fpc.originYOfSurface(for: .half))
fpc.move(to: .tip, animated: true)
waitRunLoop(secs: 0.3)
XCTAssertEqual(fpc.position, .tip)
XCTAssertEqual(delegate.position, .tip)
XCTAssertEqual(fpc.surfaceView.frame.minY, fpc.originYOfSurface(for: .tip))
fpc.move(to: .hidden, animated: true)
waitRunLoop(secs: 0.3)
XCTAssertEqual(fpc.position, .hidden)
XCTAssertEqual(delegate.position, .hidden)
XCTAssertEqual(fpc.surfaceView.frame.minY, fpc.originYOfSurface(for: .hidden))
}
func test_originSurfaceY() {
@@ -12,6 +12,7 @@ class FloatingPanelSurfaceViewTests: XCTestCase {
func test_surfaceView() {
let surface = FloatingPanelSurfaceView(frame: CGRect(x: 0.0, y: 0.0, width: 320.0, height: 480.0))
XCTAssertTrue(FloatingPanelSurfaceView.requiresConstraintBasedLayout)
XCTAssert(surface.contentView == nil)
surface.layoutIfNeeded()
XCTAssert(surface.grabberHandle.frame.minY == 6.0)
+4
View File
@@ -21,12 +21,16 @@ extension FloatingPanelController {
class FloatingPanelTestDelegate: FloatingPanelControllerDelegate {
var layout: FloatingPanelLayout?
var behavior: FloatingPanelBehavior?
var position: FloatingPanelPosition = .hidden
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? {
return layout
}
func floatingPanel(_ vc: FloatingPanelController, behaviorFor newCollection: UITraitCollection) -> FloatingPanelBehavior? {
return behavior
}
func floatingPanelDidChangePosition(_ vc: FloatingPanelController) {
position = vc.position
}
}
protocol FloatingPanelTestLayout: FloatingPanelFullScreenLayout {}
+7
View File
@@ -43,6 +43,7 @@ The new interface displays the related contents and utilities in parallel as a u
- [Work your contents together with a floating panel behavior](#work-your-contents-together-with-a-floating-panel-behavior)
- [Notes](#notes)
- ['Show' or 'Show Detail' Segues from `FloatingPanelController`'s content view controller](#show-or-show-detail-segues-from-floatingpanelcontrollers-content-view-controller)
- [UISearchController issue](#uisearchcontroller-issue)
- [FloatingPanelSurfaceView's issue on iOS 10](#floatingpanelsurfaceviews-issue-on-ios-10)
- [Author](#author)
- [License](#license)
@@ -439,6 +440,12 @@ A `FloatingPanelController` object proxies an action for `show(_:sender)` to the
It's a great way to decouple between a floating panel and the content VC.
### UISearchController issue
`UISearchController` isn't able to be used with `FloatingPanelController` by the system design.
Because `UISearchController` automatically presents itself modally when a user interacts with the search bar, and then it swaps the superview of the search bar to the view managed by itself while it displays. As a result, `FloatingPanelController` can't control the search bar when it's active, as you can see from [the screen shot](https://github.com/SCENEE/FloatingPanel/issues/248#issuecomment-521263831).
### FloatingPanelSurfaceView's issue on iOS 10
* On iOS 10, `FloatingPanelSurfaceView.cornerRadius` isn't not automatically masked with the top rounded corners because of `UIVisualEffectView` issue. See https://forums.developer.apple.com/thread/50854.