如何模拟地图应用中的底部工作表?

qwe*_*rtz 179 uiscrollview uiview mapkit ios swift

任何人都可以告诉我如何在iOS 10中的新地图应用程序中模仿底页?

在Android中,您可以使用BottomSheet模仿此行为的模式,但我找不到类似iOS的内容.

这是一个带有内容插入的简单滚动视图,以便搜索栏位于底部吗?

我对iOS编程很新,所以如果有人可以帮助我创建这个布局,那将非常感激.

这就是我所说的"底页":

地图中折叠的底部工作表的屏幕截图

地图中展开的底部工作表的屏幕截图

Ahm*_*uty 288

我不知道新地图应用程序的底页是如何响应用户交互的.但您可以创建一个类似于屏幕截图中的自定义视图,并将其添加到主视图中.

我假设你知道如何:

1-通过故事板或使用xib文件创建视图控制器.

2-使用googleMaps或Apple的MapKit.

1-创建2个视图控制器,例如MapViewControllerBottomSheetViewController.第一个控制器将托管地图,第二个是底部工作表.

配置MapViewController

创建一个添加底部工作表视图的方法.

func addBottomSheetView() {
    // 1- Init bottomSheetVC
    let bottomSheetVC = BottomSheetViewController()

    // 2- Add bottomSheetVC as a child view 
    self.addChildViewController(bottomSheetVC)
    self.view.addSubview(bottomSheetVC.view)
    bottomSheetVC.didMoveToParentViewController(self)

    // 3- Adjust bottomSheet frame and initial position.
    let height = view.frame.height
    let width  = view.frame.width
    bottomSheetVC.view.frame = CGRectMake(0, self.view.frame.maxY, width, height)
}
Run Code Online (Sandbox Code Playgroud)

并在viewDidAppear方法中调用它:

override func viewDidAppear(animated: Bool) {
    super.viewDidAppear(animated)
    addBottomSheetView()
}
Run Code Online (Sandbox Code Playgroud)

配置BottomSheetViewController

1)准备背景

创建一种方法来添加模糊和活力效果

func prepareBackgroundView(){
    let blurEffect = UIBlurEffect.init(style: .Dark)
    let visualEffect = UIVisualEffectView.init(effect: blurEffect)
    let bluredView = UIVisualEffectView.init(effect: blurEffect)
    bluredView.contentView.addSubview(visualEffect)

    visualEffect.frame = UIScreen.mainScreen().bounds
    bluredView.frame = UIScreen.mainScreen().bounds

    view.insertSubview(bluredView, atIndex: 0)
}
Run Code Online (Sandbox Code Playgroud)

在viewWillAppear中调用此方法

override func viewWillAppear(animated: Bool) {
   super.viewWillAppear(animated)
   prepareBackgroundView()
}
Run Code Online (Sandbox Code Playgroud)

确保控制器的视图背景颜色为clearColor.

2)动画bottomSheet外观

override func viewDidAppear(animated: Bool) {
    super.viewDidAppear(animated)

    UIView.animateWithDuration(0.3) { [weak self] in
        let frame = self?.view.frame
        let yComponent = UIScreen.mainScreen().bounds.height - 200
        self?.view.frame = CGRectMake(0, yComponent, frame!.width, frame!.height)
    }
}
Run Code Online (Sandbox Code Playgroud)

3)根据需要修改你的xib.

4)在您的视图中添加Pan Gesture Recognizer.

在viewDidLoad方法中添加UIPanGestureRecognizer.

override func viewDidLoad() {
    super.viewDidLoad()

    let gesture = UIPanGestureRecognizer.init(target: self, action: #selector(BottomSheetViewController.panGesture))
    view.addGestureRecognizer(gesture)

}
Run Code Online (Sandbox Code Playgroud)

并实现您的手势行为:

func panGesture(recognizer: UIPanGestureRecognizer) {
    let translation = recognizer.translationInView(self.view)
    let y = self.view.frame.minY
    self.view.frame = CGRectMake(0, y + translation.y, view.frame.width, view.frame.height)
     recognizer.setTranslation(CGPointZero, inView: self.view)
}
Run Code Online (Sandbox Code Playgroud)

可滚动底板:

如果您的自定义视图是滚动视图或继承的任何其他视图,那么您有两个选项:

第一:

使用标题视图设计视图,并将panGesture添加到标题中.(糟糕的用户体验).

第二:

1 - 将panGesture添加到底部工作表视图.

2 - 实现UIGestureRecognizerDelegate并将panGesture委托设置为控制器.

3-实现shouldRecognizeSimultaneouslyWith delegate函数并在两种情况下禁用 scrollView isScrollEnabled属性:

  • 视图部分可见.
  • 视图完全可见,scrollView contentOffset属性为0,用户向下拖动视图.

否则启用滚动.

  func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
      let gesture = (gestureRecognizer as! UIPanGestureRecognizer)
      let direction = gesture.velocity(in: view).y

      let y = view.frame.minY
      if (y == fullView && tableView.contentOffset.y == 0 && direction > 0) || (y == partialView) {
          tableView.isScrollEnabled = false
      } else {
        tableView.isScrollEnabled = true
      }

      return false
  }
