有人能告诉我如何在iOS 10的新苹果地图应用程序中模仿底部表格吗?
在Android中,你可以使用一个BottomSheet来模仿这种行为,但我在iOS中找不到任何类似的东西。
这是一个简单的带有内容嵌入的滚动视图,以便搜索栏在底部吗?
我对iOS编程相当陌生,所以如果有人能帮助我创建这个布局,那将是非常感谢的。
这就是我所说的“底稿”:
有人能告诉我如何在iOS 10的新苹果地图应用程序中模仿底部表格吗?
在Android中,你可以使用一个BottomSheet来模仿这种行为,但我在iOS中找不到任何类似的东西。
这是一个简单的带有内容嵌入的滚动视图,以便搜索栏在底部吗?
我对iOS编程相当陌生,所以如果有人能帮助我创建这个布局,那将是非常感谢的。
这就是我所说的“底稿”:
我不知道新地图应用程序的底部表单是如何响应用户交互的。但您可以创建一个自定义视图,看起来就像屏幕截图中的视图,并将其添加到主视图中。
我想你知道如何:
1-通过故事板或使用xib文件创建视图控制器。
2-使用谷歌地图或苹果的MapKit。
例子
创建2个视图控制器,例如MapViewController和BottomSheetViewController。第一个控制器将承载映射,第二个控制器是底层表本身。
配置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)
}
并在viewDidAppear方法中调用它:
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
addBottomSheetView()
}
配置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)
}
在viewWillAppear中调用这个方法
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
prepareBackgroundView()
}
确保你的控制器视图背景颜色是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)
}
}
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)
}
并执行你的手势行为:
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)
}
可滚动的底页:
如果你的自定义视图是一个滚动视图或任何其他继承的视图,那么你有两个选择:
第一:
使用头视图设计视图,并将panGesture添加到头视图。(糟糕的用户体验)。
第二:
1 -添加panGesture到bottom sheet视图。
实现UIGestureRecognizerDelegate,并将panGesture delegate设置为控制器。
实现shouldrecognizesimulouslywith委托函数,并在以下两种情况下禁用scrollView isScrollEnabled属性:
视图部分可见。 视图是完全可见的,scrollView的contenttoffset属性为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
}
NOTE
如果你将. allowuserinteraction设置为一个动画选项,就像在示例项目中一样,那么如果用户向上滚动,你需要在动画完成闭包上启用滚动。
示例项目
我在这个回购上创建了一个具有更多选项的示例项目,这可能会让您更好地了解如何自定义流程。
在演示中,addBottomSheetView()函数控制应将哪个视图用作底层表。
项目示例截图
—局部视图
——FullView
—可滚动视图
试试轮:
滑轮是一个易于使用的抽屉库,旨在模仿抽屉 在iOS 10的地图应用程序中。它公开了一个简单的API,允许你使用 任何UIViewController子类作为抽屉内容或主类 内容。
https://github.com/52inc/Pulley
也许你可以试试我的答案https://github.com/AnYuan/AYPannel,灵感来自滑轮。从移动抽屉到滚动列表的平稳过渡。我在容器滚动视图中添加了一个平移手势,并设置shouldRecognizeSimultaneouslyWithGestureRecognizer返回YES。更多细节在我的github链接上面。希望能帮上忙。
更新iOS 15
在iOS 15中,你现在可以使用原生的UISheetPresentationController。
if let sheet = viewControllerToPresent.sheetPresentationController {
sheet.detents = [.medium(), .large()]
// your sheet setup
}
present(viewControllerToPresent, animated: true, completion: nil)
注意,你甚至可以使用overcurrentcontext表示模式重现它的导航堆栈:
let nextViewControllerToPresent: UIViewController = ...
nextViewControllerToPresent.modalPresentationStyle = .overCurrentContext
viewControllerToPresent.present(nextViewControllerToPresent, animated: true, completion: nil)
遗产
我根据下面的答案发布了一个库。
它模仿快捷方式应用程序覆盖。详情请参阅本文。
这个库的主要组件是OverlayContainerViewController。它定义了一个区域,视图控制器可以被上下拖动,隐藏或显示它下面的内容。
let contentController = MapsViewController()
let overlayController = SearchViewController()
let containerController = OverlayContainerViewController()
containerController.delegate = self
containerController.viewControllers = [
contentController,
overlayController
]
window?.rootViewController = containerController
实现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
}
}
SwiftUI (12/29/20)
该库的SwiftUI版本现已可用。
Color.red.dynamicOverlay(Color.green)
以前的回答
我认为有一个重要的点没有在建议的解决方案中处理:滚动和翻译之间的过渡。
在Maps中,你可能注意到了,当tableView达到contenttoffset。Y == 0,底部薄片要么向上滑动要么向下滑动。
这一点很棘手,因为当平移手势开始转换时,我们不能简单地启用/禁用滚动。它会停止滚动,直到新的触摸开始。本文提出的大多数解决方案都是如此。
这是我实现这个动作的尝试。
起点:地图应用
为了开始我们的调查,让我们可视化地图的视图层次结构(在模拟器上启动地图,并选择调试>通过PID附加到进程或在Xcode 9中命名>地图)。
它并没有告诉我们运动是如何进行的,但它帮助我理解了它的逻辑。你可以使用lldb和视图层次调试器。
我们的视图控制器堆栈
让我们创建一个Maps ViewController架构的基本版本。
我们从一个BackgroundViewController(我们的地图视图)开始:
class BackgroundViewController: UIViewController {
override func loadView() {
view = MKMapView()
}
}
我们把tableView放到一个专用的UIViewController中:
class OverlayViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
lazy var tableView = UITableView()
override func loadView() {
view = tableView
tableView.dataSource = self
tableView.delegate = self
}
[...]
}
现在,我们需要一个VC来嵌入覆盖和管理它的翻译。 为了简化问题,我们认为它可以从一个静态点OverlayPosition转换叠加。maximum到另一个OverlayPosition.minimum。
现在它只有一个公共方法来动画位置变化,并且它有一个透明的视图:
enum OverlayPosition {
case maximum, minimum
}
class OverlayContainerViewController: UIViewController {
let overlayViewController: OverlayViewController
var translatedViewHeightContraint = ...
override func loadView() {
view = UIView()
}
func moveOverlay(to position: OverlayPosition) {
[...]
}
}
最后,我们需要一个ViewController来嵌入所有:
class StackViewController: UIViewController {
private var viewControllers: [UIViewController]
override func viewDidLoad() {
super.viewDidLoad()
viewControllers.forEach { gz_addChild($0, in: view) }
}
}
在我们的AppDelegate中,我们的启动序列是这样的:
let overlay = OverlayViewController()
let containerViewController = OverlayContainerViewController(overlayViewController: overlay)
let backgroundViewController = BackgroundViewController()
window?.rootViewController = StackViewController(viewControllers: [backgroundViewController, containerViewController])
叠加翻译背后的困难
现在,如何转换我们的叠加?
大多数提出的解决方案都使用专用的平移手势识别器,但实际上我们已经有了一个:表格视图的平移手势。 此外,我们需要保持滚动和翻译同步,并且UIScrollViewDelegate拥有我们需要的所有事件!
简单的实现将使用第二个pan Gesture,并在转换发生时尝试重置表视图的contenttoffset:
func panGestureAction(_ recognizer: UIPanGestureRecognizer) {
if isTranslating {
tableView.contentOffset = .zero
}
}
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.
翻译实现
我们在OverlayVC中添加了一个委托,将滚动视图的事件分派给我们的翻译处理程序OverlayContainerViewController:
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)
}
}
在我们的容器中,我们使用枚举来跟踪转换:
enum OverlayInFlightPosition {
case minimum
case maximum
case progressing
}
当前位置计算如下:
private var overlayInFlightPosition: OverlayInFlightPosition {
let height = translatedViewHeightContraint.constant
if height == maximumHeight {
return .maximum
} else if height == minimumHeight {
return .minimum
} else {
return .progressing
}
}
我们需要3个方法来处理翻译:
第一个函数告诉我们是否需要开始翻译。
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
}
}
第二个程序执行翻译。它使用scrollView的平移手势的翻译(in:)方法。
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)
)
}
当用户松开手指时,第三个动画显示翻译的结束。我们使用速度和视图的当前位置来计算位置。
private func animateTranslationEnd() {
let position: OverlayPosition = // ... calculation based on the current overlay position & velocity
moveOverlay(to: position)
}
我们的覆盖的委托实现看起来就像这样:
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()
}
}
最后一个问题:调度覆盖容器的触摸
翻译现在非常有效。但是还有最后一个问题:触摸没有传递到我们的背景视图。它们都被覆盖容器的视图拦截。 我们不能将isUserInteractionEnabled设置为false,因为它也会禁用表视图中的交互。解决方案是地图应用中大量使用的PassThroughView:
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
}
}
它将自己从响应器链中移除。
在OverlayContainerViewController:
override func loadView() {
view = PassThroughView()
}
结果
结果如下:
你可以在这里找到代码。
如果你看到任何虫子,请告诉我!注意,你的实现当然可以使用第二个平移手势,特别是如果你在你的覆盖中添加一个头部。
更新23/08/18
我们可以用 willEndScrollingWithVelocity,而不是在用户结束拖动时启用/禁用滚动:
func scrollView(_ scrollView: UIScrollView,
willEndScrollingWithVelocity velocity: CGPoint,
targetContentOffset: UnsafeMutablePointer<CGPoint>) {
switch overlayInFlightPosition {
case .maximum:
break
case .minimum, .progressing:
targetContentOffset.pointee = .zero
}
animateTranslationEnd(following: scrollView)
}
我们可以使用spring动画,并允许用户在动画时进行交互,以使动作更好地流动:
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)
}
我写了自己的库来实现ios地图应用程序的预期行为。这是一个面向协议的解决方案。因此,您不需要继承任何基类,而是创建一个表控制器,并按照您的意愿进行配置。它还支持内部导航/表示,有或没有UINavigationController。
更多细节请参见下面的链接。
https://github.com/OfTheWolf/UBottomSheet
你可以试试我的答案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)
}
...
}
下面是另一个示例代码,用于显示底部表单以搜索Apple Maps之类的位置。
如果你正在寻找一个使用视图结构的SwiftUI 2.0解决方案,这里是:
https://github.com/kenfai/KavSoft-Tutorials-iOS/tree/main/MapsBottomSheet
2021年的iOS 15添加了UISheetPresentationController,这是苹果首次公开发布苹果地图风格的“底稿”:
UISheetPresentationController UISheetPresentationController lets you present your view controller as a sheet. Before you present your view controller, configure its sheet presentation controller with the behavior and appearance you want for your sheet. Sheet presentation controllers specify a sheet's size based on a detent, a height where a sheet naturally rests. Detents allow a sheet to resize from one edge of its fully expanded frame while the other three edges remain fixed. You specify the detents that a sheet supports using detents, and monitor its most recently selected detent using selectedDetentIdentifier. https://developer.apple.com/documentation/uikit/uisheetpresentationcontroller
在WWDC Session 10063:在UIKit中自定义和调整表大小中探索了这个新的底部表控件
不幸的是……
在iOS 15中,UISheetPresentationController已经启动,只有中型和大型止动。
值得注意的是,iOS 15 API中没有一个小的缺陷,它将被要求显示像Apple Maps那样始终呈现的“折叠”底部表:
在UISheetPresentationController中自定义更小的止损?
释放了介质止动,以处理共享表或邮件中的“•••More”菜单等用例:按钮触发的半页。
在iOS 15中,苹果地图现在使用这种UIKit表格表示,而不是自定义实现,这是朝着正确方向迈出的一大步。iOS 15中的苹果地图继续显示“小”条,以及1/3高的条。但是这些视图大小并不是开发人员可用的公共API。
WWDC 2021实验室的UIKit工程师似乎知道,一个小的detent将是一个非常受欢迎的UIKit组件。我希望明年能看到iOS 16的API扩展。
iOS 15终于添加了一个本地UISheetPresentationController!
官方文档 https://developer.apple.com/documentation/uikit/uisheetpresentationcontroller
**为iOS 15本机支持提供此**
@IBAction func openSheet() {
let secondVC = self.storyboard?.instantiateViewController(withIdentifier: "SecondViewController")
// Create the view controller.
if #available(iOS 15.0, *) {
let formNC = UINavigationController(rootViewController: secondVC!)
formNC.modalPresentationStyle = UIModalPresentationStyle.pageSheet
guard let sheetPresentationController = formNC.presentationController as? UISheetPresentationController else {
return
}
sheetPresentationController.detents = [.medium(), .large()]
sheetPresentationController.prefersGrabberVisible = true
present(formNC, animated: true, completion: nil)
} else {
// Fallback on earlier versions
}
}
我们刚刚发布了一个支持iOS 11.4+的纯Swift包,它为您提供了一个底部表,其中包含您可以自定义的主题和行为选项。该组件使用方便,灵活。你可以在这里找到它:https://github.com/LunabeeStudio/LBBottomSheet。 在这个存储库中还提供了一个演示项目。
例如,它支持不同的方式来管理所需的高度,还添加到它后面的控制器的能力,以检测高度变化和适应其底部内容嵌入。
你可以在GitHub存储库和文档中找到更多信息:https://lbbottomsheet.lunabee.studio。
我觉得它能帮你做你想做的事。如果你有意见或问题,请不要犹豫,告诉我:)
在这里你可以看到所有可能的BottomSheet配置之一: