LNPopupController

LNPopupController 是一个框架,用于将视图控制器作为其他视图控制器的弹出窗口呈现,类似于 Apple Music 和 Podcasts 的迷你播放器。

对于 SwiftUI,请查看 LNPopupUI 库

GitHub release GitHub stars GitHub license PayPal Donation Button

GitHub issues GitHub contributors Swift Package Manager compatible Carthage compatible

一旦使用内容视图控制器呈现了弹出栏,用户可以随时滑动或点击弹出栏来呈现弹出窗口。 完成后,用户可以通过滑动或点击弹出窗口关闭按钮来关闭弹出窗口。

该框架旨在具有很强的通用性,并在大多数情况下工作,因此它被实现为 UIViewController 的一个类别。 每个视图控制器都可以呈现一个弹出栏,停靠在底部视图上。 对于 UITabBarController 子类,默认的停靠视图是标签栏。 对于 UINavigationController 子类,默认的停靠视图是工具栏。 对于其他类,弹出栏显示在屏幕底部。 视图控制器子类可以提供它们自己的停靠视图。

该框架正确地维护了容器控制器的视图及其子控制器的安全区域插图,因为弹出栏被呈现和关闭。

显示在弹出栏上的信息是通过弹出项目对象(LNPopupItem 类的实例)动态提供的,这些对象与弹出内容视图控制器相关联。 要更改此信息,请更新视图控制器的弹出项目。

通常,建议在最外层的容器控制器上呈现弹出栏。 因此,如果您的视图控制器包含在导航控制器中,导航控制器又包含在标签栏控制器中,则建议在标签栏控制器上呈现弹出栏。

查看演示项目,了解该框架在各种场景中的许多常见用例。 它包含 Swift 和 Objective C 中的示例。

特性

添加到您的项目

Swift 包管理器

Swift 包管理器是将 LNPopupController 集成到您的项目中的推荐方法。

LNPopupController 支持 SPM 5.5 及以上版本。 要使用 SPM,您应该使用 Xcode 13 打开您的项目。 点击 File -> Swift Packages -> Add Package Dependency,输入 https://github.com/LeoNatan/LNPopupController。 选择您想要使用的版本。

您也可以手动将包添加到您的 Package.swift 文件中

.package(url: "https://github.com/LeoNatan/LNPopupController.git", from: "2.15.0")

以及目标中的依赖项

.target(name: "MyExampleApp", dependencies: ["LNPopupController"]),

Carthage

将以下内容添加到您的 Cartfile 中

github "LeoNatan/LNPopupController"

请确保您遵循 此处 的 Carthage 集成说明。

手动

LNPopupController.xcodeproj 项目拖到您的项目中,并将 LNPopupController.framework 添加到您的项目目标的 General 选项卡中的 Embedded Binaries 中。 Xcode 应该会自行对其他所有内容进行排序。

CocoaPods

不支持 CocoaPods。 这有很多原因。 请使用 Xcode 中的 Swift 包管理器来代替 CocoaPods。 您可以继续使用 CocoaPods 处理其他依赖项,并使用 Swift 包管理器处理 LNPopupController

使用框架

Swift

虽然该框架是用 Objective C 编写的,但它使用了现代 Objective C 语法,因此在 Swift 中使用该框架非常容易和直观。

项目集成

在您的项目中导入该模块

import LNPopupController

弹出项目

弹出项目应始终反映与其关联的视图控制器的弹出信息。 当视图控制器作为弹出内容控制器呈现时,弹出项目应提供标题和副标题以在弹出栏中显示。 此外,该项目可能包含其他按钮,用于使用 leadingBarButtonItemstrailingBarButtonItems 在弹出栏的前缘和/或后缘上显示。

管理弹出栏

要呈现弹出栏,请创建一个内容控制器,更新其弹出项目,并使用 presentPopupBar(with:animated:completion:) 呈现弹出栏。

let demoVC = DemoPopupContentViewController()
demoVC.view.backgroundColor = .red
demoVC.popupItem.title = "Hello World"
demoVC.popupItem.subtitle = "And a subtitle!"
demoVC.popupItem.progress = 0.34
    
tabBarController?.presentPopupBar(with: demoVC, animated: true, completion: nil)

您可以在呈现弹出栏时以及弹出窗口本身打开时呈现新的内容控制器。

要以编程方式打开和关闭弹出窗口,请分别使用 openPopup(animated:completion:)closePopup(animated:completion:)

tabBarController?.openPopup(animated: true, completion: nil)

