Come posso simulare il foglio inferiore dall’app Maps?

Qualcuno può dirmi come posso imitare il foglio di sotto nella nuova app di Maps in iOS 10?

In Android, puoi usare un BottomSheet che riproduce questo comportamento, ma non ho trovato nulla di simile per iOS.

Si tratta di una semplice vista di scorrimento con un contenuto inserito, in modo che la barra di ricerca sia in fondo?

Sono abbastanza nuovo per la programmazione iOS, quindi se qualcuno potrebbe aiutarmi a creare questo layout, sarebbe molto apprezzato.

Questo è ciò che intendo per “foglio di sotto”:

screenshot del foglio inferiore compresso in Maps

screenshot del foglio inferiore espanso in Maps

Non so come esattamente il foglio inferiore della nuova app di Maps, risponda alle interazioni dell’utente. Ma puoi creare una vista personalizzata simile a quella degli screenshot e aggiungerla alla vista principale.

Presumo tu sappia come:

1- creare controller di visualizzazione tramite storyboard o file xib.

2- utilizzare googleMaps o MapKit di Apple.

Esempio

1- Creare 2 controller di vista, ad esempio, MapViewController e BottomSheetViewController . Il primo controller ospiterà la mappa e il secondo è il foglio inferiore stesso.

Configura MapViewController

Creare un metodo per aggiungere la vista del foglio di base.

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

E chiamalo nel metodo viewDidAppear:

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

Configura BottomSheetViewController

1) Prepara lo sfondo

Crea un metodo per aggiungere effetti di sfocatura e vividezza

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

chiama questo metodo nella tua viewWillAppear

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

Assicurati che il colore di sfondo della vista del controller sia clearColor.

2) Anima l’aspetto di bottomSheet

 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 il tuo xib come vuoi.

4) Aggiungi Pan Gesture Recognizer alla tua vista.

Nel tuo metodo viewDidLoad aggiungi UIPanGestureRecognizer.

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

E attua il tuo comportamento gestuale:

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

Lamina inferiore scorrevole:

Se la tua vista personalizzata è una vista di scorrimento o qualsiasi altra vista che eredita da, quindi hai due opzioni:

Primo:

Progettare la vista con una vista dell’intestazione e aggiungere il panGesture all’intestazione. (esperienza utente negativa) .

Secondo:

1 – Aggiungi il panGesture alla vista del foglio inferiore.

2 – Implementare UIGestureRecognizerDelegate e impostare il delegato di panGesture sul controller.

3- L’ attrezzo dovrebbe riconoscere simultaneamente la funzione di delega e disabilitare la proprietà scrollView isScrollEnabled in due casi:

  • La vista è parzialmente visibile.
  • La vista è totalmente visibile, la proprietà scrollView contentOffset è 0 e l’utente sta trascinando la vista verso il basso.

Altrimenti abilitare lo scorrimento.

  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

Se si imposta .allowUserInteraction come opzione di animazione, come nel progetto di esempio, è necessario abilitare lo scorrimento sulla chiusura del completamento dell’animazione se l’utente sta scorrendo verso l’alto.

Progetto di esempio

Ho creato un progetto di esempio con più opzioni su questo repository che potrebbe darti informazioni migliori su come personalizzare il stream.

Nella demo, la funzione addBottomSheetView () controlla quale vista deve essere utilizzata come foglio inferiore.

Schermate di esempio del progetto

– Vista parziale

inserisci la descrizione dell'immagine qui

– Vista completa

inserisci la descrizione dell'immagine qui

– Vista scorrevole

inserisci la descrizione dell'immagine qui

Prova la puleggia :

Pulley è una libreria di cassetti facile da usare pensata per imitare il cassetto nell’app Maps di iOS 10. Espone una semplice API che consente di utilizzare qualsiasi sottoclass UIViewController come contenuto del cassetto o del contenuto principale.

Anteprima della puleggia

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

Penso che ci sia un punto significativo che non viene trattato nelle soluzioni suggerite: la transizione tra lo scroll e la traduzione.

Transizione delle mappe tra lo scroll e la traduzione

In Maps, come forse avrai notato, quando tableView raggiunge contentOffset.y == 0 , il foglio di fondo scorre o scende.

Il punto è difficile perché non possiamo semplicemente abilitare / disabilitare lo scroll quando il nostro gesto pan inizia la traduzione. Fermerebbe la pergamena fino all’inizio di un nuovo touch. Questo è il caso della maggior parte delle soluzioni proposte qui.

Ecco il mio tentativo di implementare questa mozione.

Punto di partenza: App Maps

Per iniziare la nostra indagine, mostriamo la gerarchia delle viste di Maps (avvia Maps su un simulatore e seleziona Debug > Attach to process by PID or Name > Maps in Xcode).

Mappa della gerarchia delle viste di debug

Non dice come funziona il movimento, ma mi ha aiutato a comprenderne la logica. Puoi giocare con lldb e il debugger della gerarchia della vista.

