OverlayContainer 3.5.2

OverlayContainer 3.5.2

Gaétan Zanella维护。



  • gaetanzanella

OverlayContainer 是一个用 Swift 编写的 UI 库。它使得开发基于遮罩的界面变得更加容易,例如 Apple Maps、Stocks 或 Shortcuts 应用程序中展示的界面。

Platform Swift4 Swift5 CocoaPods Carthage Build Status License


⚠️在 iOS 15 中,请在 OverlayContainer 之前考虑使用 UISheetPresentationController


OverlayContainer 尽量保持轻量和不会干扰。布局和 UI 定制由您完成,以避免破坏项目。

它完美地模仿了 Siri Shortcuts 应用中展示的遮罩。有关详细信息,请参阅 这篇文章

  • 无限制的凹槽
  • 可以在运行时修改凹槽
  • 适应任何自定义布局
  • 橡皮筋效果
  • 动画和目标凹槽策略完全可定制
  • 单元测试

查看提供的示例以获取帮助或直接提问。



用法

设置

库的主要组件是OverlayContainerViewController。它定义了一个区域,在该区域内,一个名为覆盖视图控制器的视图控制器可以被拖动上下,隐藏或显示其下方的功能。

OverlayContainer将其viewControllers数组中的最后一个视图控制器用作覆盖视图控制器。如果有,它将其他视图控制器堆叠在最上面,并将它们添加到覆盖视图控制器下方。

启动顺序可能如下所示:

let mapsController = MapsViewController()
let searchController = SearchViewController()

let containerController = OverlayContainerViewController()
containerController.delegate = self
containerController.viewControllers = [
    mapsController,
    searchController
]

window?.rootViewController = containerController

仅指定一个视图控制器是完全有效的。例如,在MapsLikeViewController中,覆盖视图只覆盖了其内容的部分。

覆盖容器视图控制器至少需要一个凹口。通过实现OverlayContainerViewControllerDelegate来指定所需凹口数量

enum OverlayNotch: Int, CaseIterable {
    case minimum, medium, maximum
}

func numberOfNotches(in containerViewController: OverlayContainerViewController) -> Int {
    return OverlayNotch.allCases.count
}

func overlayContainerViewController(_ containerViewController: OverlayContainerViewController,
                                    heightForNotchAt index: Int,
                                    availableSpace: CGFloat) -> CGFloat {
    switch OverlayNotch.allCases[index] {
        case .maximum:
            return availableSpace * 3 / 4
        case .medium:
            return availableSpace / 2
        case .minimum:
            return availableSpace * 1 / 4
    }
}

覆盖样式

覆盖样式定义了覆盖视图控制器如何被约束在OverlayContainerViewController中。

enum OverlayStyle {
    case flexibleHeight
    case rigid
    case expandableHeight // default
}

let overlayContainer = OverlayContainerViewController(style: .rigid)
  • 刚性

rigid

覆盖视图控制器将以等于最高凹口的宽度进行约束。直到用户将其拖到这个凹口,覆盖视图才不会完全可见。

  • 可变高度

flexible

覆盖视图控制器将不受高度约束。当用户上下拖动时,它将根据需要进行增长和收缩。不过,请注意,当用户拖动覆盖视图时,视图可能会执行一些额外的布局计算。特别是对于表格视图和集合视图:当其框架变化时,一些单元格可能会被排队或移除。如果遇到性能问题,请尝试使用.rigid

请注意,始终提供比覆盖视图的内票内容更高的最小高度。

可变高度

  • 可扩展高度

expandable

覆盖视图控制器将以大于或等于最高凹口的宽度进行约束。如果覆盖视图超越了最高凹口(如果平移函数或动画控制器允许),其高度将扩展。

滚动视图支持

容器视图控制器可以协调滚动视图的滚动与覆盖平移。

scrollToTranslation

使用专门的委托方法

func overlayContainerViewController(_ containerViewController: OverlayContainerViewController,
                                    scrollViewDrivingOverlay overlayViewController: UIViewController) -> UIScrollView? {
    return (overlayViewController as? DetailViewController)?.tableView
}