或者,您可以使用 presentPopupBar(with:openPopup:animated:completion:) 在一个动画中呈现弹出栏并打开弹出窗口。

tabBarController?.presentPopupBar(with: demoVC, openPopup:true, animated: true, completion: nil)

要关闭弹出栏,请使用 dismissPopupBar(animated:completion:)

tabBarController?.dismissPopupBar(animated: true, completion: nil)

如果在关闭弹出栏时弹出窗口已打开,则也会关闭弹出内容。

弹出容器视图控制器

任何 UIViewController 子类都可以是弹出容器视图控制器。 弹出栏附加到底部停靠视图。 默认情况下,UITabBarControllerUINavigationController 子类将其底部栏返回为停靠视图,而其他控制器则返回视图底部的一个隐藏的 0pt 高度视图。 在您的子类中,重写 bottomDockingViewForPopupBardefaultFrameForBottomDockingView 并相应地返回您的视图和框架。 返回的视图必须附加到视图控制器的视图底部,否则结果是不确定的。

override var bottomDockingViewForPopupBar: UIView? {
  return myCoolBottomView
}

override var defaultFrameForBottomDockingView: CGRect {
  var bottomViewFrame = myCoolBottomView.frame
  
  if isMyCoolBottomViewHidden {
    bottomViewFrame.origin = CGPoint(x: bottomViewFrame.x, y: view.bounds.height)
  } else {
    bottomViewFrame.origin = CGPoint(x: bottomViewFrame.x, y: view.bounds.height - bottomViewFrame.height)
  }
  
  return bottomViewFrame
}

外观和行为

LNPopupController 提供了三种不同样式的弹出窗口外观,每种样式都基于 Apple 多年来推出的 Music 应用程序的外观。 弹出栏样式被标记为“floating”(浮动)、“prominent”(突出)和“compact”(紧凑),与相应的 Apple 样式相匹配。 弹出交互样式被标记为“snap”(吸附),用于现代样式的吸附弹出窗口,以及“drag”(拖动),用于 iOS 9 的交互式弹出交互。 弹出窗口关闭按钮样式被标记为“chevron”(chevron 箭头),用于现代样式的 chevron 箭头关闭按钮,以及“round”(圆形),用于 iOS 9 样式的关闭按钮。 对于每个样式,都有一个“default”(默认)样式,用于为当前平台和操作系统版本选择最合适的样式。

默认设置是

您还可以呈现完全自定义的弹出栏。 有关更多信息,请参见 自定义弹出栏

默认情况下,对于导航栏和标签栏容器控制器,弹出栏的外观根据底部栏的外观确定。 对于其他容器控制器,使用最适合当前环境的默认外观。

要禁用继承底部栏的外观,请将 inheritsAppearanceFromDockingView 属性设置为 false

栏样式

通过设置弹出栏的 barStyle 属性来实现自定义弹出栏样式。

navigationController?.popupBar.barStyle = .compact

交互样式

通过设置弹出窗口呈现容器控制器的 popupInteractionStyle 属性来实现自定义弹出窗口交互样式。

navigationController?.popupInteractionStyle = .drag

进度视图样式

通过设置弹出栏的 progressViewStyle 属性来实现自定义弹出栏进度视图样式。

navigationController?.popupBar.progressViewStyle = .top

要隐藏进度视图,请将 progressViewStyle 属性设置为 LNPopupBar.ProgressViewStyle.none





关闭按钮样式

通过设置弹出内容视图的 popupCloseButtonStyle 属性来实现自定义弹出窗口关闭按钮样式。

navigationController.popupContentView.popupCloseButtonStyle = .round

要隐藏弹出窗口关闭按钮,请将 popupCloseButtonStyle 属性设置为 LNPopupCloseButton.Style.none





文本跑马灯滚动

如果启用了文本跑马灯,为标题和/或副标题提供长文本将导致文本滚动。 否则,文本将被截断。

弹出栏自定义

LNPopupBar 公开了许多 API 来自定义默认弹出栏的外观。 使用 LNPopupBarAppearance 对象来定义栏的标准外观。

请记住将 inheritsAppearanceFromDockingView 属性设置为 false,否则您的某些自定义设置可能会被底部栏的外观覆盖。

let appearance = LNPopupBarAppearance()
appearance.titleTextAttributes = AttributeContainer()
    .font(UIFontMetrics(forTextStyle: .headline).scaledFont(for: UIFont(name: "Chalkduster", size: 14)!))
    .foregroundColor(UIColor.yellow)
