Compare commits

..

4 Commits

Author SHA1 Message Date
Shin Yamamoto 837edc6abf Update sample 2019-06-01 23:36:20 +09:00
Shin Yamamoto 20fda3888f GrabberHandleView supports UIAppearance 2019-06-01 23:36:20 +09:00
Shin Yamamoto 4728c6848b FloatingPanelBackdropView supports UIAppearance 2019-06-01 23:36:20 +09:00
Shin Yamamoto 70c945c9f5 FloatingPanelSurfaceView supports UIAppearance 2019-06-01 23:36:20 +09:00
12 changed files with 95 additions and 120 deletions
@@ -4,8 +4,18 @@
//
import UIKit
import FloatingPanel
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
FloatingPanelSurfaceView.appearance().shadowHidden = false
FloatingPanelSurfaceView.appearance().cornerRadius = 6.0
// FloatingPanelSurfaceView.appearance().backgroundColor = .lightGray
// FloatingPanelBackdropView.appearance().backgroundColor = .red
// GrabberHandleView.appearance().barColor = .red
return true
}
}
@@ -123,10 +123,6 @@ class SampleListViewController: UIViewController {
mainPanelVC = FloatingPanelController()
mainPanelVC.delegate = self
// Initialize FloatingPanelController and add the view
mainPanelVC.surfaceView.cornerRadius = 6.0
mainPanelVC.surfaceView.shadowHidden = false
// Set a content view controller
mainPanelVC.set(contentViewController: contentVC)
+1 -1
View File
@@ -1,7 +1,7 @@
Pod::Spec.new do |s|
s.name = "FloatingPanel"
s.version = "1.6.1"
s.version = "1.5.1"
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.
+29 -26
View File
@@ -36,7 +36,15 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
let panGestureRecognizer: FloatingPanelPanGestureRecognizer
var isRemovalInteractionEnabled: Bool = false
fileprivate var animator: UIViewPropertyAnimator?
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)
}
}
private var initialFrame: CGRect = .zero
private var initialTranslationY: CGFloat = 0
@@ -60,10 +68,8 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
viewcontroller = vc
surfaceView = FloatingPanelSurfaceView()
surfaceView.backgroundColor = .white
backdropView = FloatingPanelBackdropView()
backdropView.backgroundColor = .black
backdropView.alpha = 0.0
self.layoutAdapter = FloatingPanelLayoutAdapter(surfaceView: surfaceView,
@@ -116,9 +122,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
animator.addCompletion { [weak self] _ in
guard let `self` = self else { return }
self.animator = nil
if self.state == self.layoutAdapter.topMostState {
self.unlockScrollView()
}
self.unlockScrollView()
completion?()
}
self.animator = animator
@@ -126,9 +130,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
} else {
self.state = to
self.updateLayout(to: to)
if self.state == self.layoutAdapter.topMostState {
self.unlockScrollView()
}
self.unlockScrollView()
completion?()
}
}
@@ -267,6 +269,13 @@ 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 {
@@ -278,17 +287,15 @@ 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 {
if offset > 0 {
unlockScrollView()
}
unlockScrollView()
} else {
// Hide a scroll indicator just before starting an interaction by swiping a panel down.
if state == layoutAdapter.topMostState,
offset < 0, velocity.y > 0 {
lockScrollView()
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)
}
}
}
@@ -510,7 +517,11 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
viewcontroller.delegate?.floatingPanelDidEndDraggingToRemove(viewcontroller, withVelocity: velocity)
self.startRemovalAnimation(with: velocityVector) { [weak self] in
self?.finishRemovalAnimation()
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)
})
}
return
}
@@ -563,13 +574,6 @@ 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)")
@@ -580,7 +584,6 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
if grabberAreaFrame.contains(location) {
initialScrollOffset = scrollView.contentOffset
} else {
fitToBounds(scrollView: scrollView)
settle(scrollView: scrollView)
initialScrollOffset = scrollView.contentOffsetZero
}
@@ -6,4 +6,26 @@
import UIKit
/// A view that presents a backdrop interface behind a floating panel.
public class FloatingPanelBackdropView: UIView { }
public class FloatingPanelBackdropView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
setUp()
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setUp()
}
private func setUp() {
layer.backgroundColor = UIColor.black.cgColor
}
@objc dynamic public override var backgroundColor: UIColor? {
get {
guard let color = layer.backgroundColor else { return nil }
return UIColor(cgColor: color)
}
set { layer.backgroundColor = newValue?.cgColor }
}
}
+13 -17
View File
@@ -196,7 +196,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 +207,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,9 +216,14 @@ 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)
self.prepare(for: newCollection)
// Change layout for a new trait collection
reloadLayout(for: newCollection)
setUpLayout()
floatingPanel.behavior = fetchBehavior(for: newCollection)
}
open override func viewWillDisappear(_ animated: Bool) {
@@ -226,15 +231,6 @@ 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 {
@@ -261,7 +257,7 @@ open class FloatingPanelController: UIViewController, UIScrollViewDelegate, UIGe
// Prevent an infinite loop on iOS 10: setUpLayout() -> viewDidLayoutSubviews() -> setUpLayout()
preSafeAreaInsets = safeAreaInsets
activateLayout()
setUpLayout()
switch contentInsetAdjustmentBehavior {
case .always:
@@ -286,7 +282,7 @@ open class FloatingPanelController: UIViewController, UIScrollViewDelegate, UIGe
}
}
private func activateLayout() {
private func setUpLayout() {
// preserve the current content offset
let contentOffset = scrollView?.contentOffset
@@ -302,7 +298,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)
activateLayout()
setUpLayout()
if #available(iOS 11.0, *) {
// Must track the safeAreaInsets of `self.view` to update the layout.
@@ -517,7 +513,7 @@ open class FloatingPanelController: UIViewController, UIScrollViewDelegate, UIGe
/// animation block.
public func updateLayout() {
reloadLayout(for: traitCollection)
activateLayout()
setUpLayout()
}
/// Returns the y-coordinate of the point at the origin of the surface view.
@@ -15,7 +15,7 @@ public class FloatingPanelSurfaceView: UIView {
public let grabberHandle: GrabberHandleView = GrabberHandleView()
/// Offset of the grabber handle from the top
public var grabberTopPadding: CGFloat = 6.0 { didSet {
@objc dynamic public var grabberTopPadding: CGFloat = 6.0 { didSet {
setNeedsUpdateConstraints()
} }
@@ -25,10 +25,10 @@ public class FloatingPanelSurfaceView: UIView {
}
/// Grabber view width and height
public var grabberHandleWidth: CGFloat = 36.0 { didSet {
@objc dynamic public var grabberHandleWidth: CGFloat = 36.0 { didSet {
setNeedsUpdateConstraints()
} }
public var grabberHandleHeight: CGFloat = 5.0 { didSet {
@objc dynamic public var grabberHandleHeight: CGFloat = 5.0 { didSet {
setNeedsUpdateConstraints()
} }
@@ -48,7 +48,7 @@ public class FloatingPanelSurfaceView: UIView {
private var color: UIColor? = .white { didSet { setNeedsLayout() } }
var bottomOverflow: CGFloat = 0.0 // Must not call setNeedsLayout()
public override var backgroundColor: UIColor? {
@objc dynamic public override var backgroundColor: UIColor? {
get { return color }
set { color = newValue }
}
@@ -57,34 +57,34 @@ public class FloatingPanelSurfaceView: UIView {
///
/// `self.contentView` is masked with the top rounded corners automatically on iOS 11 and later.
/// On iOS 10, they are not automatically masked because of a UIVisualEffectView issue. See https://forums.developer.apple.com/thread/50854
public var cornerRadius: CGFloat {
@objc dynamic public var cornerRadius: CGFloat {
set { containerView.layer.cornerRadius = newValue; setNeedsLayout() }
get { return containerView.layer.cornerRadius }
}
/// A Boolean indicating whether the surface shadow is displayed.
public var shadowHidden: Bool = false { didSet { setNeedsLayout() } }
@objc dynamic public var shadowHidden: Bool = false { didSet { setNeedsLayout() } }
/// The color of the surface shadow.
public var shadowColor: UIColor = .black { didSet { setNeedsLayout() } }
@objc dynamic public var shadowColor: UIColor = .black { didSet { setNeedsLayout() } }
/// The offset (in points) of the surface shadow.
public var shadowOffset: CGSize = CGSize(width: 0.0, height: 1.0) { didSet { setNeedsLayout() } }
@objc dynamic public var shadowOffset: CGSize = CGSize(width: 0.0, height: 1.0) { didSet { setNeedsLayout() } }
/// The opacity of the surface shadow.
public var shadowOpacity: Float = 0.2 { didSet { setNeedsLayout() } }
@objc dynamic public var shadowOpacity: Float = 0.2 { didSet { setNeedsLayout() } }
/// The blur radius (in points) used to render the surface shadow.
public var shadowRadius: CGFloat = 3 { didSet { setNeedsLayout() } }
@objc dynamic public var shadowRadius: CGFloat = 3 { didSet { setNeedsLayout() } }
/// The width of the surface border.
public var borderColor: UIColor? { didSet { setNeedsLayout() } }
@objc dynamic public var borderColor: UIColor? { didSet { setNeedsLayout() } }
/// The color of the surface border.
public var borderWidth: CGFloat = 0.0 { didSet { setNeedsLayout() } }
@objc dynamic public var borderWidth: CGFloat = 0.0 { didSet { setNeedsLayout() } }
/// Offset of the container view from the top
public var containerTopInset: CGFloat = 0.0 { didSet {
@objc dynamic public var containerTopInset: CGFloat = 0.0 { didSet {
setNeedsUpdateConstraints()
} }
@@ -149,6 +149,7 @@ public class FloatingPanelSurfaceView: UIView {
}
public override func updateConstraints() {
super.updateConstraints()
containerViewTopInsetConstraint.constant = containerTopInset
containerViewHeightConstraint.constant = bottomOverflow
@@ -160,8 +161,6 @@ public class FloatingPanelSurfaceView: UIView {
grabberHandleTopConstraint.constant = grabberTopPadding
grabberHandleWidthConstraint.constant = grabberHandleWidth
grabberHandleHeightConstraint.constant = grabberHandleHeight
super.updateConstraints()
}
public override func layoutSubviews() {
+3 -1
View File
@@ -7,7 +7,9 @@ import UIKit
public class GrabberHandleView: UIView {
public var barColor = UIColor(displayP3Red: 0.76, green: 0.77, blue: 0.76, alpha: 1.0) { didSet { backgroundColor = barColor } }
@objc dynamic public var barColor = UIColor(displayP3Red: 0.76, green: 0.77, blue: 0.76, alpha: 1.0) {
didSet { backgroundColor = barColor }
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
+1 -1
View File
@@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.6.1</string>
<string>1.5.1</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
</dict>
-9
View File
@@ -124,12 +124,3 @@ 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,6 +28,7 @@ class FloatingPanelControllerTests: XCTestCase {
func test_addPanel() {
guard let rootVC = UIApplication.shared.keyWindow?.rootViewController else { fatalError() }
let fpc = FloatingPanelController()
fpc.addPanel(toParent: rootVC)
@@ -38,24 +39,6 @@ class FloatingPanelControllerTests: XCTestCase {
waitRunLoop(secs: 1.0)
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) {
-27
View File
@@ -1,27 +0,0 @@
// 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")]
)