Run Code Online (Sandbox Code Playgroud)

注意

如果您将.allowUserInteraction设置为动画选项,就像在示例项目中一样,那么如果用户向上滚动,则需要在动画完成闭包上启用滚动.

示例项目

我创建了一个示例项目,在 repo 上有更多选项,可以为您提供有关如何自定义流的更好的见解.

在演示中,addBottomSheetView()函数控制哪个视图应该用作底部工作表.

示例项目截图

- 部分视图

在此输入图像描述

- FullView

在此输入图像描述

- 可滚动视图

在此输入图像描述

  • 我注意到Apple Maps底部工作表以一个平滑的动作处理从表单拖动手势到滚动视图滚动手势的转换,即用户不需要停止一个手势并开始一个新手势以便开始滚动视图.你的例子没有这样做.你对如何实现这一点有什么想法吗?谢谢,非常感谢您在回购中发布示例! (12认同)
  • 这实际上是childViews和subViews的一个很好的教程.谢谢 :) (3认同)
  • @nikhilnangia我刚刚使用另一个包含tableView的viewController“ ScrollableBottomSheetViewController.swift”更新了存储库。我将尽快更新答案。也检查一下https://github.com/AhmedElassuty/IOS-BottomSheet/issues/1 (3认同)
  • @RafaelaLourenço我很高兴您发现我的回答很有帮助!谢谢! (2认同)

Gaé*_*anZ 47

2018年12月4日更新

我发布了一个基于此解决方案的库https://github.com/applidium/ADOverlayContainer.

它模仿快捷方式应用程序的叠加层,其中包括:

  • 滚动和平移之间的平滑过渡
  • 橡皮筋效果
  • 自定义可拖动内容
  • 无限的缺口

以前的答案

我认为在建议的解决方案中没有对待一个重要的观点:滚动和翻译之间的过渡.

滚动和翻译之间的地图过渡

在地图中,您可能已经注意到,当tableView到达时OverlayContainerViewController,底部工作表会向上滑动或向下滑动.

这一点很棘手,因为当我们的平移手势开始翻译时,我们不能简单地启用/禁用滚动.它将停止滚动直到新的触摸开始.这是大多数提出的解决方案中的情况.

这是我尝试实施这一动议.

起点:地​​图应用

开始我们的调查,让我们直观地图的视图层次结构(启动地图上的模拟器和选择OverlayContainerViewControllerDelegate> contentOffset.y == 0> Debug在Xcode 9).

映射调试视图层次结构

它没有说明动作是如何工作的,但它帮助我理解了它的逻辑.您可以使用lldb和视图层次结构调试器.

我们的ViewController堆栈

让我们创建一个Maps ViewController体系结构的基本版本.

我们从Attach to process by PID or Name(我们的地图视图)开始:

let contentController = MapsViewController()
let overlayController = SearchViewController()

let containerController = OverlayContainerViewController()
containerController.delegate = self
containerController.viewControllers = [
    contentController,
    overlayController
]

window?.rootViewController = containerController
Run Code Online (Sandbox Code Playgroud)

我们把tableView放在一个专用的Maps:

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
    }
}
Run Code Online (Sandbox Code Playgroud)

现在,我们需要VC来嵌入叠加层并管理其翻译.为了简化问题,我们认为它可以将叠加层从一个静态点转换BackgroundViewController为另一个静态点UIViewController.

目前它只有一个公共方法来设置位置变化的动画,它有一个透明的视图:

class BackgroundViewController: UIViewController {
    override func loadView() {
        view = MKMapView()
    }
}
Run Code Online (Sandbox Code Playgroud)

最后我们需要一个ViewController来嵌入all:

class OverlayViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {

    lazy var tableView = UITableView()

    override func loadView() {
        view = tableView
        tableView.dataSource = self
        tableView.delegate = self
    }

    [...]
}
Run Code Online (Sandbox Code Playgroud)

在我们的AppDelegate中,我们的启动顺序如下:

enum OverlayPosition {
    case maximum, minimum
}

class OverlayContainerViewController: UIViewController {

    let overlayViewController: OverlayViewController
    var translatedViewHeightContraint = ...

    override func loadView() {
        view = UIView()
    }

    func moveOverlay(to position: OverlayPosition) {
        [...]
    }
}
Run Code Online (Sandbox Code Playgroud)