I nostri stack ViewController

Creiamo una versione base dell’architettura di ViewController di Maps.

Iniziamo con BackgroundViewController (la nostra vista mappa):

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

Mettiamo la tabellaView in un UIViewController dedicato:

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

Ora, abbiamo bisogno di un VC per incorporare l’overlay e gestire la sua traduzione. Per semplificare il problema, consideriamo che può OverlayPosition.maximum l’overlay da un punto statico OverlayPosition.maximum a un altro OverlayPosition.minimum .

Per ora ha solo un metodo pubblico per animare il cambio di posizione e ha una vista trasparente:

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

Infine abbiamo bisogno di un ViewController per incorporare il tutto:

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

Nel nostro AppDelegate, la nostra sequenza di avvio è simile alla seguente:

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

La difficoltà dietro la traduzione di sovrapposizione

Ora, come tradurre il nostro overlay?

La maggior parte delle soluzioni proposte utilizza un riconoscimento di gesture di panoramica dedicato, ma in realtà ne abbiamo già uno: il gesto pan della vista tabella. Inoltre, abbiamo bisogno di mantenere la scroll e la traduzione sincronizzati e UISrollViewDelegate ha tutti gli eventi di cui abbiamo bisogno!

Un’implementazione ingenua userebbe un secondo Gesto e provò a resettare il contentOffset della vista tabella quando si verifica la traduzione:

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

Ma non funziona. Il TableView aggiorna il suo contentOffset quando sono contentOffset i propri trigger di azione di riconoscimento di gesture pan o quando viene richiamato il suo callback displayLink. Non vi è alcuna possibilità che il nostro riconoscitore si inneschi subito dopo che questi abbia sostituito con successo il contentOffset . La nostra unica possibilità è quella di prendere parte alla fase di layout (sovrascrivendo layoutSubviews delle chiamate di visualizzazione a scorrimento in ogni frame della vista di scorrimento) o di rispondere al metodo didScroll del delegato chiamato ogni volta che contentOffset viene modificato. Proviamo questo.

L’implementazione della traduzione

Aggiungiamo un delegato al nostro OverlayVC per inviare gli eventi della scrollview al nostro gestore di traduzioni 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) } } 

Nel nostro contenitore, teniamo traccia della traduzione utilizzando un enum:

 enum OverlayInFlightPosition { case minimum case maximum case progressing } 

Il calcolo della posizione attuale ha il seguente aspetto:

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

Abbiamo bisogno di 3 metodi per gestire la traduzione:

Il primo ci dice se dobbiamo iniziare la traduzione.

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

Il secondo esegue la traduzione. Usa la translation(in:) metodo translation(in:) del gesto pan di 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) ) } 

Il terzo anima la fine della traduzione quando l’utente rilascia il dito. Calcoliamo la posizione utilizzando la velocità e la posizione corrente della vista.

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

L’implementazione dei delegati del nostro overlay sembra semplicemente:

 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 finale: inviare i tocchi del contenitore di sovrapposizione

La traduzione è ora abbastanza efficiente. Ma c’è ancora un problema finale: i tocchi non vengono consegnati alla nostra vista di sfondo. Sono tutti intercettati dalla vista del contenitore di sovrapposizione. Non possiamo impostare isUserInteractionEnabled su false perché disabiliterebbe anche l’interazione nella nostra vista tabella. La soluzione è quella utilizzata in modo massiccio nell’app 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 } } 

Si rimuove dalla catena del risponditore.

In OverlayContainerViewController :

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

Risultato

Ecco il risultato:

Risultato

Puoi trovare il codice qui .

Per favore se vedi qualche bug, fammi sapere! Nota che la tua implementazione può ovviamente utilizzare un secondo gesto pan, specialmente se aggiungi un’intestazione nel tuo overlay.

Aggiornamento 23/08/18

Possiamo sostituire scrollViewDidEndDragging con willEndScrollingWithVelocity invece di enable / disable lo scroll quando l’utente termina il trascinamento:

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

Possiamo utilizzare un’animazione a molla e consentire l’interazione dell’utente durante l’animazione per rendere il stream migliore:

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

Forse puoi provare la mia risposta https://github.com/AnYuan/AYPannel , ispirata da Pulley. Passaggio fluido dallo spostamento del cassetto allo scorrimento della lista. Ho aggiunto un gesto di panoramica sulla vista di scorrimento del contenitore e impostato shouldRecognizeSimultaneousWithGestureRecognizer per restituire SÌ. Maggiori dettagli nel mio link github sopra. Vorrei aiutare.

Nessuno di quanto sopra funziona per me perché il panning sia della vista tabella che della vista esterna simultaneamente non funziona correttamente. Così ho scritto il mio codice per ottenere il comportamento desiderato nell’app di ios Maps.

https://github.com/OfTheWolf/UBottomSheet

foglio di ubottom