或直接设置专门的属性

let containerController = OverlayContainerViewController()
containerController.drivingScrollView = myScrollView

确保将UIScrollView.alwaysBounceVertical设置为true,这样无论内容大小如何,滚动视图都将始终滚动。

拖动手势支持

容器视图控制器会识别其自身视图上的滑动操作。使用专门的代理方法检查指定的起始滑动操作位置是否对应于您的自定义覆盖层中的一个可抓取的视图。

func overlayContainerViewController(_ containerViewController: OverlayContainerViewController,
                                    shouldStartDraggingOverlay overlayViewController: UIViewController,
                                    at point: CGPoint,
                                    in coordinateSpace: UICoordinateSpace) -> Bool {
    guard let header = (overlayViewController as? DetailViewController)?.header else {
        return false
    }
    let convertedPoint = coordinateSpace.convert(point, to: header)
    return header.bounds.contains(convertedPoint)
}

跟踪覆盖层

您可以使用专门的代理方法来跟踪覆盖层的运动

  • 翻译开始

通知委托,当用户开始拖动覆盖层视图控制器时。

func overlayContainerViewController(_ containerViewController: OverlayContainerViewController,
                                    willStartDraggingOverlay overlayViewController: UIViewController)
  • 翻译结束

通知委托,当用户以指定的速度完成拖动覆盖层视图控制器时。

func overlayContainerViewController(_ containerViewController: OverlayContainerViewController,
                                    willEndDraggingOverlay overlayViewController: UIViewController,
                                    atVelocity velocity: CGPoint)
  • 翻译进行中

通知委托,当容器即将将覆盖层视图控制器移动到指定的凹槽时。

在某些情况下,覆盖层视图控制器可能无法成功到达指定的凹槽。例如,如果用户取消翻译。如果您需要在每次翻译成功时都收到通知,请使用 overlayContainerViewController(_:didMove:toNotchAt:)

func overlayContainerViewController(_ containerViewController: OverlayContainerViewController,
                                    willMoveOverlay overlayViewController: UIViewController,
                                    toNotchAt index: Int)

通知委托,当容器已将覆盖层视图控制器移动到指定的凹槽时。

func overlayContainerViewController(_ containerViewController: OverlayContainerViewController,
                                    didMoveOverlay overlayViewController: UIViewController,
                                    toNotchAt index: Int)

通知委托,每当覆盖层视图控制器即将进行翻译时。

委托通常实现此方法以在覆盖层视图控制器的翻译期间进行协调。

func overlayContainerViewController(_ containerViewController: OverlayContainerViewController,
                                    willTranslateOverlay overlayViewController: UIViewController,
                                    transitionCoordinator: OverlayContainerTransitionCoordinator)

转换协调器提供了关于即将启动的翻译的信息

/// A Boolean value that indicates whether the user is current dragging the overlay.
var isDragging: Bool { get }

/// The overlay velocity.
var velocity: CGPoint { get }

/// The current translation height.
var overlayTranslationHeight: CGFloat { get }

/// The notch indexes.
var notchIndexes: Range<Int> { get }

/// The reachable indexes. Some indexes might be disabled by the `canReachNotchAt` delegate method.
var reachableIndexes: [Int] { get }

/// Returns the height of the specified notch.
func height(forNotchAt index: Int) -> CGFloat

/// A Boolean value indicating whether the transition is explicitly animated.
var isAnimated: Bool { get }

/// A Boolean value indicating whether the transition was cancelled.
var isCancelled: Bool { get }

/// The overlay height the container expects to reach.
var targetTranslationHeight: CGFloat { get }

并允许您在它旁边添加动画

transitionCoordinator.animate(alongsideTransition: { context in
    // ...
}, completion: nil)

示例

要测试示例,请打开 OverlayContainer.xcworkspace 并运行 OverlayContainer_Example 目标。

AppDelegate 中选择您想显示的布局

  • MapsLikeViewController:一种可旋转时调整其层级的自定义布局。