叠加翻译背后的困难

现在,如何翻译我们的叠加层?

大多数提出的解决方案使用专用的平移手势识别器,但我们实际上已经有一个:表视图的平移手势.此外,我们需要保持滚动和翻译同步,并OverlayPosition.maximum拥有我们需要的所有事件!

一个简单的实现将使用第二个pan Gesture并尝试OverlayPosition.minimum在发生转换时重置表视图:

class StackViewController: UIViewController {

    private var viewControllers: [UIViewController]

    override func viewDidLoad() {
        super.viewDidLoad()
        viewControllers.forEach { gz_addChild($0, in: view) }
    }
}
Run Code Online (Sandbox Code Playgroud)

但它不起作用.tableView UIScrollViewDelegate在其自己的平移手势识别器操作触发或调用其displayLink回调时更新它.我们的识别器不可能在成功覆盖之后触发contentOffset.我们唯一的机会是参与布局阶段(通过覆盖contentOffset滚动视图每帧的滚动视图调用)或响应contentOffset每次layoutSubviews修改时调用的委托方法.我们来试试吧.

翻译实施

我们向我们添加一个委托,didScroll将scrollview的事件发送给我们的翻译处理程序,contentOffset:

let overlay = OverlayViewController()
let containerViewController = OverlayContainerViewController(overlayViewController: overlay)
let backgroundViewController = BackgroundViewController()
window?.rootViewController = StackViewController(viewControllers: [backgroundViewController, containerViewController])
Run Code Online (Sandbox Code Playgroud)

在我们的容器中,我们使用枚举跟踪翻译:

func panGestureAction(_ recognizer: UIPanGestureRecognizer) {
    if isTranslating {
        tableView.contentOffset = .zero
    }
}
Run Code Online (Sandbox Code Playgroud)

当前位置计算如下:

protocol OverlayViewControllerDelegate: class {
    func scrollViewDidScroll(_ scrollView: UIScrollView)
    func scrollViewDidStopScrolling(_ scrollView: UIScrollView)
}

class OverlayViewController: UIViewController {

    [...]

    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        delegate?.scrollViewDidScroll(scrollView)
    }

    func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
        delegate?.scrollViewDidStopScrolling(scrollView)
    }
}
Run Code Online (Sandbox Code Playgroud)

我们需要3种方法来处理翻译:

第一个告诉我们是否需要开始翻译.

enum OverlayInFlightPosition {
    case minimum
    case maximum
    case progressing
}
Run Code Online (Sandbox Code Playgroud)

第二个执行翻译.它使用OverlayVCscrollView的平移手势的方法.

private var overlayInFlightPosition: OverlayInFlightPosition {
    let height = translatedViewHeightContraint.constant
    if height == maximumHeight {
        return .maximum
    } else if height == minimumHeight {
        return .minimum
    } else {
        return .progressing
    }
}
Run Code Online (Sandbox Code Playgroud)

第三个动画在用户松开手指时动画翻译结束.我们使用视图的速度和当前位置计算位置.

private func shouldTranslateView(following scrollView: UIScrollView) -> Bool {
    guard scrollView.isTracking else { return false }
    let offset = scrollView.contentOffset.y
    switch overlayInFlightPosition {
    case .maximum:
        return offset < 0
    case .minimum:
        return offset > 0
    case .progressing:
        return true
    }
}
Run Code Online (Sandbox Code Playgroud)

我们的overlay的委托实现看起来像:

private func translateView(following scrollView: UIScrollView) {
    scrollView.contentOffset = .zero
    let translation = translatedViewTargetHeight - scrollView.panGestureRecognizer.translation(in: view).y
    translatedViewHeightContraint.constant = max(
        Constant.minimumHeight,
        min(translation, Constant.maximumHeight)
    )
}
Run Code Online (Sandbox Code Playgroud)

最后的问题:调度叠加容器的触摸

翻译现在非常有效.但仍然存在最终问题:触摸不会传递到我们的背景视图.它们都被覆盖容器的视图拦截.我们无法设置OverlayContainerViewController,translation(in:)因为它还会禁用表视图中的交互.解决方案是地图应用程序中大量使用的解决方案isUserInteractionEnabled:

private func animateTranslationEnd() {
    let position: OverlayPosition =  // ... calculation based on the current overlay position & velocity
    moveOverlay(to: position)
}
Run Code Online (Sandbox Code Playgroud)

它将自己从响应者链中移除.

false:

class OverlayContainerViewController: UIViewController {

    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        guard shouldTranslateView(following: scrollView) else { return }
        translateView(following: scrollView)
    }

    func scrollViewDidStopScrolling(_ scrollView: UIScrollView) {
        // prevent scroll animation when the translation animation ends
        scrollView.isEnabled = false
        scrollView.isEnabled = true
        animateTranslationEnd()
    }
}
Run Code Online (Sandbox Code Playgroud)

