qwe*_*rtz 179 uiscrollview uiview mapkit ios swift
任何人都可以告诉我如何在iOS 10中的新地图应用程序中模仿底页?
在Android中,您可以使用BottomSheet模仿此行为的模式,但我找不到类似iOS的内容.
这是一个带有内容插入的简单滚动视图,以便搜索栏位于底部吗?
我对iOS编程很新,所以如果有人可以帮助我创建这个布局,那将非常感激.
这就是我所说的"底页":
Ahm*_*uty 288
我不知道新地图应用程序的底页是如何响应用户交互的.但您可以创建一个类似于屏幕截图中的自定义视图,并将其添加到主视图中.
我假设你知道如何:
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)
}
Run Code Online (Sandbox Code Playgroud)
并在viewDidAppear方法中调用它:
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
addBottomSheetView()
}
Run Code Online (Sandbox Code Playgroud)
创建一种方法来添加模糊和活力效果
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)
}
Run Code Online (Sandbox Code Playgroud)
在viewWillAppear中调用此方法
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
prepareBackgroundView()
}
Run Code Online (Sandbox Code Playgroud)
确保控制器的视图背景颜色为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)
}
}
Run Code Online (Sandbox Code Playgroud)
在viewDidLoad方法中添加UIPanGestureRecognizer.
override func viewDidLoad() {
super.viewDidLoad()
let gesture = UIPanGestureRecognizer.init(target: self, action: #selector(BottomSheetViewController.panGesture))
view.addGestureRecognizer(gesture)
}
Run Code Online (Sandbox Code Playgroud)
并实现您的手势行为:
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)
}
Run Code Online (Sandbox Code Playgroud)
如果您的自定义视图是滚动视图或继承的任何其他视图,那么您有两个选项:
第一:
使用标题视图设计视图,并将panGesture添加到标题中.(糟糕的用户体验).
第二:
1 - 将panGesture添加到底部工作表视图.
2 - 实现UIGestureRecognizerDelegate并将panGesture委托设置为控制器.
3-实现shouldRecognizeSimultaneouslyWith delegate函数并在两种情况下禁用 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
}
Run Code Online (Sandbox Code Playgroud)
注意
如果您将.allowUserInteraction设置为动画选项,就像在示例项目中一样,那么如果用户向上滚动,则需要在动画完成闭包上启用滚动.
我创建了一个示例项目,在此 repo 上有更多选项,可以为您提供有关如何自定义流的更好的见解.
在演示中,addBottomSheetView()函数控制哪个视图应该用作底部工作表.
Gaé*_*anZ 47
我发布了一个基于此解决方案的库https://github.com/applidium/ADOverlayContainer.
它模仿快捷方式应用程序的叠加层,其中包括:
我认为在建议的解决方案中没有对待一个重要的观点:滚动和翻译之间的过渡.
在地图中,您可能已经注意到,当tableView到达时OverlayContainerViewController,底部工作表会向上滑动或向下滑动.
这一点很棘手,因为当我们的平移手势开始翻译时,我们不能简单地启用/禁用滚动.它将停止滚动直到新的触摸开始.这是大多数提出的解决方案中的情况.
这是我尝试实施这一动议.
开始我们的调查,让我们直观地图的视图层次结构(启动地图上的模拟器和选择OverlayContainerViewControllerDelegate> contentOffset.y == 0> Debug在Xcode 9).
它没有说明动作是如何工作的,但它帮助我理解了它的逻辑.您可以使用lldb和视图层次结构调试器.
让我们创建一个Maps ViewController体系结构的基本版本.
我们从Attach to process by PID or Name(我们的地图视图)开始:
let contentController = MapsViewController()
let overlayController = SearchViewController()
let containerController = OverlayContainerViewController()
containerController.delegate = self
containerController.viewControllers = [
contentController,
overlayController
]
window?.rootViewController = containerController
Run Code Online (Sandbox Code Playgroud)
我们把tableView放在一个专用的Maps:
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
}
}
Run Code Online (Sandbox Code Playgroud)
现在,我们需要VC来嵌入叠加层并管理其翻译.为了简化问题,我们认为它可以将叠加层从一个静态点转换BackgroundViewController为另一个静态点UIViewController.
目前它只有一个公共方法来设置位置变化的动画,它有一个透明的视图:
class BackgroundViewController: UIViewController {
override func loadView() {
view = MKMapView()
}
}
Run Code Online (Sandbox Code Playgroud)
最后我们需要一个ViewController来嵌入all:
class OverlayViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
lazy var tableView = UITableView()
override func loadView() {
view = tableView
tableView.dataSource = self
tableView.delegate = self
}
[...]
}
Run Code Online (Sandbox Code Playgroud)
在我们的AppDelegate中,我们的启动顺序如下:
enum OverlayPosition {
case maximum, minimum
}
class OverlayContainerViewController: UIViewController {
let overlayViewController: OverlayViewController
var translatedViewHeightContraint = ...
override func loadView() {
view = UIView()
}
func moveOverlay(to position: OverlayPosition) {
[...]
}
}
Run Code Online (Sandbox Code Playgroud)
现在,如何翻译我们的叠加层?
大多数提出的解决方案使用专用的平移手势识别器,但我们实际上已经有一个:表视图的平移手势.此外,我们需要保持滚动和翻译同步,并OverlayPosition.maximum拥有我们需要的所有事件!
一个简单的实现将使用第二个pan Gesture并尝试OverlayPosition.minimum在发生转换时重置表视图:
class StackViewController: UIViewController {
private var viewControllers: [UIViewController]
override func viewDidLoad() {
super.viewDidLoad()
viewControllers.forEach { gz_addChild($0, in: view) }
}
}
Run Code Online (Sandbox Code Playgroud)
但它不起作用.tableView UIScrollViewDelegate在其自己的平移手势识别器操作触发或调用其displayLink回调时更新它.我们的识别器不可能在成功覆盖之后触发contentOffset.我们唯一的机会是参与布局阶段(通过覆盖contentOffset滚动视图每帧的滚动视图调用)或响应contentOffset每次layoutSubviews修改时调用的委托方法.我们来试试吧.
我们向我们添加一个委托,didScroll将scrollview的事件发送给我们的翻译处理程序,contentOffset:
let overlay = OverlayViewController()
let containerViewController = OverlayContainerViewController(overlayViewController: overlay)
let backgroundViewController = BackgroundViewController()
window?.rootViewController = StackViewController(viewControllers: [backgroundViewController, containerViewController])
Run Code Online (Sandbox Code Playgroud)
在我们的容器中,我们使用枚举跟踪翻译:
func panGestureAction(_ recognizer: UIPanGestureRecognizer) {
if isTranslating {
tableView.contentOffset = .zero
}
}
Run Code Online (Sandbox Code Playgroud)
当前位置计算如下:
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)
}
}
Run Code Online (Sandbox Code Playgroud)
我们需要3种方法来处理翻译:
第一个告诉我们是否需要开始翻译.
enum OverlayInFlightPosition {
case minimum
case maximum
case progressing
}
Run Code Online (Sandbox Code Playgroud)
第二个执行翻译.它使用OverlayVCscrollView的平移手势的方法.
private var overlayInFlightPosition: OverlayInFlightPosition {
let height = translatedViewHeightContraint.constant
if height == maximumHeight {
return .maximum
} else if height == minimumHeight {
return .minimum
} else {
return .progressing
}
}
Run Code Online (Sandbox Code Playgroud)
第三个动画在用户松开手指时动画翻译结束.我们使用视图的速度和当前位置计算位置.
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
}
}
Run Code Online (Sandbox Code Playgroud)
我们的overlay的委托实现看起来像:
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)
)
}
Run Code Online (Sandbox Code Playgroud)
翻译现在非常有效.但仍然存在最终问题:触摸不会传递到我们的背景视图.它们都被覆盖容器的视图拦截.我们无法设置OverlayContainerViewController,translation(in:)因为它还会禁用表视图中的交互.解决方案是地图应用程序中大量使用的解决方案isUserInteractionEnabled:
private func animateTranslationEnd() {
let position: OverlayPosition = // ... calculation based on the current overlay position & velocity
moveOverlay(to: position)
}
Run Code Online (Sandbox Code Playgroud)
它将自己从响应者链中移除.
在false:
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()
}
}
Run Code Online (Sandbox Code Playgroud)
结果如下:
你可以在这里找到代码.
如果您发现任何错误,请告诉我们!请注意,您的实现当然可以使用第二个平移手势,特别是如果您在叠加层中添加标题.
我们可以替换PassThroughView使用
OverlayContainerViewController,而非scrollViewDidEndDragging/ willEndScrollingWithVelocity当用户结束拖动滚动:
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
}
}
Run Code Online (Sandbox Code Playgroud)
我们可以使用弹簧动画并允许用户在动画时进行交互,以使运动更好:
override func loadView() {
view = PassThroughView()
}
Run Code Online (Sandbox Code Playgroud)
小智 17
尝试滑轮:
Pulley是一个易于使用的抽屉库,用于模仿iOS 10的地图应用程序中的抽屉.它公开了一个简单的API,允许您使用任何UIViewController子类作为抽屉内容或主要内容.