appearance.subtitleTextAttributes = AttributeContainer()
    .font(UIFontMetrics(forTextStyle: .subheadline).scaledFont(for: UIFont(name: "Chalkduster", size: 12)!))
    .foregroundColor(UIColor.green)

let floatingBarBackgroundShadow = NSShadow()
floatingBarBackgroundShadow.shadowColor = UIColor.red
floatingBarBackgroundShadow.shadowOffset = .zero
floatingBarBackgroundShadow.shadowBlurRadius = 8.0
appearance.floatingBarBackgroundShadow = floatingBarBackgroundShadow

let imageShadow = NSShadow()
imageShadow.shadowColor = UIColor.yellow
imageShadow.shadowOffset = .zero
imageShadow.shadowBlurRadius = 3.0
appearance.imageShadow = imageShadow

if navigationController?.popupBar.barStyle == .floating {
    appearance.floatingBackgroundEffect = UIBlurEffect(style: .dark)
} else {
    appearance.backgroundEffect = UIBlurEffect(style: .dark)
    navigationController?.popupBar.inheritsAppearanceFromDockingView = false
}

navigationController?.popupBar.standardAppearance = appearance
navigationController?.popupBar.tintColor = .yellow

系统交互

过渡

导航栏和标签栏控制器支持 hidesBottomBarWhenPushed 属性。 当设置为 true 时,弹出栏将过渡到推送控制器的视图底部。 也支持设置 isToolbarHidden = true 并调用 setToolbarHidden(_:animated:)

标签栏侧边栏

从 iPadOS 18 开始,该框架支持 UITabBarController 侧边栏。 当侧边栏替换基础内容时,弹出栏会移开。



当侧边栏覆盖基础内容时,弹出栏会与内容一起变暗

状态栏管理

弹出内容视图控制器的状态栏管理受到尊重并在适当时应用。

主页指示器可见性控制受到尊重并在适当时应用。

上下文菜单交互

支持上下文菜单。 将 UIContextMenuInteraction 交互对象添加到弹出栏,它将按预期运行。

指针交互

支持指针交互,并且为系统栏样式提供了默认实现。

对于自定义弹出栏控制器,LNPopupCustomBarViewController 类实现了 UIPointerInteractionDelegate 协议。 在您的子类中实现该协议的方法,以实现自定义指针交互。

滚动边缘外观

从 iOS 15 开始,无论内容滚动位置如何,当呈现弹出栏时,工具栏和标签栏的滚动边缘外观都会自动禁用。 关闭弹出栏后,滚动边缘外观将恢复。

自定义弹出栏

该框架支持实现自定义弹出栏。

要实现自定义弹出栏,您可以继承 LNPopupCustomBarViewController

在您的 LNPopupCustomBarViewController 子类中,构建弹出栏的视图层次结构,并使用首选的弹出栏高度设置控制器的 preferredContentSize 属性。 重写任何 wantsDefaultTapGestureRecognizerwantsDefaultPanGestureRecognizer 和/或 wantsDefaultHighlightGestureRecognizer 属性,以禁用自定义弹出栏中的默认手势识别器功能。

在您的子类中,实现 popupItemDidUpdate() 方法以接收有关弹出内容视图控制器的项目更新的通知,或者当呈现新的弹出内容视图控制器时(带有新的弹出项目)。 您必须调用此方法的 super 实现。

最后,将弹出栏对象的 customBarViewController 属性设置为您的 LNPopupCustomBarViewController 子类的实例。 这将自动将栏样式更改为 LNPopupBar.Style.custom

包含的演示项目包括两个示例自定义弹出栏场景。

提示

只有当您需要与提供的标准弹出栏样式显著不同的设计时,才需要实现自定义弹出栏。 我们已经投入了大量的精力和心思,将这些弹出栏样式与 UIKit 系统集成,包括外观、感觉、过渡和交互。 自定义栏为您提供了一个空白的画布来实现您自己的栏,但如果您最终重新创建了一个与标准栏样式相似的栏设计,您很可能会失去多年来在标准实现中添加和完善的细微之处。 相反,请考虑使用许多自定义 API来调整标准栏样式以适应您的应用程序的设计。

ProMotion 支持

LNPopupController 完全支持 iPhone 和 iPad 上的 ProMotion。

对于 iPhone 13 Pro 及更高版本,您需要将 CADisableMinimumFrameDurationOnPhone 键添加到您的 Info.plist 文件中,并将其设置为 true。 有关更多信息,请参阅优化 iPhone 13 Pro 和 iPad Pro 的 ProMotion 刷新率。 如果缺少此键或设置为 falseLNPopupController 将在控制台中记录一条警告消息。

