ios - mejor - ¿Cómo puedo imitar la hoja inferior de la aplicación Mapas?
mapas iphone no funciona (6)
Actualización 4 de diciembre de 2018
Lancé una biblioteca basada en esta solución https://github.com/applidium/ADOverlayContainer .
Imita la superposición de la aplicación Atajos. Vea este artículo para más detalles.
El componente principal de la biblioteca es
OverlayContainerViewController
.
Define un área donde un controlador de vista se puede arrastrar hacia arriba y hacia abajo, ocultando o revelando el contenido debajo de él.
let contentController = MapsViewController()
let overlayController = SearchViewController()
let containerController = OverlayContainerViewController()
containerController.delegate = self
containerController.viewControllers = [
contentController,
overlayController
]
window?.rootViewController = containerController
Implemente
OverlayContainerViewControllerDelegate
para especificar el número de muescas deseadas:
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
}
}
Respuesta anterior
Creo que hay un punto significativo que no se trata en las soluciones sugeridas: la transición entre el desplazamiento y la traducción.
En Maps, como habrás notado, cuando tableView alcanza
contentOffset.y == 0
, la hoja inferior se desliza hacia arriba o hacia abajo.
El punto es complicado porque no podemos simplemente habilitar / deshabilitar el desplazamiento cuando nuestro gesto panorámico 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 intento de implementar esta moción.
Punto de partida: aplicación de 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 9).
No dice cómo funciona el movimiento, pero me ayudó a entender la lógica del mismo. Puedes jugar con el lldb y el depurador de jerarquía de vistas.
Nuestras pilas ViewController
Creemos una versión básica de la arquitectura Maps ViewController.
Comenzamos con un
BackgroundViewController
(nuestra vista de mapa):
class BackgroundViewController: UIViewController {
override func loadView() {
view = MKMapView()
}
}
Ponemos el 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 superpuesta
Ahora, ¿cómo traducir nuestra superposición?
La mayoría de las soluciones propuestas usan un reconocedor de gestos panorámicos dedicado, 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
UIScrollViewDelegate
tiene todos los eventos que necesitamos!
Una implementación ingenua usaría un segundo gesto panorámico e intentaría restablecer el contenido
contentOffset
de la vista de tabla cuando se produzca 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 panorámicos o cuando se llama a su devolución de llamada displayLink.
No hay posibilidad de que nuestro reconocedor se active inmediatamente después de aquellos para anular con éxito el
contentOffset
.
Nuestra única posibilidad es participar en la fase de diseño (anulando las
layoutSubviews
de
layoutSubviews
de las vistas de desplazamiento en cada cuadro de la vista de desplazamiento) o responder al método
didScroll
del delegado llamado cada vez que se modifica
contentOffset
.
Probemos con 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,
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 de desplazamiento 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: despache 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:
Puedes encontrar el código here .
Por favor, si ve algún error, ¡avíseme! Tenga en cuenta que su implementación puede, por supuesto, usar un segundo gesto de desplazamiento, 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 termina de arrastrar:
func scrollView(_ scrollView: UIScrollView,
willEndScrollingWithVelocity velocity: CGPoint,
targetContentOffset: UnsafeMutablePointer<CGPoint>) {
switch overlayInFlightPosition {
case .maximum:
break
case .minimum, .progressing:
targetContentOffset.pointee = .zero
}
animateTranslationEnd(following: scrollView)
}
Podemos usar una animación de primavera y permitir la interacción del usuario mientras animamos para 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)
}
¿Alguien puede decirme cómo puedo imitar la hoja inferior en la nueva aplicación Maps en iOS 10?
En Android, puede usar una
BottomSheet
que imita este comportamiento, pero no pude encontrar algo así para iOS.
¿Es una simple vista de desplazamiento con un contenido insertado, de modo que la barra de búsqueda está en la parte inferior?
Soy bastante nuevo en la programación de iOS, por lo que si alguien pudiera ayudarme a crear este diseño, sería muy apreciado.
Esto es lo que quiero decir con "hoja inferior":
Nada de lo anterior funciona para mí porque la panorámica tanto de la vista de tabla como de la vista exterior simultáneamente no funciona sin problemas. Entonces escribí mi propio código para lograr el comportamiento previsto en la aplicación ios Maps.
No sé cómo responde exactamente la hoja inferior de la nueva aplicación Maps a las interacciones de los usuarios. Pero puede crear una vista personalizada que se parezca a la de las capturas de pantalla y agregarla a la vista principal.
Supongo que sabes cómo:
1- crea controladores de vista ya sea por guiones gráficos o usando archivos xib.
2- usa googleMaps o el MapKit de Apple.
Ejemplo
1- Cree 2 controladores de vista, por ejemplo, MapViewController y BottomSheetViewController . El primer controlador alojará el mapa y el segundo es la hoja inferior.
Configurar MapViewController
Cree un método para agregar la vista de la hoja inferior.
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
Cree un método para agregar efectos de desenfoque y 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)
}
llame a este método en su vista
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) animar la apariencia de la hoja inferior
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 Pan Gesture Recognizer 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 implemente su 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 herede, entonces tiene dos opciones:
Primero:
Diseñe la vista con una vista de encabezado y agregue panGesture al encabezado. (mala experiencia del usuario) .
Segundo:
1 - Agregue panGesture a la vista de la hoja inferior.
2 - Implemente el UIGestureRecognizerDelegate y configure el delegado panGesture en el controlador.
3- Implemente shouldRecognizeSimuallylyWith delegate y deshabilita 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 muestra, debe habilitar el desplazamiento en el cierre de finalización de la animación si el usuario se desplaza hacia arriba.
Proyecto de muestra
Creé un proyecto de muestra con más opciones en this repositorio que puede brindarle una mejor perspectiva sobre cómo personalizar el flujo.
Ejemplos de capturas de pantalla del proyecto
- Vista parcial
- Vista completa
- Vista desplazable
Puedes probar mi respuesta https://github.com/SCENEE/FloatingPanel . Proporciona un controlador de vista de contenedor para mostrar una interfaz de "hoja inferior".
¡Es fácil de usar y no le importa el manejo del reconocedor de gestos! También puede rastrear una vista de desplazamiento (o la vista de hermanos) en una hoja inferior si es necesario.
Este es un ejemplo simple. Tenga en cuenta que debe preparar un controlador de vista para mostrar su contenido en una hoja inferior.
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 hay otro código de ejemplo para mostrar una hoja inferior para buscar una ubicación como Apple Maps.
Quizás puedas probar mi respuesta https://github.com/AnYuan/AYPannel , inspirada en Pulley. Transición suave de mover el cajón a desplazarse por la lista. Agregué un gesto panorámico en la vista de desplazamiento del contenedor y configuré shouldRecognizeSim simultáneamenteWithGestureRecognizer para devolver YES. Más detalles en mi enlace de github arriba. Deseo ayudar