有谁可以告诉我如何在iOS 10中的新地图应用中模仿底页?
在Android中,您可以使用模仿此行为的BottomSheet
,但我找不到类似iOS的内容。
这是一个带有内容插入的简单滚动视图,以便搜索栏位于底部吗?
我对iOS编程很新,所以如果有人可以帮我创建这个版面,那将非常感激。
我的意思是"底页":
答案 0 :(得分:246)
我不知道新地图应用的底页是如何响应用户互动的。但您可以创建一个类似于截图中的自定义视图,并将其添加到主视图中。
我假设您知道如何:
1-通过故事板或使用xib文件创建视图控制器。
2-使用googleMaps或Apple的MapKit。
1-创建2个视图控制器,例如 MapViewController 和 BottomSheetViewController 。第一个控制器将托管地图,第二个是底部工作表。
创建一个添加底部工作表视图的方法。
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()
}
创建一种添加模糊和活力效果的方法
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。
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)
}
}
在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添加到底部工作表视图。
2 - 实施 UIGestureRecognizerDelegate 并将panGesture委托设置为控制器。
3-在两种情况下实施 shouldRecognizeSimultaneouslyWith 委托功能和禁用 scrollView isScrollEnabled 属性:
否则启用滚动。
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
}
注意强>
如果您将 .allowUserInteraction 设置为动画选项,就像示例项目一样,so you need to enable scrolling on the animation completion closure if the user is scrolling up.
我在this repo上创建了一个包含更多选项的示例项目,可以让您更好地了解如何自定义流程。
In the demo, addBottomSheetView() function controls which view should be used as a bottom sheet.
答案 1 :(得分:40)
我基于此解决方案https://github.com/applidium/ADOverlayContainer发布了一个库。
它模仿“快捷方式”应用程序的覆盖图。有关详细信息,请参见this article。
该库的主要组件是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
}
}
我认为建议的解决方案中没有提到一个重要的观点:滚动和翻译之间的过渡。
您可能已经注意到,在Maps中,当tableView到达contentOffset.y == 0
时,底部工作表将向上或向下滑动。
这一点很棘手,因为当平移手势开始翻译时,我们不能简单地启用/禁用滚动。它将停止滚动,直到开始新的触摸为止。此处大多数建议的解决方案都是这种情况。
这是我尝试执行此议案的方法。
要开始调查,让我们可视化地图的视图层次结构(在模拟器上启动地图,然后在Xcode 9中选择Debug
> Attach to process by PID or Name
> Maps
。
它不能说明运动的原理,但是可以帮助我理解它的逻辑。您可以使用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手势,并在发生翻译时尝试重置表视图的contentOffset
:
func panGestureAction(_ recognizer: UIPanGestureRecognizer) {
if isTranslating {
tableView.contentOffset = .zero
}
}
但是它不起作用。当触发其自己的平移手势识别器动作或调用其displayLink回调时,tableView将更新其contentOffset
。在识别器成功覆盖contentOffset
之后,我们的识别器将不会立即触发。
我们唯一的机会是要么参与布局阶段(通过在滚动视图的每一帧覆盖滚动视图调用的layoutSubviews
),要么响应每次调用的委托的didScroll
方法contentOffset
被修改。让我们尝试一下。
我们在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的平移手势的translation(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
,因为它还会禁用表视图中的交互。该解决方案是在Google地图应用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()
}
这是结果:
您可以找到代码here。
如果您发现任何错误,请告诉我!请注意,您的实现当然可以使用第二个平移手势,特别是如果您在叠加层中添加标题时。
我们可以将scrollViewDidEndDragging
替换为
用户结束拖动时,willEndScrollingWithVelocity
而不是enable
/ disable
滚动:
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)
}
答案 2 :(得分:15)
尝试 Pulley :
Pulley是一个易于使用的抽屉库,用于模仿抽屉 在iOS 10的地图应用程序中。它公开了一个允许您使用的简单API 任何UIViewController子类作为抽屉内容或主要内容 内容。
答案 3 :(得分:8)
您可以尝试我的答案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)
}
...
}
Here是另一个示例代码,显示底页以搜索类似Apple Maps的位置。
答案 4 :(得分:4)
2021 年的 iOS 15 添加了 UISheetPresentationController
,这是 Apple 首次公开发布 Apple Maps 风格的“底页”:
UISheetPresentationController 允许您将视图控制器显示为表格。在展示您的视图控制器之前,请使用您想要的工作表行为和外观配置其工作表展示控制器。
Sheet 展示控制器根据棘爪指定纸张的尺寸,止动器是纸张自然放置的高度。棘爪允许纸张从其完全展开框架的一个边缘调整大小,而其他三个边缘保持固定。您可以使用 detents
指定工作表支持的制动器,并使用 selectedDetentIdentifier
监视其最近选择的制动器。
https://developer.apple.com/documentation/uikit/uisheetpresentationcontroller
在 WWDC Session 10063 中探索了这个新的底部表单控件:Customize and Resize Sheets in UIKit
在 iOS 15 中,UISheetPresentationController
仅具有 medium
和 large
棘爪。
iOS 15 API 中明显没有 small
制动器,它需要显示始终呈现的“折叠”底部表单,例如 Apple 地图:
Custom smaller Detents in UISheetPresentationController?
释放了 medium
制动器以处理诸如共享表或邮件中的“••• 更多”菜单等用例:按钮触发的半页。
在 iOS 15 中,Apple Maps 现在使用这个 UIKit 表单演示而不是自定义实现,这是朝着正确方向迈出的一大步。 iOS 15 中的 Apple Maps 继续显示“小”栏,以及 1/3 的高度栏。但这些视图大小不是开发人员可用的公共 API。
WWDC 2021 实验室的 UIKit 工程师似乎知道 small
制动器将是一个非常受欢迎的 UIKit 组件。我希望明年能看到 iOS 16 的 API 扩展。
答案 5 :(得分:2)
以上内容对我来说都不起作用,因为同时平移tableview和外部视图都无法顺利进行。因此,我编写了自己的代码来实现ios Maps应用程序中的预期行为。
答案 6 :(得分:1)
我最近创建了一个名为SwipeableView
的组件,它是UIView
的子类,它是用Swift 5.1编写的。它支持所有4个方向,具有多个自定义选项,并且可以对不同的属性和项进行动画处理和插值(例如布局约束,背景/色调颜色,仿射变换,alpha通道和视图中心,所有这些都通过各自的展示案例进行了演示)。如果设置或自动检测到,它还支持与内部滚动视图的滑动协调。应该非常简单易用(我希望hope)
链接为https://github.com/LucaIaco/SwipeableView
概念验证:
希望有帮助
答案 7 :(得分:1)
**适用于 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
}
}
答案 8 :(得分:0)
也许你可以尝试我的答案https://github.com/AnYuan/AYPannel,灵感来自Pulley。从移动抽屉到滚动列表的平滑过渡。我在容器滚动视图上添加了一个平移手势,并设置了shouldRecognizeSimultaneouslyWithGestureRecognizer以返回YES。我上面的github链接中的更多细节。希望能提供帮助。
答案 9 :(得分:0)
如果您正在寻找使用View
Struct的SwiftUI 2.0解决方案,则为:
https://github.com/kenfai/KavSoft-Tutorials-iOS/tree/main/MapsBottomSheet
答案 10 :(得分:0)
iOS 15 终于添加了原生的 UISheetPresentationController
!
官方文档 https://developer.apple.com/documentation/uikit/uisheetpresentationcontroller