https://github.com/52inc/Pulley
您可以尝试我的答案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)
}
...
}
Run Code Online (Sandbox Code Playgroud)
这是另一个显示底部工作表以搜索Apple Maps等位置的示例代码.
以上对我都不起作用,因为同时平移tableview和外部视图无法顺利进行。因此,我编写了自己的代码来实现ios Maps应用程序中的预期行为。
https://github.com/OfTheWolf/UBottomSheet
2021 年的 iOS 15 增加了UISheetPresentationController,这是苹果首次公开发布的苹果地图风格的“底单”:
UISheetPresentationController
UISheetPresentationController 允许你将你的视图控制器呈现为一张表。在你展示你的视图控制器之前,用你想要的工作表的行为和外观配置它的工作表展示控制器。
片材展示控制器根据棘爪(片材自然放置的高度)指定片材的尺寸。棘爪允许纸张从其完全展开框架的一个边缘调整大小,而其他三个边缘保持固定。您可以使用 指定工作表支持的
detents制动器,并使用监视其最近选择的制动器selectedDetentIdentifier。https://developer.apple.com/documentation/uikit/uisheetpresentationcontroller
在 WWDC Session 10063: Customize and Resize Sheets in UIKit 中探索了这个新的底部表单控件
在 iOS 15 中,UISheetPresentationController已经推出了只有medium和large棘爪。
甲small棘爪是从iOS 15 API,这将需要以显示总是呈现-“坍塌”底部片材,如苹果地图明显缺少:
该medium制动被释放到手柄的使用情况下,如股份片或“•••更多”菜单中的邮件:一键触发的半张。
在 iOS 15 中,Apple Maps 现在使用这个 UIKit 表单呈现而不是自定义实现,这是朝着正确方向迈出的一大步。iOS 15 中的 Apple Maps 继续显示“小”栏,以及 1/3 的高度栏。但是这些视图大小不是开发人员可用的公共 API。
WWDC 2021 实验室的 UIKit 工程师似乎知道small制动器将是一个非常受欢迎的 UIKit 组件。我希望明年能看到 iOS 16 的 API 扩展。
| 归档时间: |
|
| 查看次数: |
66467 次 |
| 最近记录: |