交互手势识别器

LNPopupContentView 通过 popupInteractionGestureRecognizer 属性公开对弹出窗口交互手势识别器的访问。 此手势识别器用于通过向上滑动或平移弹出栏(当弹出窗口关闭时)来打开弹出窗口,以及通过向下滑动或平移弹出内容视图(当弹出栏打开时)来关闭弹出窗口。

打开弹出窗口时,系统会查询弹出内容视图控制器的 viewForPopupInteractionGestureRecognizer 属性,以确定将交互手势识别器添加到哪个视图。 默认情况下,该属性返回内容控制器的根视图。 覆盖该属性的 getter 以更改此行为。

系统会尽最大努力与其他手势(包括系统手势、控件和滚动)配合。 当用户在弹出内容视图层次结构中滚动时,系统将尽最大努力不干扰用户的手势,并且仅在滚动内容的边缘做出反应。

对于垂直滚动内容,只有当用户滑动或拖动超过滚动内容的边缘时,弹出窗口才会关闭。

对于水平滚动内容,只有当用户向下滑动或拖动且没有水平滚动时,弹出窗口才会关闭。

对于双向滚动内容,仅支持 isDirectionalLockEnabled = true。 在这种情况下,如果满足以上两个条件,弹出窗口将关闭。

对于双向滚动内容,系统不会尝试在任何时候关闭弹出窗口。 用户仍然可以通过点击关闭按钮或在可滚动区域外滑动或拖动来关闭弹出窗口。

您可以实现交互手势识别器的委托,以影响其行为,例如当用户与弹出内容视图层次结构中的其他控件或视图交互时,阻止弹出窗口交互。

注意

如果在打开弹出窗口后禁用手势识别器,则必须监视弹出窗口的状态,并在用户关闭或通过代码关闭后重新启用手势识别器。 相反,请考虑实现手势识别器的委托并提供自定义逻辑以禁用交互。

完整的从右到左支持

该框架具有完整的从右到左支持。

默认情况下,弹出栏将遵循系统的用户界面布局方向,但会保留栏按钮项的顺序。 要自定义此行为,请修改弹出栏的 semanticContentAttributebarItemsSemanticContentAttribute 属性。

辅助功能

该框架支持辅助功能,并将遵循辅助功能标签、提示和值。 默认情况下,弹出栏的辅助功能标签是弹出项提供的标题和副标题。

要修改弹出栏的辅助功能标签和提示,请设置弹出内容视图控制器的 LNPopupItem 对象的 accessibilityLabelaccessibilityHint 属性。

demoVC.popupItem.accessibilityLabel = NSLocalizedString("Custom popup bar accessibility label", comment: "")
demoVC.popupItem.accessibilityHint = NSLocalizedString("Custom popup bar accessibility hint", comment: "")

要向按钮添加辅助功能标签和提示,请设置 UIBarButtonItem 对象的 accessibilityLabelaccessibilityHint 属性。

let upNext = UIBarButtonItem(image: UIImage(named: "next"), style: .plain, target: self, action: #selector(nextItem))
upNext.accessibilityLabel = NSLocalizedString("Up Next", comment: "")
upNext.accessibilityHint = NSLocalizedString("Double tap to show up next list", comment: "")

要修改弹出窗口关闭按钮的辅助功能标签和提示,请设置弹出窗口容器视图控制器的 LNPopupCloseButton 对象的 accessibilityLabelaccessibilityHint 属性。

tabBarController?.popupContentView.popupCloseButton.accessibilityLabel = NSLocalizedString("Custom popup close button accessibility label", comment: "")
tabBarController?.popupContentView.popupCloseButton.accessibilityHint = NSLocalizedString("Custom popup close button accessibility hint", comment: "")

要修改弹出栏进度视图的辅助功能标签和值,请设置弹出内容视图控制器的 LNPopupItem 对象的 accessibilityProgressLabelaccessibilityProgressValue 属性。

demoVC.popupItem.accessibilityImageLabel = NSLocalizedString("Custom image label", comment: "")
demoVC.popupItem.accessibilityProgressLabel = NSLocalizedString("Custom accessibility progress label", comment: "")
demoVC.popupItem.accessibilityProgressValue = "\(accessibilityDateComponentsFormatter.stringFromTimeInterval(NSTimeInterval(popupItem.progress) * totalTime)!) \(NSLocalizedString("of", comment: "")) \(accessibilityDateComponentsFormatter.stringFromTimeInterval(totalTime)!)"

注意

致谢

该框架使用

此外,演示项目使用

Star History

Star History Chart