¿Cómo puedo imitar la hoja inferior de la aplicación Mapas?

¿Alguien puede decirme cómo puedo imitar la última página de la nueva aplicación Mapas en iOS 10?

En Android, puede usar una BottomSheet que imita este comportamiento, pero no pude encontrar algo así para iOS.

¿Es eso una simple vista de desplazamiento con un recuadro de contenido, de modo que la barra de búsqueda está en la parte inferior?

Soy bastante nuevo en la progtwigción de iOS, así que si alguien pudiera ayudarme a crear este diseño, sería muy apreciado.

Esto es lo que quiero decir con “hoja inferior”:

captura de pantalla de la hoja inferior colapsado en Mapas

captura de pantalla de la hoja inferior expandida en Maps

No sé cómo exactamente la última hoja de la nueva aplicación Mapas responde a las interacciones del usuario. Pero puede crear una vista personalizada que se parece a la de las capturas de pantalla y agregarla a la vista principal.

Supongo que sabes cómo:

1- crear controladores de vista ya sea por storyboards o usando archivos xib.

2- usa googlemaps o MapKit de Apple.

Ejemplo

1- Crea 2 controladores de vista, p. Ej., MapViewController y BottomSheetViewController . El primer controlador alojará el mapa y el segundo es la hoja inferior.

Configurar MapViewController

Crea un método para agregar la vista inferior de la hoja.

 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) } 

Y llámalo en el método viewDidAppear:

 override func viewDidAppear(animated: Bool) { super.viewDidAppear(animated) addBottomSheetView() } 

Configurar BottomSheetViewController

1) Preparar el fondo

Crea un método para agregar efectos borrosos y de vitalidad

 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) } 

llama a este método en tu vista, apareceráApare

 override func viewWillAppear(animated: Bool) { super.viewWillAppear(animated) prepareBackgroundView() } 

Asegúrese de que el color de fondo de la vista de su controlador sea clearColor.

2) Animate bottomSheet apariencia

 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) Modifica tu xib como quieras.

4) Agregue el Reconocedor de gestos de giro a su vista.

En su método viewDidLoad, agregue UIPanGestureRecognizer.

 override func viewDidLoad() { super.viewDidLoad() let gesture = UIPanGestureRecognizer.init(target: self, action: #selector(BottomSheetViewController.panGesture)) view.addGestureRecognizer(gesture) } 

E implementa tu comportamiento gestual:

 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) } 

Hoja inferior desplazable:

Si su vista personalizada es una vista de desplazamiento o cualquier otra vista que hereda, entonces tiene dos opciones:

Primero:

Diseña la vista con una vista de encabezado y agrega panGesture al encabezado. (mala experiencia de usuario) .

Segundo:

1 – Agregue panGesture a la vista inferior de la hoja.

2 – Implemente el UIGestureRecognizerDelegate y configure el delegado panGesture en el controlador.

3- Implementar shouldRecognizeSimultaneouslyCon la función de delegado y deshabilitar la propiedad scrollView isScrollEnabled en dos casos:

  • La vista es parcialmente visible.
  • La vista es totalmente visible, la propiedad scrollView contentOffset es 0 y el usuario arrastra la vista hacia abajo.

De lo contrario, habilite el desplazamiento.

  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 } 

NOTA

En caso de que establezca .allowUserInteraction como una opción de animación, como en el proyecto de ejemplo, entonces debe habilitar el desplazamiento en el cierre de finalización de la animación si el usuario está desplazándose hacia arriba.

Proyecto de muestra

Creé un proyecto de muestra con más opciones en este repository que puede brindarle mejores ideas sobre cómo personalizar el flujo.

En la demostración, la función addBottomSheetView () controla qué vista se debe usar como una hoja inferior.

Sample Project Capturas de pantalla

– Vista parcial

enter image description here

– Vista completa

enter image description here

– Vista desplazable

enter image description here

Prueba Polea :

Pulley es una biblioteca de cajones fácil de usar que pretende imitar el cajón en la aplicación Maps de iOS 10. Expone una API simple que le permite usar cualquier subclase UIViewController como el contenido del cajón o el contenido principal.

Vista previa de la polea

https://github.com/52inc/Pulley

Creo que hay un punto importante que no se trata en las soluciones sugeridas: la transición entre el desplazamiento y la traducción.

Transición de mapas entre el desplazamiento y la traducción

En Maps, como habrás notado, cuando tableView llegue a contentOffset.y == 0 , la hoja inferior se desliza hacia arriba o hacia abajo.

El punto es complicado porque no podemos simplemente activar / desactivar el desplazamiento cuando nuestro gesto de panorámica comienza la traducción. Pararía el desplazamiento hasta que comience un nuevo toque. Este es el caso en la mayoría de las soluciones propuestas aquí.

Aquí está mi bash de implementar este movimiento.

Punto de partida: aplicación Mapas

Para comenzar nuestra investigación, visualicemos la jerarquía de vistas de Maps (inicie Maps en un simulador y seleccione Debug > Attach to process by PID or Name > Maps en Xcode).

Jerarquía de vista de depuración de mapas

No dice cómo funciona el movimiento, pero me ayudó a comprender su lógica. Puede jugar con lldb y el depurador de jerarquía de vistas.

Nuestras stacks de ViewController

Vamos a crear una versión básica de la architecture Maps ViewController.

Comenzamos con un BackgroundViewController (nuestra vista de mapa):

 class BackgroundViewController: UIViewController { override func loadView() { view = MKMapView() } } 

Ponemos tableView en un UIViewController dedicado:

 class OverlayViewController: UIViewController, UITableViewDataSource, UITableViewDelegate { lazy var tableView = UITableView() override func loadView() { view = tableView tableView.dataSource = self tableView.delegate = self } [...] } 

