有人能告诉我如何在iOS 10的新苹果地图应用程序中模仿底部表格吗?
在Android中,你可以使用BottomSheet来模仿这种行为,但我在iOS中找不到任何类似的东西。
BottomSheet
这是一个简单的带有内容嵌入的滚动视图,以便搜索栏在底部吗?
我对iOS编程相当陌生,所以如果有人能帮助我创建这个布局,那将是非常感谢的。
这就是我所说的“底页”:
我不知道新地图应用程序的底部表单是如何响应用户交互的。但您可以创建一个自定义视图,看起来就像屏幕截图中的视图,并将其添加到主视图中。
我想你知道如何:
1-通过故事板或使用xib文件创建视图控制器。
2-使用谷歌地图或苹果的MapKit。
创建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到bottom sheet视图。
实现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设置为动画选项,就像在示例项目中一样,如果用户向上滚动,你需要在动画完成闭包上启用滚动。 . c
我在这 repo上创建了一个带有更多选项的示例项目,这可能会让你更好地了解如何自定义流程。
在演示中,addBottomSheetView()函数控制哪个视图应该被用作底层表
试试滑轮:
滑轮是一个易于使用的抽屉库,旨在模仿抽屉 在iOS 10的地图应用程序中。它公开了一个简单的API,允许你使用 任何UIViewController子类作为抽屉内容或主类 内容。< / p >
https://github.com/52inc/Pulley
也许你可以试试我的答案https://github.com/AnYuan/AYPannel,灵感来自滑轮。从移动抽屉到滚动列表的平稳过渡。我在容器滚动视图中添加了一个平移手势,并设置shouldRecognizeSimultaneouslyWithGestureRecognizer返回YES。更多细节在我的github链接上面。希望能帮上忙。
在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。它定义了一个区域,视图控制器可以被上下拖动,隐藏或显示它下面的内容。
OverlayContainerViewController
let contentController = MapsViewController() let overlayController = SearchViewController() let containerController = OverlayContainerViewController() containerController.delegate = self containerController.viewControllers = [ contentController, overlayController ] window?.rootViewController = containerController
实现OverlayContainerViewControllerDelegate来指定希望的缺口数量:
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版本现在可用。
Color.red.dynamicOverlay(Color.green)
我认为有一个重要的点没有在建议的解决方案中处理:滚动和翻译之间的过渡。
在Maps中,正如你可能已经注意到的,当tableView到达contentOffset.y == 0时,底层表要么向上滑动,要么向下滑动。
contentOffset.y == 0
这一点很棘手,因为当平移手势开始转换时,我们不能简单地启用/禁用滚动。它会停止滚动,直到新的触摸开始。本文提出的大多数解决方案都是如此。
这是我实现这个动作的尝试。
为了开始我们的研究,让我们可视化Maps的视图层次结构(在模拟器上启动Maps并选择Debug >Attach to process by PID or Name祝辞Maps在Xcode 9)。
Debug
Attach to process by PID or Name
Maps
.
它并没有告诉我们运动是如何进行的,但它帮助我理解了它的逻辑。你可以使用lldb和视图层次调试器。
让我们创建一个Maps ViewController架构的基本版本。
我们从BackgroundViewController(我们的地图视图)开始:
BackgroundViewController
class BackgroundViewController: UIViewController { override func loadView() { view = MKMapView() } }
我们把tableView放在一个专用的UIViewController中:
UIViewController
class OverlayViewController: UIViewController, UITableViewDataSource, UITableViewDelegate { lazy var tableView = UITableView() override func loadView() { view = tableView tableView.dataSource = self tableView.delegate = self } [...] }
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,并在转换发生时尝试重置表视图的contentOffset:
contentOffset
func panGestureAction(_ recognizer: UIPanGestureRecognizer) { if isTranslating { tableView.contentOffset = .zero } }
但是它不起作用。当tableView自己的平移手势识别器动作触发或调用它的displayLink回调时,tableView更新它的contentOffset。我们的识别器不可能在这些之后立即触发来成功覆盖contentOffset。 我们唯一的机会是参与布局阶段(通过在滚动视图的每一帧重写滚动视图调用的layoutSubviews),或者在每次修改contentOffset时响应委托的didScroll方法。让我们试试这个
layoutSubviews
didScroll
我们向OverlayVC添加了一个委托,将滚动视图的事件分派给我们的翻译处理程序OverlayContainerViewController:
OverlayVC
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:)方法。
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
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() }
结果如下:
你可以找到代码在这里。
如果你看到任何虫子,请告诉我!注意,你的实现当然可以使用第二个平移手势,特别是如果你在你的覆盖中添加一个头部。
scrollViewDidEndDragging
willEndScrollingWithVelocity
enabling
disabling
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这样的位置。
我最近创建了一个名为SwipeableView的组件,作为UIView的子类,用Swift 5.1编写。它支持所有4个方向,有几个自定义选项,可以动画和插值不同的属性和项目(如布局约束,背景/色调颜色,仿射变换,alpha通道和视图中心,所有这些都演示了各自的展示案例)。它还支持滑动协调与内部滚动视图,如果设置或自动检测。应该是相当容易和直接使用(我希望🙂)
SwipeableView
UIView
链接到https://github.com/LucaIaco/SwipeableView
概念证明:
希望能有所帮助
如果你正在寻找一个使用View Struct的SwiftUI 2.0解决方案,这里是:
View
https://github.com/kenfai/KavSoft-Tutorials-iOS/tree/main/MapsBottomSheet
2021年的iOS 15添加了UISheetPresentationController,这是苹果第一次公开发布苹果地图风格的“底部表格”:
UISheetPresentationController
UISheetPresentationController UISheetPresentationController让你把你的视图控制器呈现为一个表格。在呈现你的视图控制器之前,用你想要的工作表的行为和外观配置它的工作表呈现控制器。 表显示控制器根据表的止动(表自然停留的高度)指定表的大小。止动器允许薄片从其完全展开的框架的一个边缘调整大小,而其他三个边缘保持固定。使用detents指定表支持的止动,并使用selectedDetentIdentifier监视其最近选择的止动。 https://developer.apple.com/documentation/uikit/uisheetpresentationcontroller
UISheetPresentationController让你把你的视图控制器呈现为一个表格。在呈现你的视图控制器之前,用你想要的工作表的行为和外观配置它的工作表呈现控制器。
表显示控制器根据表的止动(表自然停留的高度)指定表的大小。止动器允许薄片从其完全展开的框架的一个边缘调整大小,而其他三个边缘保持固定。使用detents指定表支持的止动,并使用selectedDetentIdentifier监视其最近选择的止动。
detents
selectedDetentIdentifier
https://developer.apple.com/documentation/uikit/uisheetpresentationcontroller
这个新的底页控件在WWDC Session 10063: 在UIKit中定制和调整表的大小中进行了探讨
在iOS 15中,UISheetPresentationController只带有medium和large止动。
medium
large
值得注意的是,iOS 15 API中没有small止回器,它将被要求显示始终呈现的“;折叠”。像苹果地图的底页:
small
在UISheetPresentationController中自定义较小的Detents ?< / >
medium止动被释放,以处理诸如Share Sheet或“;•••more”等用例;邮件菜单:一个按钮触发的半页。
在iOS 15中,苹果地图现在使用这种UIKit表格表示,而不是自定义实现,这是朝着正确方向迈出的一大步。iOS 15中的苹果地图继续显示“小”;杠,以及1/3高的杠。但是这些视图大小并不是开发人员可用的公共API。
WWDC 2021实验室的UIKit工程师似乎知道small制动器将是一个非常受欢迎的UIKit组件。我希望明年能看到iOS 16的API扩展。
iOS 15终于添加了原生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 } }
例如,它支持不同的方式来管理所需的高度,还添加到它后面的控制器的能力,以检测高度变化和适应其底部内容嵌入。
你可以在GitHub存储库和文档中找到更多信息:https://lbbottomsheet.lunabee.studio。
我觉得它能帮你做你想做的事。如果你有意见或问题,请不要犹豫,告诉我:)
在这里你可以看到所有可能的BottomSheet配置之一: