web-dev-qa-db-de.com

Wie kann ich das unterste Blatt der Google Maps-App nachahmen?

Kann mir jemand sagen, wie ich das untere Blatt in der neuen Google Maps-App in iOS 10 nachahmen kann? 

In Android können Sie eine BottomSheet verwenden, die dieses Verhalten imitiert, aber für iOS konnte ich nichts dergleichen finden. 

Handelt es sich um eine einfache Bildlaufansicht mit Inhaltseinfügung, so dass sich die Suchleiste unten befindet? 

Ich bin relativ neu in der iOS-Programmierung. Wenn mir jemand bei der Erstellung dieses Layouts helfen könnte, wäre das sehr zu schätzen.

Das meine ich mit "unterem Blatt":

 screenshot of the collapsed bottom sheet in Maps

 screenshot of the expanded bottom sheet in Maps

127
qwertz

Ich weiß nicht, wie genau das untere Blatt der neuen Maps-App auf Benutzerinteraktionen reagiert. Sie können jedoch eine benutzerdefinierte Ansicht erstellen, die der in den Screenshots ähnelt, und sie der Hauptansicht hinzufügen.

Ich gehe davon aus, dass Sie wissen, wie Sie

1- Erstellen Sie View-Controller entweder über Storyboards oder mithilfe von Xib-Dateien.

2- Verwenden Sie googleMaps oder Apples MapKit.

Beispiel

1- Erstellen Sie zwei Ansichtssteuerungen, z. B. MapViewController und BottomSheetViewController. Der erste Controller hostet die Karte und der zweite ist das unterste Blatt.

Konfigurieren Sie MapViewController

Erstellen Sie eine Methode zum Hinzufügen der unteren Blattansicht.

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

Und rufen Sie es in der viewDidAppear-Methode auf:

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

Konfigurieren Sie BottomSheetViewController

1) Bereiten Sie den Hintergrund vor

Erstellen Sie eine Methode zum Hinzufügen von Unschärfe- und Lebendigkeitseffekten

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

rufen Sie diese Methode in Ihrem viewWillAppear auf

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

Stellen Sie sicher, dass die Hintergrundfarbe Ihres Controllers clearColor ist.

2) Animieren Sie das bottomSheet-Erscheinungsbild

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) Ändern Sie Ihr Xib wie gewünscht.

4) Fügen Sie Pan Gestenerkennung Ihrer Ansicht hinzu.

Fügen Sie in Ihrer viewDidLoad-Methode UIPanGestureRecognizer hinzu.

override func viewDidLoad() {
    super.viewDidLoad()

    let gesture = UIPanGestureRecognizer.init(target: self, action: #selector(BottomSheetViewController.panGesture))
    view.addGestureRecognizer(gesture)

}

Und implementiere dein Gestenverhalten:

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

Scrollbares unteres Blatt:

Wenn Ihre benutzerdefinierte Ansicht eine Bildlaufansicht oder eine andere Ansicht ist, von der erbt, haben Sie zwei Optionen:

Zuerst:

Entwerfen Sie die Ansicht mit einer Kopfansicht und fügen Sie der Kopfzeile panGesture hinzu. (schlechte Benutzererfahrung).

Zweite:

1 - Fügen Sie panGesture der unteren Blattansicht hinzu.

2 - Implementieren Sie den UIGestureRecognizerDelegate und setzen Sie den panGesture-Delegaten auf den Controller.

3- Implementieren Sie shouldRecognizeSimultaneousWith delegate function und disable die scrollView isScrollEnabled - Eigenschaft in zwei Fällen:

  • Die Ansicht ist teilweise sichtbar.
  • Die Ansicht ist vollständig sichtbar, die Eigenschaft scrollView contentOffset ist 0 und der Benutzer zieht die Ansicht nach unten.

Aktivieren Sie andernfalls das Scrollen.

  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
  }

HINWEIS

Falls Sie .allowUserInteraction als Animationsoption wie im Beispielprojekt festgelegt haben, , müssen Sie den Bildlauf beim Abschluss der Animation beenden, wenn der Benutzer einen Bildlauf vorn durchführt.

Beispielprojekt

Ich habe ein Beispielprojekt mit mehr Optionen für this repo erstellt, das Ihnen bessere Einblicke in die Anpassung des Flusses geben kann.

In der Demo steuert die Funktion addBottomSheetView (), welche Ansicht als unteres Blatt verwendet werden soll.

Beispielprojekt-Screenshots

- Teilansicht

 enter image description here

- Vollansicht

 enter image description here

- Scrollbare Ansicht

 enter image description here

221
Ahmad Elassuty

Update 4 Dezember 2018

Ich habe eine Bibliothek veröffentlicht, die auf dieser Lösung basiert https://github.com/applidium/ADOverlayContainer .

Es ahmt die Überlagerung der Shortcuts-App nach, die Folgendes beinhaltet:

  • Reibungslose Übergänge zwischen einer Schriftrolle und einer Übersetzung
  • Gummibandeffekt
  • Benutzerdefinierte Inhalte zum Ziehen
  • Unbegrenzte Kerben

Vorherige Antwort

Ich denke, es gibt einen wichtigen Punkt, der in den vorgeschlagenen Lösungen nicht behandelt wird: der Übergang zwischen der Schriftrolle und der Übersetzung.

 Maps transition between the scroll and the translation

Wie Sie vielleicht bemerkt haben, wird das untere Blatt in Maps nach oben gleiten, wenn die tableView contentOffset.y == 0 erreicht.

Der Punkt ist knifflig, weil wir die Bildlaufleiste nicht einfach aktivieren/deaktivieren können, wenn mit der Pan-Bewegung die Übersetzung beginnt. Es würde die Schriftrolle anhalten, bis eine neue Berührung beginnt. Dies ist bei den meisten hier vorgeschlagenen Lösungen der Fall.

Hier ist mein Versuch, diesen Antrag umzusetzen.

Ausgangspunkt: Karten App

Um unsere Untersuchung zu beginnen, visualisieren wir die Ansichtshierarchie von Maps (starten Sie Maps in einem Simulator und wählen Sie in Xcode 9 Debug> Attach to process by PID or Name> Maps).

 Maps debug view hierarchy

Es verrät nicht, wie die Bewegung funktioniert, aber es half mir, die Logik davon zu verstehen. Sie können mit dem lldb- und dem View-Hierarchie-Debugger spielen.

Unsere ViewController-Stapel

Erstellen wir eine Basisversion der Maps ViewController-Architektur.

Wir beginnen mit einer BackgroundViewController (unserer Kartenansicht):

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

Wir haben die tableView in eine dedizierte UIViewController eingefügt:

class OverlayViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {

    lazy var tableView = UITableView()

    override func loadView() {
        view = tableView
        tableView.dataSource = self
        tableView.delegate = self
    }

    [...]
}

Jetzt brauchen wir ein VC, um das Overlay einzubetten und seine Übersetzung zu verwalten. Zur Vereinfachung des Problems können wir die Überlagerung von einem statischen Punkt OverlayPosition.maximum in einen anderen OverlayPosition.minimum übersetzen. 

Momentan gibt es nur eine öffentliche Methode zum Animieren der Positionsänderung und eine transparente Ansicht:

enum OverlayPosition {
    case maximum, minimum
}

class OverlayContainerViewController: UIViewController {

    let overlayViewController: OverlayViewController
    var translatedViewHeightContraint = ...

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

    func moveOverlay(to position: OverlayPosition) {
        [...]
    }
}

Zum Schluss brauchen wir einen ViewController, um alle einzubetten: 

class StackViewController: UIViewController {

    private var viewControllers: [UIViewController]

    override func viewDidLoad() {
        super.viewDidLoad()
        viewControllers.forEach { gz_addChild($0, in: view) }
    }
}

In unserer AppDelegate sieht unsere Startsequenz folgendermaßen aus:

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

Die Schwierigkeit hinter der Overlay-Übersetzung

Nun, wie kann unser Overlay übersetzt werden?

Die meisten der vorgeschlagenen Lösungen verwenden eine dedizierte Pan-Gestenerkennung, aber tatsächlich haben wir bereits eine: die Pan-Geste der Tabellenansicht. Außerdem müssen wir die Bildlaufleiste und die Übersetzung synchron halten, und die UIScrollViewDelegate hat alle Ereignisse, die wir brauchen!

Eine naive Implementierung würde eine zweite pan-Geste verwenden und versuchen, die contentOffset der Tabellenansicht zurückzusetzen, wenn die Übersetzung auftritt:

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

Aber es funktioniert nicht. Die tableView aktualisiert ihre contentOffset, wenn ihre eigene Aktion zur Erkennung der Schwenkbewegung ausgelöst wird oder wenn ihr displayLink-Callback aufgerufen wird. Es besteht keine Chance, dass unser Erkennungsprogramm direkt nach diesen auslöst, um die contentOffset erfolgreich zu überschreiben. Unsere einzige Chance besteht darin, entweder an der Layoutphase teilzunehmen (indem Sie layoutSubviews der Aufrufe der Bildlaufansicht an jedem Frame der Bildlaufansicht überschreiben) oder auf die Methode didScroll des Delegaten zu antworten, die bei jeder Änderung der contentOffset aufgerufen wird. Lass uns dies versuchen.

Die Umsetzung der Übersetzung

Wir fügen unserer OverlayVC einen Delegierten hinzu, um die Ereignisse des Scrollview an unseren Übersetzungs-Handler, die OverlayContainerViewController, zu senden:

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

In unserem Container verfolgen wir die Übersetzung mit einem Enum:

enum OverlayInFlightPosition {
    case minimum
    case maximum
    case progressing
}

Die aktuelle Positionsberechnung sieht folgendermaßen aus:

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

Wir benötigen 3 Methoden, um die Übersetzung zu handhaben:

Der erste sagt uns, ob wir mit der Übersetzung beginnen müssen.

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

Der zweite führt die Übersetzung durch. Es verwendet die translation(in:)-Methode der Pan-Geste der 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)
    )
}

Der dritte animiert das Ende der Übersetzung, wenn der Benutzer seinen Finger loslässt. Wir berechnen die Position anhand der Geschwindigkeit und der aktuellen Position der Ansicht.

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

Die Delegaten-Implementierung unseres Overlays sieht einfach so aus:

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

Letztes Problem: Versenden Sie die Berührungen des Overlay-Containers

Die Übersetzung ist jetzt ziemlich effizient. Es gibt jedoch noch ein letztes Problem: Die Hintergrundinformationen werden nicht angezeigt. Sie werden alle von der Ansicht des Overlay-Containers abgefangen. Wir können isUserInteractionEnabled nicht auf false setzen, da dies auch die Interaktion in unserer Tabellensicht deaktivieren würde. Die Lösung ist die massiv in der Maps-App verwendete 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
    }
}

Es entfernt sich aus der Responder-Kette.

In OverlayContainerViewController:

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

Ergebnis

Hier ist das Ergebnis:

 Result

Den Code finden Sie hier .

Bitte, wenn Sie Fehler sehen, lassen Sie es mich wissen! Beachten Sie, dass Ihre Implementierung natürlich eine zweite Pan-Geste verwenden kann, insbesondere wenn Sie einen Header in Ihrer Überlagerung hinzufügen. 

Update vom 23.08.18

Wir können scrollViewDidEndDragging durch .__ ersetzen. willEndScrollingWithVelocity statt enable/disable der Bildlauf, wenn der Benutzer das Ziehen beendet:

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

Wir können eine Federanimation verwenden und die Interaktion des Benutzers während der Animation ermöglichen, um den Bewegungsfluss zu verbessern:

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)
}
32
GaétanZ

Versuchen Sie es mit Riemenscheibe:

Pulley ist eine benutzerfreundliche Schubladenbibliothek, die die Schublade imitieren soll in der iOS 10-Karten-App. Es stellt eine einfache API zur Verfügung, mit der Sie .__ verwenden können. eine beliebige UIViewController-Unterklasse als Schubladeninhalt oder primäre Inhalt.

Pulley Preview

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

13
Rajee Jones

Sie können meine Antwort probieren https://github.com/SCENEE/FloatingPanel . Es stellt einen Container-View-Controller zur Verfügung, um eine "unterste Blatt" -Schnittstelle anzuzeigen. 

Es ist einfach zu bedienen und es macht Ihnen nichts aus, mit der Gestenerkennung umzugehen! Bei Bedarf können Sie auch eine Bildlaufansicht (oder die Geschwisteransicht) in einem unteren Blatt nachverfolgen.

Dies ist ein einfaches Beispiel. Bitte beachten Sie, dass Sie einen View Controller vorbereiten müssen, um Ihren Inhalt in einem unteren Blatt anzuzeigen.

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

Hier ist ein weiterer Beispielcode, um ein unteres Blatt anzuzeigen, um einen Ort wie Apple Maps zu suchen.

 Sample App like Apple Maps

6
SCENEE

Vielleicht können Sie meine Antwort https://github.com/AnYuan/AYPannel versuchen, die von Pulley inspiriert ist. Reibungsloser Übergang vom Verschieben der Schublade zum Scrollen der Liste. Ich habe eine Pan-Geste in der Container-Bildlaufansicht hinzugefügt und setzeRecognizeSimultWithGestureRecognizer so ein, dass YES zurückgegeben wird. Mehr Details in meinem Github-Link oben. Ich möchte helfen.

1
day

Keine der oben genannten Funktionen funktioniert für mich, da das gleichzeitige Verschieben der Tabellenansicht und der Außenansicht nicht reibungslos funktioniert. Also habe ich meinen eigenen Code geschrieben, um das beabsichtigte Verhalten in der ios Maps-App zu erreichen.

https://github.com/OfTheWolf/UBottomSheet

 ubottom sheet

1
ugur