ios 如何模仿地图应用程序的底部工作表?
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/37967555/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
How can I mimic the bottom sheet from the Maps app?
提问by qwertz
Can anyone tell me how I can mimic the bottom sheet in the new Maps app in iOS 10?
谁能告诉我如何在 iOS 10 的新地图应用程序中模仿底部工作表?
In Android, you can use a BottomSheet
which mimics this behaviour, but I could not find anything like that for iOS.
在 Android 中,您可以使用BottomSheet
模仿这种行为的 a,但我在 iOS 中找不到类似的东西。
Is that a simple scroll view with a content inset, so that the search bar is at the bottom?
这是一个带有内容插入的简单滚动视图,以便搜索栏位于底部?
I am fairly new to iOS programming so if someone could help me creating this layout, that would be highly appreciated.
我对 iOS 编程相当陌生,所以如果有人能帮助我创建这个布局,那将不胜感激。
This is what I mean by "bottom sheet":
这就是我所说的“底页”的意思:
回答by Ahmad Elassuty
I don't know how exactly the bottom sheet of the new Maps app, responds to user interactions. But you can create a custom view that looks like the one in the screenshots and add it to the main view.
我不知道新地图应用程序的底部工作表如何响应用户交互。但是您可以创建一个看起来像屏幕截图中的自定义视图并将其添加到主视图中。
I assume you know how to:
我假设你知道如何:
1- create view controllers either by storyboards or using xib files.
1- 通过故事板或使用 xib 文件创建视图控制器。
2- use googleMaps or Apple's MapKit.
2- 使用 googleMaps 或 Apple 的 MapKit。
Example
例子
1- Create 2 view controllers e.g, MapViewControllerand BottomSheetViewController. The first controller will host the map and the second is the bottom sheet itself.
1- 创建 2 个视图控制器,例如MapViewController和BottomSheetViewController。第一个控制器将托管地图,第二个是底部工作表本身。
Configure MapViewController
配置 MapViewController
Create a method to add the bottom sheet view.
创建一个方法来添加底部工作表视图。
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)
}
And call it in viewDidAppear method:
并在 viewDidAppear 方法中调用它:
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
addBottomSheetView()
}
Configure BottomSheetViewController
配置BottomSheetViewController
1) Prepare background
1) 准备背景
Create a method to add blur and vibrancy effects
创建一种添加模糊和活力效果的方法
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)
}
call this method in your viewWillAppear
在您的 viewWillAppear 中调用此方法
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
prepareBackgroundView()
}
Make sure that your controller's view background color is clearColor.
确保控制器的视图背景颜色是 clearColor。
2) Animate bottomSheet appearance
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)
}
}
3) Modify your xib as you want.
3)根据需要修改您的xib。
4) Add Pan Gesture Recognizer to your view.
4) 将平移手势识别器添加到您的视图中。
In your viewDidLoad method add UIPanGestureRecognizer.
在您的 viewDidLoad 方法中添加 UIPanGestureRecognizer。
override func viewDidLoad() {
super.viewDidLoad()
let gesture = UIPanGestureRecognizer.init(target: self, action: #selector(BottomSheetViewController.panGesture))
view.addGestureRecognizer(gesture)
}
And implement your gesture behaviour:
并实现您的手势行为:
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)
}
Scrollable Bottom Sheet:
可滚动的底部工作表:
If your custom view is a scroll view or any other view that inherits from, so you have two options:
如果您的自定义视图是滚动视图或任何其他继承自的视图,那么您有两个选择:
First:
第一的:
Design the view with a header view and add the panGesture to the header. (bad user experience).
使用标题视图设计视图并将 panGesture 添加到标题。(糟糕的用户体验)。
Second:
第二:
1 - Add the panGesture to the bottom sheet view.
1 - 将 panGesture 添加到底部工作表视图。
2 - Implement the UIGestureRecognizerDelegateand set the panGesture delegate to the controller.
2 - 实现UIGestureRecognizerDelegate并将 panGesture 委托设置为控制器。
3- Implement shouldRecognizeSimultaneouslyWithdelegate function and disablethe scrollView isScrollEnabledproperty in two case:
3- 实现shouldRecognizeSimultaneousWith委托函数并在两种情况下禁用scrollView isScrollEnabled属性:
- The view is partially visible.
- The view is totally visible, the scrollView contentOffsetproperty is 0 and the user is dragging the view downwards.
- 该视图是部分可见的。
- 视图是完全可见的,scrollView contentOffset属性为 0 并且用户正在向下拖动视图。
Otherwise enable scrolling.
否则启用滚动。
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
}
NOTE
笔记
In case you set .allowUserInteractionas an animation option, like in the sample project, so you need to enable scrolling on the animation completion closure if the user is scrolling up.
如果您将.allowUserInteraction设置为动画选项,就像在示例项目中一样,那么如果用户向上滚动,您需要在动画完成关闭上启用滚动。
Sample Project
示例项目
I created a sample project with more options on thisrepo which may give you better insights about how to customise the flow.
我在这个repo上创建了一个带有更多选项的示例项目,它可以让您更好地了解如何自定义流程。
In the demo, addBottomSheetView() function controls which view should be used as a bottom sheet.
在演示中, addBottomSheetView() 函数控制应将哪个视图用作底部工作表。
Sample Project Screenshots
示例项目截图
- Partial View
- 部分视图
- FullView
- 全视图
- Scrollable View
- 可滚动视图
回答by GaétanZ
I released a librarybased on my answer below.
我根据下面的回答发布了一个库。
It mimics the Shortcuts application overlay. See this articlefor details.
它模仿快捷方式应用程序覆盖。有关详细信息,请参阅此文章。
The main component of the library is the OverlayContainerViewController
. It defines an area where a view controller can be dragged up and down, hiding or revealing the content underneath it.
该库的主要组成部分是OverlayContainerViewController
. 它定义了一个区域,可以在其中上下拖动视图控制器,隐藏或显示其下方的内容。
let contentController = MapsViewController()
let overlayController = SearchViewController()
let containerController = OverlayContainerViewController()
containerController.delegate = self
containerController.viewControllers = [
contentController,
overlayController
]
window?.rootViewController = containerController
Implement OverlayContainerViewControllerDelegate
to specify the number of notches wished:
实施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
}
}
Previous answer
上一个答案
I think there is a significant point that is not treated in the suggested solutions: the transition between the scroll and the translation.
我认为在建议的解决方案中没有处理一个重要的问题:滚动和翻译之间的过渡。
In Maps, as you may have noticed, when the tableView reaches contentOffset.y == 0
, the bottom sheet either slides up or goes down.
在 Maps 中,您可能已经注意到,当 tableView 到达 时contentOffset.y == 0
,底部工作表要么向上滑动,要么向下滑动。
The point is tricky because we can not simply enable/disable the scroll when our pan gesture begins the translation. It would stop the scroll until a new touch begins. This is the case in most of the proposed solutions here.
这一点很棘手,因为当平移手势开始翻译时,我们不能简单地启用/禁用滚动。它会停止滚动,直到新的触摸开始。此处提出的大多数解决方案都是这种情况。
Here is my try to implement this motion.
这是我尝试执行此议案的方法。
Starting point: Maps App
起点:地图应用
To start our investigation, let's visualize the view hierarchy of Maps (start Maps on a simulator and select Debug
> Attach to process by PID or Name
> Maps
in Xcode 9).
为了开始我们的调查,让我们可视化 Maps 的视图层次结构(在模拟器上启动 Maps 并在 Xcode 9 中选择Debug
> Attach to process by PID or Name
> Maps
)。
It doesn't tell how the motion works, but it helped me to understand the logic of it. You can play with the lldb and the view hierarchy debugger.
它没有说明运动是如何工作的,但它帮助我理解了它的逻辑。您可以使用 lldb 和视图层次结构调试器。
Our view controller stacks
我们的视图控制器堆栈
Let's create a basic version of the Maps ViewController architecture.
让我们创建 Maps ViewController 架构的基本版本。
We start with a BackgroundViewController
(our map view):
我们从BackgroundViewController
(我们的地图视图)开始:
class BackgroundViewController: UIViewController {
override func loadView() {
view = MKMapView()
}
}
We put the tableView in a dedicated UIViewController
:
我们把 tableView 放在一个专用的UIViewController
:
class OverlayViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
lazy var tableView = UITableView()
override func loadView() {
view = tableView
tableView.dataSource = self
tableView.delegate = self
}
[...]
}
Now, we need a VC to embed the overlay and manage its translation.
To simplify the problem, we consider that it can translate the overlay from one static point OverlayPosition.maximum
to another OverlayPosition.minimum
.
现在,我们需要一个 VC 来嵌入叠加层并管理其翻译。为了简化问题,我们认为它可以将覆盖从一个静态点转换OverlayPosition.maximum
为另一个静态点OverlayPosition.minimum
。
For now it only has one public method to animate the position change and it has a transparent view:
目前它只有一个公共方法来为位置变化设置动画,并且它有一个透明的视图:
enum OverlayPosition {
case maximum, minimum
}
class OverlayContainerViewController: UIViewController {
let overlayViewController: OverlayViewController
var translatedViewHeightContraint = ...
override func loadView() {
view = UIView()
}
func moveOverlay(to position: OverlayPosition) {
[...]
}
}
Finally we need a ViewController to embed the all:
最后我们需要一个 ViewController 来嵌入所有:
class StackViewController: UIViewController {
private var viewControllers: [UIViewController]
override func viewDidLoad() {
super.viewDidLoad()
viewControllers.forEach { gz_addChild(let overlay = OverlayViewController()
let containerViewController = OverlayContainerViewController(overlayViewController: overlay)
let backgroundViewController = BackgroundViewController()
window?.rootViewController = StackViewController(viewControllers: [backgroundViewController, containerViewController])
, in: view) }
}
}
In our AppDelegate, our startup sequence looks like:
在我们的 AppDelegate 中,我们的启动顺序如下所示:
func panGestureAction(_ recognizer: UIPanGestureRecognizer) {
if isTranslating {
tableView.contentOffset = .zero
}
}
The difficulty behind the overlay translation
叠加翻译背后的难点
Now, how to translate our overlay?
现在,如何翻译我们的叠加层?
Most of the proposed solutions use a dedicated pan gesture recognizer, but we actually already have one : the pan gesture of the table view.
Moreover, we need to keep the scroll and the translation synchronised and the UIScrollViewDelegate
has all the events we need!
大多数提议的解决方案使用专用的平移手势识别器,但我们实际上已经有了一个:表格视图的平移手势。此外,我们需要保持滚动和翻译同步,并且UIScrollViewDelegate
拥有我们需要的所有事件!
A naive implementation would use a second pan Gesture and try to reset the contentOffset
of the table view when the translation occurs:
一个简单的实现将使用第二个平移手势并尝试contentOffset
在转换发生时重置表视图的 :
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)
}
}
But it does not work. The tableView updates its contentOffset
when its own pan gesture recognizer action triggers or when its displayLink callback is called. There is no chance that our recognizer triggers right after those to successfully override the contentOffset
.
Our only chance is either to take part of the layout phase (by overriding layoutSubviews
of the scroll view calls at each frame of the scroll view) or to respond to the didScroll
method of the delegate called each time the contentOffset
is modified. Let's try this one.
但它不起作用。当 tableViewcontentOffset
自己的平移手势识别器操作触发或调用其 displayLink 回调时,会更新它。我们的识别器不可能在成功覆盖contentOffset
. 我们唯一的机会是要么参与布局阶段(通过layoutSubviews
在滚动视图的每一帧覆盖滚动视图调用)要么响应didScroll
每次contentOffset
修改时调用的委托方法。让我们试试这个。
The translation Implementation
翻译实施
We add a delegate to our OverlayVC
to dispatch the scrollview's events to our translation handler, the OverlayContainerViewController
:
我们向我们添加一个委托以OverlayVC
将滚动视图的事件分派到我们的翻译处理程序,即OverlayContainerViewController
:
enum OverlayInFlightPosition {
case minimum
case maximum
case progressing
}
In our container, we keep track of the translation using a enum:
在我们的容器中,我们使用枚举跟踪翻译:
private var overlayInFlightPosition: OverlayInFlightPosition {
let height = translatedViewHeightContraint.constant
if height == maximumHeight {
return .maximum
} else if height == minimumHeight {
return .minimum
} else {
return .progressing
}
}
The current position calculation looks like :
当前位置计算看起来像:
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
}
}
We need 3 methods to handle the translation:
我们需要 3 种方法来处理翻译:
The first one tells us if we need to start the translation.
第一个告诉我们是否需要开始翻译。
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)
)
}
The second one performs the translation. It uses the translation(in:)
method of the scrollView's pan gesture.
第二个执行翻译。它使用了translation(in:)
scrollView 的平移手势的方法。
private func animateTranslationEnd() {
let position: OverlayPosition = // ... calculation based on the current overlay position & velocity
moveOverlay(to: position)
}
The third one animates the end of the translation when the user releases its finger. We calculate the position using the velocity & the current position of the view.
当用户松开手指时,第三个动画动画结束翻译。我们使用速度和视图的当前位置来计算位置。
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()
}
}
Our overlay's delegate implementation simply looks like :
我们的叠加层的委托实现看起来很简单:
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
}
}
Final problem: dispatching the overlay container's touches
最后一个问题:调度覆盖容器的触摸
The translation is now pretty efficient. But there is still a final problem: the touches are not delivered to our background view. They are all intercepted by the overlay container's view.
We can not set isUserInteractionEnabled
to false
because it would also disable the interaction in our table view. The solution is the one used massively in the Maps app, PassThroughView
:
翻译现在非常有效。但还有最后一个问题:触摸没有传递到我们的背景视图。它们都被覆盖容器的视图拦截。我们不能设置isUserInteractionEnabled
为,false
因为它也会禁用我们表视图中的交互。解决方案是在地图应用程序中大量使用的解决方案PassThroughView
:
override func loadView() {
view = PassThroughView()
}
It removes itself from the responder chain.
它将自己从响应者链中移除。
In OverlayContainerViewController
:
在OverlayContainerViewController
:
func scrollView(_ scrollView: UIScrollView,
willEndScrollingWithVelocity velocity: CGPoint,
targetContentOffset: UnsafeMutablePointer<CGPoint>) {
switch overlayInFlightPosition {
case .maximum:
break
case .minimum, .progressing:
targetContentOffset.pointee = .zero
}
animateTranslationEnd(following: scrollView)
}
Result
结果
Here is the result:
结果如下:
You can find the code here.
您可以在此处找到代码。
Please if you see any bugs, let me know ! Note that your implementation can of course use a second pan gesture, specially if you add a header in your overlay.
如果您看到任何错误,请告诉我!请注意,您的实现当然可以使用第二个平移手势,特别是当您在叠加层中添加标题时。
Update 23/08/18
23/08/18 更新
We can replace scrollViewDidEndDragging
with
willEndScrollingWithVelocity
rather than enabling
/disabling
the scroll when the user ends dragging:
当用户结束拖动时,我们可以scrollViewDidEndDragging
用
willEndScrollingWithVelocity
而不是enabling
/替换disabling
滚动:
func moveOverlay(to position: OverlayPosition,
duration: TimeInterval,
velocity: CGPoint) {
overlayPosition = position
translatedViewHeightContraint.constant = translatedViewTargetHeight
UIView.animate(
withDuration: duration,
delay: 0,
usingSpringWithDamping: velocity.y == 0 ? 1 : 0.6,
initialSpringVelocity: abs(velocity.y),
options: [.allowUserInteraction],
animations: {
self.view.layoutIfNeeded()
}, completion: nil)
}
We can use a spring animation and allow user interaction while animating to make the motion flow better:
我们可以使用弹簧动画并允许用户在动画的同时进行交互以使运动流更好:
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)
}
...
}
回答by Rajee Jones
Try Pulley:
试试滑轮:
Pulley is an easy to use drawer library meant to imitate the drawer in iOS 10's Maps app. It exposes a simple API that allows you to use any UIViewController subclass as the drawer content or the primary content.
Pulley 是一个易于使用的抽屉库,旨在模仿 iOS 10 的地图应用程序中的抽屉。它公开了一个简单的 API,允许您使用任何 UIViewController 子类作为抽屉内容或主要内容。
回答by SCENEE
You can try my answer https://github.com/SCENEE/FloatingPanel. It provides a container view controller to display a "bottom sheet" interface.
你可以试试我的回答https://github.com/SCENEE/FloatingPanel。它提供了一个容器视图控制器来显示“底部工作表”界面。
It's easy to use and you don't mind any gesture recognizer handling! Also you can track a scroll view's(or the sibling view) in a bottom sheet if needed.
它易于使用,您不介意任何手势识别器处理!如果需要,您还可以在底部工作表中跟踪滚动视图(或同级视图)。
This is a simple example. Please note that you need to prepare a view controller to display your content in a bottom sheet.
这是一个简单的例子。请注意,您需要准备一个视图控制器来在底部工作表中显示您的内容。
##代码##Hereis another example code to display a bottom sheet to search a location like Apple Maps.
这是另一个示例代码,用于显示底部工作表以搜索 Apple 地图等位置。
回答by ugur
I wrote my own library to achieve the intended behaviour in ios Maps app. It is a protocol oriented solution. So you don't need to inherit any base class instead create a sheet controller and configure as you wish. It also supports inner navigation/presentation with or without UINavigationController.
我编写了自己的库来实现 ios Maps 应用程序中的预期行为。它是一个面向协议的解决方案。因此,您不需要继承任何基类,而是创建一个工作表控制器并根据需要进行配置。它还支持带有或不带有 UINavigationController 的内部导航/演示。
See below link for more details.
有关更多详细信息,请参阅以下链接。
https://github.com/OfTheWolf/UBottomSheet
回答by Luca Iaco
I recently created a component called SwipeableView
as subclass of UIView
, written in Swift 5.1 . It support all 4 direction, has several customisation options and can animate and interpolate different attributes and items ( such as layout constraints, background/tint color, affine transform, alpha channel and view center, all of them demoed with the respective show case ). It also supports the swiping coordination with the inner scroll view if set or auto detected. Should be pretty easy and straightforward to be used ( I hope )
我最近创建了一个名为 的SwipeableView
子类的组件UIView
,它是用 Swift 5.1 编写的。它支持所有 4 个方向,有几个自定义选项,可以动画和插入不同的属性和项目(例如布局约束、背景/色调颜色、仿射变换、alpha 通道和视图中心,所有这些都在各自的展示案例中进行了演示)。如果设置或自动检测到,它还支持与内部滚动视图的滑动协调。应该非常容易和直接使用(我希望)
Link at https://github.com/LucaIaco/SwipeableView
链接在https://github.com/LucaIaco/SwipeableView
proof of concept:
概念证明:
Hope it helps
希望能帮助到你
回答by day
Maybe you can try my answer https://github.com/AnYuan/AYPannel, inspired by Pulley. Smooth transition from moving the drawer to scrolling the list. I added a pan gesture on the container scroll view, and set shouldRecognizeSimultaneouslyWithGestureRecognizer to return YES. More detail in my github link above. Wish to help.
也许你可以试试我的答案https://github.com/AnYuan/AYPannel,灵感来自 Pulley。从移动抽屉到滚动列表的平滑过渡。我在容器滚动视图上添加了一个平移手势,并将 shouldRecognizeSimultaneouslyWithGestureRecognizer 设置为返回 YES。上面我的github链接中有更多详细信息。愿意帮忙。