FloatingPanel
FloatingPanel是一个简单易用的UI组件,旨在提供像苹果地图、快捷指令和股票应用那样的用户界面。该用户界面可以在主内容旁边显示相关内容和实用工具。
有关更多详细信息,请参阅API参考。
- 功能
- 要求
- 安装
- 入门
- 视图层次
- 用法
- 注意
- 维护者
- 许可协议
特性
- 简单的容器视图控制器
- 使用数字弹簧的流畅行为
- 滚动视图跟踪
- 移除交互
- 支持多面板
- 模态呈现
- 支持4个位置(顶部、左侧、底部、右侧)
- 1个或多个磁性锚点(完整、一半、末端等)
- 支持所有特征环境的布局(例如,横向方向)
- 常见UI元素:表面、背景和抓取把手
- 免受常见的自动布局和手势处理问题的影响
- 与Objective-C兼容
示例请在此处查找
要求
FloatingPanel使用Swift 5.0+编写,与iOS 11.0+兼容。
虽然它仍然支持iOS 10,但建议在iOS 11+上使用此库。
安装
CocoaPods
FloatingPanel可通过CocoaPods获取。要安装它,只需将以下行添加到您的Podfile中
pod 'FloatingPanel'
swift_versions
。
Carthage
对于 Carthage,将以下内容添加到你的 Cartfile
github "scenee/FloatingPanel"
Swift Package Manager
参考此文档。
入门
将浮動面板作為子視圖控制器添加
import UIKit
import FloatingPanel
class ViewController: UIViewController, FloatingPanelControllerDelegate {
var fpc: FloatingPanelController!
override func viewDidLoad() {
super.viewDidLoad()
// Initialize a `FloatingPanelController` object.
fpc = FloatingPanelController()
// Assign self as the delegate of the controller.
fpc.delegate = self // Optional
// Set a content view controller.
let contentVC = ContentViewController()
fpc.set(contentViewController: contentVC)
// Track a scroll view(or the siblings) in the content view controller.
fpc.track(scrollView: contentVC.tableView)
// Add and show the views managed by the `FloatingPanelController` object to self.view.
fpc.addPanel(toParent: self)
}
}
以模態形式显示浮動面板
let fpc = FloatingPanelController()
let contentVC = ...
fpc.set(contentViewController: contentVC)
fpc.isRemovalInteractionEnabled = true // Optional: Let it removable by a swipe-down
self.present(fpc, animated: true, completion: nil)
您可以从容器视图控制器中显示浮动面板,作为.overCurrentContext
样式的模式。
视图层次结构
FloatingPanelController
管理的视图层次结构如下。
FloatingPanelController.view (FloatingPanelPassThroughView)
├─ .backdropView (FloatingPanelBackdropView)
└─ .surfaceView (FloatingPanelSurfaceView)
├─ .containerView (UIView)
│ └─ .contentView (FloatingPanelController.contentViewController.view)
└─ .grabber (FloatingPanelGrabberView)
用法
使用您的视图层次结构显示/隐藏浮动面板
如果您需要更细粒度地控制浮动面板的显示和隐藏,您可以直接调用addPanel
和removePanelFromParent
方法。这些方法是针对FloatingPanel
的show
和hide
方法以及一些必需设置的便利封装。
使用FloatingPanelController
有两种方式:
- 只需将其加入层次结构一次,然后调用
show
和hide
方法,就可以让面板出现或消失。 - 当需要时将其加入层次结构,之后再移除。
以下示例展示了如何将控制器添加到您的UIViewController
中以及如何将其移除。请确保在移除前不要将相同的FloatingPanelController
加入层次结构中。
注意:self.
前缀不是必需的,也不建议使用。这里使用它是为了使使用到的函数来源更清晰。在您的代码中,self
是您自定义UIViewController
的一个实例。
// Add the floating panel view to the controller's view on top of other views.
self.view.addSubview(fpc.view)
// REQUIRED. It makes the floating panel view have the same size as the controller's view.
fpc.view.frame = self.view.bounds
// In addition, Auto Layout constraints are highly recommended.
// Constraint the fpc.view to all four edges of your controller's view.
// It makes the layout more robust on trait collection change.
fpc.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
fpc.view.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 0.0),
fpc.view.leftAnchor.constraint(equalTo: self.view.leftAnchor, constant: 0.0),
fpc.view.rightAnchor.constraint(equalTo: self.view.rightAnchor, constant: 0.0),
fpc.view.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: 0.0),
])
// Add the floating panel controller to the controller hierarchy.
self.addChild(fpc)
// Show the floating panel at the initial position defined in your `FloatingPanelLayout` object.
fpc.show(animated: true) {
// Inform the floating panel controller that the transition to the controller hierarchy has completed.
fpc.didMove(toParent: self)
}
如您所看到,添加FloatingPanelController
后,您可以调用fpc.show(animated: true) { }
来显示面板,以及使用fpc.hide(animated: true) { }
来隐藏它。
要从层次结构中移除FloatingPanelController
,请参照以下示例。
// Inform the panel controller that it will be removed from the hierarchy.
fpc.willMove(toParent: nil)
// Hide the floating panel.
fpc.hide(animated: true) {
// Remove the floating panel view from your controller's view.
fpc.view.removeFromSuperview()
// Remove the floating panel controller from the controller hierarchy.
fpc.removeFromParent()
}
当表面位置变化时缩放内容视图
指定 contentMode
为 .fitToBounds
,如果表面高度适合 FloatingPanelController.view
的边界,当表面位置变化时
fpc.contentMode = .fitToBounds
否则,FloatingPanelController
会通过最顶部位置的高度固定内容。
.fitToBounds
模式下,表面高度会根据用户交互变化,因此您有责任配置 Auto Layout 约束,以防止弹性表面高度破坏内容视图的布局。
FloatingPanelLayout
协议自定义布局
使用 更改初始布局
class ViewController: UIViewController, FloatingPanelControllerDelegate {
... {
fpc = FloatingPanelController(delegate: self)
fpc.layout = MyFloatingPanelLayout()
}
}
class MyFloatingPanelLayout: FloatingPanelLayout {
let position: FloatingPanelPosition = .bottom
let initialState: FloatingPanelState = .tip
let anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] = [
.full: FloatingPanelLayoutAnchor(absoluteInset: 16.0, edge: .top, referenceGuide: .safeArea),
.half: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .safeArea),
.tip: FloatingPanelLayoutAnchor(absoluteInset: 44.0, edge: .bottom, referenceGuide: .safeArea),
]
}
更新您的面板布局
有两种更新面板布局的方法。
- 直接手动设置
FloatingPanelController.layout
为新布局对象。
fpc.layout = MyPanelLayout()
fpc.invalidateLayout() // If needed
注意:如果您已经设置了 FloatingPanelController
实例的 delegate
属性,invalidateLayout()
将使用委托对象返回的布局对象覆盖 FloatingPanelController
的布局对象。
- 在两个
floatingPanel(_:layoutFor Nguyên)
委托中的一个中返回合适的布局对象。
class ViewController: UIViewController, FloatingPanelControllerDelegate {
...
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout {
return MyFloatingPanelLayout()
}
// OR
func floatingPanel(_ vc: FloatingPanelController, layoutFor size: CGSize) -> FloatingPanelLayout {
return MyFloatingPanelLayout()
}
}
支持你的景观布局
class ViewController: UIViewController, FloatingPanelControllerDelegate {
...
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout {
return (newCollection.verticalSizeClass == .compact) ? LandscapePanelLayout() : FloatingPanelBottomLayout()
}
}
class LandscapePanelLayout: FloatingPanelLayout {
let position: FloatingPanelPosition = .bottom
let initialState: FloatingPanelState = .tip
let anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] = [
.full: FloatingPanelLayoutAnchor(absoluteInset: 16.0, edge: .top, referenceGuide: .safeArea),
.tip: FloatingPanelLayoutAnchor(absoluteInset: 69.0, edge: .bottom, referenceGuide: .safeArea),
]
func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint] {
return [
surfaceView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 8.0),
surfaceView.widthAnchor.constraint(equalToConstant: 291),
]
}
}
在面板布局中使用内容的固有大小
- 使用固有高度大小来布局内容视图。例如,参见"详情视图控制器场景"/"固有视图控制器场景",在Main.storyboard中。'Stack View.bottom'约束决定了固有高度。
- 使用
FloatingPanelIntrinsicLayoutAnchor
指定布局锚点。
class IntrinsicPanelLayout: FloatingPanelLayout {
let position: FloatingPanelPosition = .bottom
let initialState: FloatingPanelState = .full
let anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] = [
.full: FloatingPanelIntrinsicLayoutAnchor(absoluteOffset: 0, referenceGuide: .safeArea),
.half: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 0.5, referenceGuide: .safeArea),
]
...
}
FloatingPanelIntrinsicLayout
在v1版本已被弃用。
通过FloatingPanelController.view框的内边距指定每个状态的一个锚点
在锚点中使用.superview
参考指南。
class MyFullScreenLayout: FloatingPanelLayout {
...
let anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] = [
.full: FloatingPanelLayoutAnchor(absoluteInset: 16.0, edge: .top, referenceGuide: .superview),
.half: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .superview),
.tip: FloatingPanelLayoutAnchor(absoluteInset: 44.0, edge: .bottom, referenceGuide: .superview),
]
}
FloatingPanelFullScreenLayout
在v1版本已被弃用。
改变背景透明度
您可以通过每个状态(.full
、.half
和 .tip
)的 FloatingPanelLayout.backdropAlpha(for:)
方法改变背景透明度。
例如,如果面板看起来在.half
状态时背景视图不存在,就是时候实现backdropAlpha API并为以下状态返回一个值了。
class MyPanelLayout: FloatingPanelLayout {
func backdropAlpha(for state: FloatingPanelState) -> CGFloat {
switch state {
case .full, .half: return 0.3
default: return 0.0
}
}
}
使用自定义面板状态
您能够定义自定义面板状态并像以下示例那样使用它们。
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),
]
}
}
FloatingPanelBehavior
协议自定义行为
使用修改您的浮动面板交互
class ViewController: UIViewController, FloatingPanelControllerDelegate {
...
func viewDidLoad() {
...
fpc.behavior = CustomPanelBehavior()
}
}
class CustomPanelBehavior: FloatingPanelBehavior {
let springDecelerationRate = UIScrollView.DecelerationRate.fast.rawValue + 0.02
let springResponseTime = 0.4
func shouldProjectMomentum(_ fpc: FloatingPanelController, to proposedTargetPosition: FloatingPanelState) -> Bool {
return true
}
}
floatingPanel(_ vc:behaviorFor:)
在v1版本中已弃用。
在面板边缘激活橡皮筋效果
class MyPanelBehavior: FloatingPanelBehavior {
...
func allowsRubberBanding(for edge: UIRectEdge) -> Bool {
return true
}
}
管理平移手势动量的投影
这允许完整的投影面板行为。例如,用户可以从尖端向上滑动面板到靠近尖端的整个位置。
class MyPanelBehavior: FloatingPanelBehavior {
...
func shouldProjectMomentum(_ fpc: FloatingPanelController, to proposedTargetPosition: FloatingPanelPosition) -> Bool {
return true
}
}
指定面板移动的边界
FloatingPanelController.surfaceLocation
在 floatingPanelDidMove(_:)
委托方法中的行为类似于 UIScrollView.contentOffset
在 scrollViewDidScroll(_:)
中。因此,您可以按以下方式指定面板移动的边界。
func floatingPanelDidMove(_ vc: FloatingPanelController) {
if vc.isAttracting == false {
let loc = vc.surfaceLocation
let minY = vc.surfaceLocation(for: .full).y - 6.0
let maxY = vc.surfaceLocation(for: .tip).y + 6.0
vc.surfaceLocation = CGPoint(x: loc.x, y: min(max(loc.y, minY), maxY))
}
}
{top,bottom}InteractionBuffer
属性已被从 FloatingPanelLayout
中移除。
自定义表面设计
修改你的表面外观
// Create a new appearance.
let appearance = SurfaceAppearance()
// Define shadows
let shadow = SurfaceAppearance.Shadow()
shadow.color = UIColor.black
shadow.offset = CGSize(width: 0, height: 16)
shadow.radius = 16
shadow.spread = 8
appearance.shadows = [shadow]
// Define corner radius and background color
appearance.cornerRadius = 8.0
appearance.backgroundColor = .clear
// Set the new appearance
fpc.surfaceView.appearance = appearance
使用自定义捕获器句柄
let myGrabberHandleView = MyGrabberHandleView()
fpc.surfaceView.grabberHandle.isHidden = true
fpc.surfaceView.addSubview(myGrabberHandleView)
自定义捕获器句柄布局
fpc.surfaceView.grabberHandlePadding = 10.0
fpc.surfaceView.grabberHandleSize = .init(width: 44.0, height: 12.0)
自定义从表面边缘的内容内边距
fpc.surfaceView.contentPadding = .init(top: 20, left: 20, bottom: 20, right: 20)
自定义表面边缘的边距
fpc.surfaceView.containerMargins = .init(top: 20.0, left: 16.0, bottom: 16.0, right: 16.0)
此功能可应用于以下两种类型的面板
- 类似Facebook/Slack的面板,其表面顶部边距与捕获器句柄分开。
- 用于显示AirPods信息等的iOS原生面板。
自定义手势
阻止面板交互
您可以直接禁用平移手势识别器
fpc.panGestureRecognizer.isEnabled = false
或者使用这个 FloatingPanelControllerDelegate
方法。
func floatingPanelShouldBeginDragging(_ vc: FloatingPanelController) -> Bool {
return aCondition ? false : true
}
向表面视图添加点击手势
override func viewDidLoad() {
...
let surfaceTapGesture = UITapGestureRecognizer(target: self, action: #selector(handleSurface(tapGesture:)))
fpc.surfaceView.addGestureRecognizer(surfaceTapGesture)
surfaceTapGesture.isEnabled = (fpc.position == .tip)
}
// Enable `surfaceTapGesture` only at `tip` state
func floatingPanelDidChangeState(_ vc: FloatingPanelController) {
surfaceTapGesture.isEnabled = (vc.position == .tip)
}
FloatingPanelController.panGestureRecognizer
的代理方法
中断 如果您将 FloatingPanelController.panGestureRecognizer.delegateProxy
设置为继承自 UIGestureRecognizerDelegate
的对象,则它将覆盖平移手势识别器的代理方法。
class MyGestureRecognizerDelegate: UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return false
}
}
class ViewController: UIViewController {
let myGestureDelegate = MyGestureRecognizerDelegate()
func setUpFpc() {
....
fpc.panGestureRecognizer.delegateProxy = myGestureDelegate
}
为详细信息创建一个附加浮动面板
override func viewDidLoad() {
// Setup Search panel
self.searchPanelVC = FloatingPanelController()
let searchVC = SearchViewController()
self.searchPanelVC.set(contentViewController: searchVC)
self.searchPanelVC.track(scrollView: contentVC.tableView)
self.searchPanelVC.addPanel(toParent: self)
// Setup Detail panel
self.detailPanelVC = FloatingPanelController()
let contentVC = ContentViewController()
self.detailPanelVC.set(contentViewController: contentVC)
self.detailPanelVC.track(scrollView: contentVC.scrollView)
self.detailPanelVC.addPanel(toParent: self)
}
使用动画移动位置
以下示例中,我将浮动面板移至全位置或半位置,就像在苹果地图中打开或关闭搜索栏一样。
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
...
fpc.move(to: .half, animated: true)
}
func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
...
fpc.move(to: .full, animated: true)
}
您还可以使用视图动画来移动面板。
UIView.animate(withDuration: 0.25) {
self.fpc.move(to: .half, animated: false)
}
与浮动面板行为一起工作内容
class ViewController: UIViewController, FloatingPanelControllerDelegate {
...
func floatingPanelWillBeginDragging(_ vc: FloatingPanelController) {
if vc.position == .full {
searchVC.searchBar.showsCancelButton = false
searchVC.searchBar.resignFirstResponder()
}
}
func floatingPanelWillEndDragging(_ vc: FloatingPanelController, withVelocity velocity: CGPoint, targetState: UnsafeMutablePointer<FloatingPanelState>) {
if targetState.pointee != .full {
searchVC.hideHeader()
}
}
}
启用背景视图的点击消失操作
默认情况下禁用点击消失操作。因此,需要按照以下步骤启用。
fpc.backdropView.dismissalTapGestureRecognizer.isEnabled = true
备注
FloatingPanelController
的内容视图控制器跳转到 '显示' 或 '显示详情' Segues
从 从内容视图控制器跳转到 '显示' 或 '显示详情' Segues 将由添加浮动面板的视图控制器(以下称为主 VC)来管理。因为浮动面板只是主 VC 的子视图(除模态之外)。
FloatingPanelController
没有像 UINavigationController
那样管理视图控制器堆栈的方法。如果是这样,那将非常复杂,并且界面将变成 UINavigationController
。这个组件不应该承担管理堆栈的责任。
顺便说一句,内容视图控制器可以使用 present(_:animated:completion:)
方法或 '模态出现' Segues 以模态形式显示视图控制器。
然而,有时你想通过另一个浮动面板显示 '显示' 或 '显示详情' Segues 的目标视图控制器。你可以覆盖主 VC 中的 show(_:sender)
方法!
这里有一个例子。
class ViewController: UIViewController {
var fpc: FloatingPanelController!
var secondFpc: FloatingPanelController!
...
override func show(_ vc: UIViewController, sender: Any?) {
secondFpc = FloatingPanelController()
secondFpc.set(contentViewController: vc)
secondFpc.addPanel(toParent: self)
}
}
FloatingPanelController
对象将 show(_:sender)
的动作代理给主 VC。这就是为什么主 VC 可以处理 '显示' 或 '显示详情' Segues 的目标视图控制器,并且你可以将 show(_:sender)
钩到显示二级浮动面板,并将目标视图控制器设置为内容。
这是一种将浮动面板与内容 VC 解耦的好方法。
UISearchController 问题
UISearchController
因系统设计的原因不能与 FloatingPanelController
一起使用。
因为 UISearchController
用户与搜索栏交互时,会自动以模态形式呈现自己,然后它在其显示时将其父视图切换到自身管理的视图。因此,FloatingPanelController
在其活动状态时不能控制搜索栏,正如您可以从以下链接中的截图看到这里。
FloatingPanelSurfaceView的iOS 10问题
- 在iOS 10上,由于UIVisualEffectView的问题,
FloatingPanelSurfaceView.cornerRadius
属性并不会自动为顶部边缘添加圆角。请参阅https://forums.developer.apple.com/thread/50854。因此,您需要为您的内容绘制顶部圆角。在Examples/Maps中有一个示例。
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if #available(iOS 10, *) {
visualEffectView.layer.cornerRadius = 9.0
visualEffectView.clipsToBounds = true
}
}
- 如果您设置了
FloatingPanelSurfaceView.backgroundColor
的清除颜色,请注意当内容在全位置弹跳时的底部溢出情况。为了避免这种情况,您需要扩展您的内容。例如,请参阅Example/Maps应用程序在Main.storyboard中UIVisualEffectView
的Auto Layout设置。
维护者
山本真 [email protected] | @scenee
许可证
FloatingPanel遵循MIT许可。有关更多信息,请参阅LICENSE文件。