谁能告诉我如何模仿iOS 10中新Maps应用程序的底部?
在Android中,您可以使用BottomSheet
模仿此行为的,但我找不到适用于iOS的类似内容。
那是一个带有内容插图的简单滚动视图,以便搜索栏位于底部吗?
我是iOS编程的新手,所以如果有人可以帮助我创建此布局,将不胜感激。
这就是我所说的“底页”:
谁能告诉我如何模仿iOS 10中新Maps应用程序的底部?
在Android中,您可以使用BottomSheet
模仿此行为的,但我找不到适用于iOS的类似内容。
那是一个带有内容插图的简单滚动视图,以便搜索栏位于底部吗?
我是iOS编程的新手,所以如果有人可以帮助我创建此布局,将不胜感激。
这就是我所说的“底页”:
Answers:
我不知道新的Google Maps应用程序的底部如何准确响应用户交互。但是您可以创建一个自定义视图,看起来像屏幕截图中的视图,并将其添加到主视图中。
我假设您知道如何:
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设置为动画选项,那么如果用户正在向上滚动,则需要在动画完成关闭处启用滚动。
我创建了一个示例项目,此仓库上有更多选项,可以使您更好地了解如何自定义流程。
在演示中,addBottomSheetView()函数控制将哪个视图用作底页。
我根据以下答案发布了一个库。
它模仿“快捷方式”应用程序覆盖。有关详细信息,请参见本文。
该库的主要组件是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
,底部工作表将向上或向下滑动。
这一点很棘手,因为当平移手势开始平移时,我们不能简单地启用/禁用滚动。它将停止滚动,直到开始新的触摸为止。此处大多数建议的解决方案都是这种情况。
这是我尝试执行这项议案的尝试。
开始我们的调查,让我们直观地图的视图层次结构(开始仿真地图和选择Debug
> Attach to process by PID or Name
> Maps
在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并尝试contentOffset
在发生转换时重置表视图的:
func panGestureAction(_ recognizer: UIPanGestureRecognizer) {
if isTranslating {
tableView.contentOffset = .zero
}
}
但这行不通。tableView contentOffset
在其自身的平移手势识别器动作触发时或在调用其displayLink回调时更新其。在这些识别符成功覆盖之后,我们的识别器不可能立即触发contentOffset
。我们唯一的机会是要么参与布局阶段(通过layoutSubviews
在滚动视图的每一帧覆盖滚动视图调用),要么响应didScroll
每次contentOffset
修改时调用的委托方法。让我们尝试一下。
我们向我们添加一个委托,OverlayVC
以将scrollview的事件分派给我们的翻译处理程序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
}
}
第二个执行翻译。它使用translation(in:)
scrollView的平移手势的方法。
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
因为它还会禁用表视图中的交互。该解决方案是在Maps应用中大量使用的解决方案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)
}
UIViewPropertyAnimator
(可以暂停)解决该问题,然后使用该fractionComplete
属性执行清理。
translateView(following:)
方法中,您将基于平移计算高度,但是高度可以从不同于0.0的值开始(例如,表格视图稍微滚动一点,并且在开始拖动时叠加层会最大化)。那将导致最初的跳跃。您可以通过scrollViewWillBeginDragging
回调解决此问题,在该回调中您将记住contentOffset
表视图的初始值并将其用于translateView(following:)
的计算中。
csrutil disable && reboot
。然后,您将拥有root通常赋予您的所有权力,包括能够附加到Apple进程的权力。
尝试滑轮:
Pulley是一个易于使用的抽屉库,旨在模仿iOS 10的Maps应用中的抽屉。它公开了一个简单的API,该API允许您将任何UIViewController子类用作抽屉内容或主要内容。
您可以尝试我的回答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的位置。
我编写了自己的库来实现ios Maps应用程序中的预期行为。这是一个面向协议的解决方案。因此,您无需继承任何基类,而是创建工作表控制器并根据需要进行配置。它还支持带有或不带有UINavigationController的内部导航/演示。
有关更多详细信息,请参见下面的链接。
我最近创建了一个称为SwipeableView
的子类的组件UIView
Swift的。它支持所有4个方向,具有多个自定义选项,并且可以对不同的属性和项进行动画处理和插值(例如布局约束,背景/色调颜色,仿射变换,alpha通道和视图中心,所有这些都通过各自的展示案例进行了演示)。如果已设置或自动检测到,它还支持与内部滚动视图的滑动协调。应该非常简单易用(我希望I)
链接在https://github.com/LucaIaco/SwipeableView
概念证明:
希望能帮助到你
也许您可以尝试我的答案https://github.com/AnYuan/AYPannel,它受Pulley启发。从移动抽屉到滚动列表的平滑过渡。我在容器滚动视图上添加了平移手势,并设置了ShouldRecognizeSimultaneouslyWithGestureRecognizer以返回YES。我上面的github链接中有更多细节。希望能对您有所帮助。