Maps

  • ShortcutsLikeViewController:一种在特徵集合变化时会调整其层级的自定义布局:从常规环境中的 UISplitViewController 移动到紧凑环境中的简单 StackViewController。在 iPad Pro 上查看。

Shortcuts

高级用法

多个覆盖层

OverlayContainer 不提供内置视图控制器导航管理。它专注于覆盖层翻译。

然而,在项目中,有一个示例,展示了如何叠加多个视图,就像在 Apple Maps 应用中一样。它是基于 UINavigationController 和其代理的自定义实现。

// MARK: - UINavigationControllerDelegate

func navigationController(_ navigationController: UINavigationController,
                          animationControllerFor operation: UINavigationController.Operation,
                          from fromVC: UIViewController,
                          to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
    return OverlayNavigationAnimationController(operation: operation)
}

func navigationController(_ navigationController: UINavigationController,
                          didShow viewController: UIViewController,
                          animated: Bool) {
    overlayController.drivingScrollView = (viewController as? SearchViewController)?.tableView
}

OverlayNavigationAnimationController 调整了 UINavigationController 的原生行为:它将从屏幕底部滑动推入的视图控制器。您可以根据需要添加阴影和修改动画曲线。唯一的限制是您不能在一个 UINavigationController 内部推入另一个 UINavigationController

展示叠加容器

叠加视图控制器的转换可以与其容器的展示状态相关联。通过继承 OverlayContainerPresentationController 来在展示内容中发生叠加转换时接收通知,或者使用内置的 OverlayContainerSheetPresentationController 类。

一个常用的使用场景是复制 UIActivityViewController 的展示样式。 ActivityControllerPresentationLikeViewController 提供了它的基本实现。

func displayActivityLikeViewController() {
    let container = OverlayContainerViewController()
    container.viewControllers = [MyActivityViewController()]
    container.transitioningDelegate = self
    container.modalPresentationStyle = .custom
    present(container, animated: true, completion: nil)
}

// MARK: - UIViewControllerTransitioningDelegate

func presentationController(forPresented presented: UIViewController,
                            presenting: UIViewController?,
                            source: UIViewController) -> UIPresentationController? {
    return OverlayContainerSheetPresentationController(
        presentedViewController: presented,
        presenting: presenting
    )
}

如果用户点击背景内容或快速拖动叠加内容,容器控制器将自动关闭。

启用 & 禁用缺口

OverlayContainer 提供了一种简单的方法来动态启用和禁用缺口。一个常见的用例是显示和隐藏叠加内容。 ShowOverlayExampleViewController 提供了其基本实现。

var showsOverlay = false

func showOrHideOverlay() {
    showsOverlay.toggle()
    let targetNotch: Notch = showsOverlay ? .med : .hidden
    overlayContainerController.moveOverlay(toNotchAt: targetNotch.rawValue, animated: true)
}

// MARK: - OverlayContainerViewControllerDelegate

func overlayContainerViewController(_ containerViewController: OverlayContainerViewController,
                                    heightForNotchAt index: Int,
                                    availableSpace: CGFloat) -> CGFloat {
    switch Notch.allCases[index] {
    case .max:
        return ...
    case .med:
        return ...
    case .hidden:
        return 0
    }
}

func overlayContainerViewController(_ containerViewController: OverlayContainerViewController,
                                    canReachNotchAt index: Int,
                                    forOverlay overlayViewController: UIViewController) -> Bool {
    switch Notch.allCases[index] {
    case .max:
        return showsOverlay
    case .med:
        return showsOverlay
    case .hidden:
        return !showsOverlay
    }
}

确保使用 rigid 叠加样式,如果内容不能被压平。

背景视图

使用专属的代理方法将叠加移动与视图的方面协调一致。请参阅 背景视图示例

backdrop

func overlayContainerViewController(_ containerViewController: OverlayContainerViewController,
                                    willTranslateOverlay overlayViewController: UIViewController,
                                    transitionCoordinator: OverlayContainerTransitionCoordinator) {
    transitionCoordinator.animate(alongsideTransition: { [weak self] context in
        self?.backdropViewController.view.alpha = context.translationProgress()
    }, completion: nil)
}