Ahora, necesitamos un VC para incrustar la superposición y administrar su traducción. Para simplificar el problema, consideramos que puede traducir la superposición de un punto estático OverlayPosition.maximum a otro OverlayPosition.minimum .

Por ahora solo tiene un método público para animar el cambio de posición y tiene una vista transparente:

 enum OverlayPosition { case maximum, minimum } class OverlayContainerViewController: UIViewController { let overlayViewController: OverlayViewController var translatedViewHeightContraint = ... override func loadView() { view = UIView() } func moveOverlay(to position: OverlayPosition) { [...] } } 

Finalmente, necesitamos un ViewController para incrustar todo:

 class StackViewController: UIViewController { private var viewControllers: [UIViewController] override func viewDidLoad() { super.viewDidLoad() viewControllers.forEach { gz_addChild($0, in: view) } } } 

En nuestro AppDelegate, nuestra secuencia de inicio se ve así:

 let overlay = OverlayViewController() let containerViewController = OverlayContainerViewController(overlayViewController: overlay) let backgroundViewController = BackgroundViewController() window?.rootViewController = StackViewController(viewControllers: [backgroundViewController, containerViewController]) 

La dificultad detrás de la traducción de superposición

Ahora, ¿cómo traducir nuestra superposición?

La mayoría de las soluciones propuestas utilizan un reconocedor de gestos de pan exclusivo, pero en realidad ya tenemos uno: el gesto panorámico de la vista de tabla. Además, necesitamos mantener sincronizados el desplazamiento y la traducción, y UISrollViewDelegate tiene todos los eventos que necesitamos.

Una implementación ingenua usaría un segundo gesto Pan y trataría de restablecer el contentOffset de la vista de tabla cuando ocurra la traducción:

 func panGestureAction(_ recognizer: UIPanGestureRecognizer) { if isTranslating { tableView.contentOffset = .zero } } 

Pero no funciona. TableView actualiza su contentOffset cuando se contentOffset su propia acción de reconocimiento de gestos pan o cuando se llama a su callback displayLink. No hay posibilidad de que nuestro reconocedor se active inmediatamente después de aquellos para anular exitosamente el contentOffset . Nuestra única oportunidad es participar en la fase de diseño (anulando las layoutSubviews de layoutSubviews de las layoutSubviews de la vista de desplazamiento en cada fotogtwig de la vista de desplazamiento) o responder al método didScroll del delegado llamado cada vez que se modifique contentOffset . Probemos este.

La implementación de la traducción

OverlayVC un delegado a nuestro OverlayVC para enviar los eventos de scrollview a nuestro controlador de traducción, el 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) } } 

En nuestro contenedor, hacemos un seguimiento de la traducción usando una enumeración:

 enum OverlayInFlightPosition { case minimum case maximum case progressing } 

El cálculo de la posición actual se ve así:

 private var overlayInFlightPosition: OverlayInFlightPosition { let height = translatedViewHeightContraint.constant if height == maximumHeight { return .maximum } else if height == minimumHeight { return .minimum } else { return .progressing } } 

Necesitamos 3 métodos para manejar la traducción:

El primero nos dice si necesitamos comenzar la traducción.

 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 } } 

El segundo realiza la traducción. Utiliza el método de translation(in:) del gesto panorámico de 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) ) } 

El tercero anima el final de la traducción cuando el usuario suelta su dedo. Calculamos la posición usando la velocidad y la posición actual de la vista.

 private func animateTranslationEnd() { let position: OverlayPosition = // ... calculation based on the current overlay position & velocity moveOverlay(to: position) } 

La implementación de delegado de nuestra superposición simplemente se ve así:

 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() } } 

Problema final: despachar los toques del contenedor de superposición

La traducción ahora es bastante eficiente. Pero todavía hay un problema final: los toques no se entregan a nuestra vista de fondo. Todos son interceptados por la vista del contenedor de superposición. No podemos establecer isUserInteractionEnabled en false porque también deshabilitaría la interacción en nuestra vista de tabla. La solución es la que se usa masivamente en la aplicación Mapas, 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 } } 

Se elimina de la cadena de respuesta.

En OverlayContainerViewController :

 override func loadView() { view = PassThroughView() } 

Resultado

Aquí está el resultado:

Resultado

Puedes encontrar el código aquí .

Por favor, si ve algún error, ¡hágamelo saber! Tenga en cuenta que su implementación puede, por supuesto, usar un segundo gesto de panorámica, especialmente si agrega un encabezado en su superposición.

Actualización 23/08/18

Podemos reemplazar scrollViewDidEndDragging con willEndScrollingWithVelocity lugar de enable / disable el desplazamiento cuando el usuario finaliza el arrastre:

 func scrollView(_ scrollView: UIScrollView, willEndScrollingWithVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { switch overlayInFlightPosition { case .maximum: break case .minimum, .progressing: targetContentOffset.pointee = .zero } animateTranslationEnd(following: scrollView) } 

Podemos usar una animación de spring y permitir la interacción del usuario mientras animamos para hacer que el movimiento fluya mejor:

 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) } 

Tal vez puedas probar mi respuesta https://github.com/AnYuan/AYPannel , inspirada en Pulley. Suave transición de mover el cajón a desplazarse por la lista. Agregué un gesto de panoramización en la vista de desplazamiento del contenedor y establecí shouldRecognizeSimultaneouslyWithGestureRecognizer para que devolviese SÍ. Más detalles en mi enlace github arriba. Deseo ayudar

Ninguno de los anteriores funciona para mí porque la panorámica tanto de la vista de tabla como de la vista externa simultáneamente no funciona sin problemas. Así que escribí mi propio código para lograr el comportamiento deseado en la aplicación ios Mapas.

https://github.com/OfTheWolf/UBottomSheet

hoja ubottom