结果

结果如下:

结果

你可以在这里找到代码.

如果您发现任何错误,请告诉我们!请注意,您的实现当然可以使用第二个平移手势,特别是如果您在叠加层中添加标题.

更新于08/08/18

我们可以替换PassThroughView使用 OverlayContainerViewController,而非scrollViewDidEndDragging/ willEndScrollingWithVelocity当用户结束拖动滚动:

class PassThroughView: UIView {
    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        let view = super.hitTest(point, with: event)
        if view == self {
            return nil
        }
        return view
    }
}
Run Code Online (Sandbox Code Playgroud)

我们可以使用弹簧动画并允许用户在动画时进行交互,以使运动更好:

override func loadView() {
    view = PassThroughView()
}
Run Code Online (Sandbox Code Playgroud)


小智 17

尝试滑轮:

Pulley是一个易于使用的抽屉库,用于模仿iOS 10的地图应用程序中的抽屉.它公开了一个简单的API,允许您使用任何UIViewController子类作为抽屉内容或主要内容.

滑轮预览

https://github.com/52inc/Pulley


SCE*_*NEE 9

您可以尝试我的答案https://github.com/SCENEE/FloatingPanel.它提供了一个容器视图控制器来显示"底部工作表"界面.

它易于使用,您不介意任何手势识别器处理!如果需要,您还可以在底部工作表中跟踪滚动视图(或兄弟视图).

这是一个简单的例子.请注意,您需要准备一个视图控制器,以在底部工作表中显示您的内容.

import UIKit
import FloatingPanel

class ViewController: UIViewController {
    var fpc: FloatingPanelController!

    override func viewDidLoad() {
        super.viewDidLoad()

        fpc = FloatingPanelController()

        // Add "bottom sheet" in self.view.
        fpc.add(toParent: self)

        // Add a view controller to display your contents in "bottom sheet".
        let contentVC = ContentViewController()
        fpc.set(contentViewController: contentVC)

        // Track a scroll view in "bottom sheet" content if needed.
        fpc.track(scrollView: contentVC.tableView)    
    }
    ...
}
Run Code Online (Sandbox Code Playgroud)

是另一个显示底部工作表以搜索Apple Maps等位置的示例代码.

样本应用程序,如Apple Maps


ugu*_*gur 6

以上对我都不起作用,因为同时平移tableview和外部视图无法顺利进行。因此,我编写了自己的代码来实现ios Maps应用程序中的预期行为。

https://github.com/OfTheWolf/UBottomSheet

底纸


pka*_*amb 6

2021 年的 iOS 15 增加了UISheetPresentationController,这是苹果首次公开发布的苹果地图风格的“底单”:

UISheetPresentationController

UISheetPresentationController 允许你将你的视图控制器呈现为一张表。在你展示你的视图控制器之前,用你想要的工作表的行为和外观配置它的工作表展示控制器。

片材展示控制器根据棘爪(片材自然放置的高度)指定片材的尺寸。棘爪允许纸张从其完全展开框架的一个边缘调整大小,而其他三个边缘保持固定。您可以使用 指定工作表支持的detents制动器,并使用监视其最近选择的制动器selectedDetentIdentifier

https://developer.apple.com/documentation/uikit/uisheetpresentationcontroller

在 WWDC Session 10063: Customize and Resize Sheets in UIKit 中探索了这个新的底部表单控件

UISheetPresentationController


很遗憾....

在 iOS 15 中,UISheetPresentationController已经推出了只有mediumlarge棘爪。

small棘爪是从iOS 15 API,这将需要以显示总是呈现-“坍塌”底部片材,如苹果地图明显缺少:

在 UISheetPresentationController 中自定义较小的制动器?

medium制动被释放到手柄的使用情况下,如股份片或“•••更多”菜单中的邮件:一键触发的半张。

在 iOS 15 中,Apple Maps 现在使用这个 UIKit 表单呈现而不是自定义实现,这是朝着正确方向迈出的一大步。iOS 15 中的 Apple Maps 继续显示“小”栏,以及 1/3 的高度栏。但是这些视图大小不是开发人员可用的公共 API。

WWDC 2021 实验室的 UIKit 工程师似乎知道small制动器将是一个非常受欢迎的 UIKit 组件。我希望明年能看到 iOS 16 的 API 扩展。

  • 从 iOS 16 开始,UISheetPresentationController 现在有一个[自定义制动器](https://developer.apple.com/documentation/uikit/uisheetpresentationcontroller/detent/3976719-custom)。迷你示例代码:`.custom(identifier: .small) { context in 0.3 * context.maximumDetentValue }` (2认同)