安全区域问题

在使用安全区域时请小心。正如在WWDC "UIKit:适配各种尺寸和形状的应用"视频中所描述,如果你的视图超出了屏幕界限,安全区域的填充将不会更新。在使用OverlayStyle.expandableHeight时尤其如此:当覆盖层超出屏幕底部限制时,它的安全区域将不会更新。

正确处理安全区域的最简单方法是使用容器的safeAreaInsets计算您的刘海高度,并在您的覆盖视图中避免使用safeAreaLayoutGuide

func overlayContainerViewController(_ containerViewController: OverlayContainerViewController,
                                    heightForNotchAt index: Int,
                                    availableSpace: CGFloat) -> CGFloat {
    let bottomInset = containerViewController.view.safeAreaInsets.bottom
    switch OverlayNotch.allCases[index] {

        // ...

        case .minimum:
            return bottomInset + 100
    }
}

如果您依赖不忽略安全区域的原生组件(如UINavigationBar),请使用OverlayStyle.flexibleHeight样式。

自定义翻译

采用OverlayTranslationFunction修改用户手指翻译与实际覆盖翻译之间的关系。

默认情况下,覆盖容器使用一个提供弹力效果的RubberBandOverlayTranslationFunction

rubberBand

func overlayContainerViewController(_ containerViewController: OverlayContainerViewController,
                                    overlayTranslationFunctionForOverlay overlayViewController: UIViewController) -> OverlayTranslationFunction? {
    let function = RubberBandOverlayTranslationFunction()
    function.factor = 0.7
    function.bouncesAtMinimumHeight = false
    return function
}

自定义翻译动画

采用OverlayTranslationTargetNotchPolicy & OverlayAnimatedTransitioning协议来定义用户释放触摸后覆盖应该去哪里,以及如何进行动画。

默认情况下,覆盖容器使用模拟弹簧行为的SpringOverlayTranslationAnimationController。相关的目标缺口策略RushingForwardTargetNotchPolicy将在用户的手指达到一定程度速度时始终尝试向前移动。如果用户移动得太快,它也可能决定跳过一些缺口。

调整提供的实现或实现我们自己的对象以修改覆盖翻译行为。

animations

func overlayTargetNotchPolicy(for overlayViewController: UIViewController) -> OverlayTranslationTargetNotchPolicy? {
    let policy = RushingForwardTargetNotchPolicy()
    policy.minimumVelocity = 0
    return policy
}

func animationController(for overlayViewController: UIViewController) -> OverlayAnimatedTransitioning? {
    let controller = SpringOverlayTranslationAnimationController()
    controller.damping = 0.2
    return controller
}

重新加载缺口

您可以使用专用方法重新加载构建缺口所使用的所有数据。

func invalidateNotchHeights()

此方法不会立即重新加载凹槽高度。它只会清除当前容器的状态。因为凹槽的数量可能会变化,容器将使用其目标凹槽策略来确定去向。调用moveOverlay(toNotchAt:animated:)来覆盖此行为。

要求

OverlayContainer使用Swift 5编写。兼容iOS 10.0及以上。

安装

OverlayContainer可以通过CocoaPods获得。要安装它,只需将以下行添加到你的Podfile:

CocoaPods

pod 'OverlayContainer'

Carthage

在Cartfile中添加以下内容:

github "https://github.com/applidium/OverlayContainer"

Swift包管理器

OverlayContainer可以作为Swift包使用Xcode 11及以上版本进行安装。要安装它,请使用Xcode或将其添加到Package.swift文件中的依赖项:

.package(url: "https://github.com/applidium/OverlayContainer.git", from: "3.4.0")

SwiftUI

参考DynamicOverlay

作者

@gaetanzanella, [email protected]

许可

OverlayContainer 在 MIT 许可下可用。更多信息请参阅 LICENSE 文件。