web-dev-qa-db-fra.com

Comment puis-je imiter le fond de l'application Maps?

Quelqu'un peut-il me dire comment imiter le fond de la nouvelle application Cartes dans iOS 10? 

Sous Android, vous pouvez utiliser une variable BottomSheet qui imite ce comportement, mais je n’ai rien trouvé de tel pour iOS. 

S'agit-il d'une simple vue de défilement avec un encart de contenu, de sorte que la barre de recherche se trouve en bas? 

Je suis assez nouveau en programmation iOS, donc si quelqu'un pouvait m'aider à créer cette mise en page, ce serait très apprécié.

C'est ce que je veux dire par "feuille de fond":

 screenshot of the collapsed bottom sheet in Maps

 screenshot of the expanded bottom sheet in Maps

127
qwertz

Je ne sais pas comment la feuille de fond de la nouvelle application Maps répond exactement aux interactions des utilisateurs. Mais vous pouvez créer une vue personnalisée qui ressemble à celle des captures d'écran et l'ajouter à la vue principale.

Je suppose que vous savez comment:

1- Créer des contrôleurs de vue soit par storyboards ou en utilisant des fichiers xib.

2- utilisez googleMaps ou MapKit d'Apple.

Exemple

1- Créez 2 contrôleurs de vue, par exemple MapViewController et BottomSheetViewController. Le premier contrôleur hébergera la carte et le second sera la feuille inférieure elle-même.

Configurer MapViewController

Créez une méthode pour ajouter la vue de la dernière page.

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

Et appelez-le dans la méthode viewDidAppear:

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

Configurer BottomSheetViewController

1) Préparer le fond

Créer une méthode pour ajouter des effets de flou et de dynamisme

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

appelez cette méthode dans votre viewWillAppear

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

Assurez-vous que la couleur d'arrière-plan de la vue de votre contrôleur est clearColor.

2) Animer l'apparence de la feuille de fond

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) Modifiez votre xib comme vous le souhaitez.

4) Ajoutez la reconnaissance de geste panoramique à votre vue.

Dans votre méthode viewDidLoad, ajoutez UIPanGestureRecognizer.

override func viewDidLoad() {
    super.viewDidLoad()

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

}

Et implémentez votre comportement de geste:

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

Feuille de fond à défilement:

Si votre vue personnalisée est une vue de défilement ou toute autre vue héritée, vous avez donc deux options:

Premier:

Concevez la vue avec une vue en-tête et ajoutez le panGesture à l'en-tête. (mauvaise expérience utilisateur).

Seconde:

1 - Ajoutez le panGesture à la vue de dessous.

2 - Implémentez UIGestureRecognizerDelegate et définissez le délégué panGesture sur le contrôleur.

3- Implémentez la fonction déléguée shouldRecognizeSimultaneousWith et disable la propriété scrollView isScrollEnabled dans deux cas:

  • La vue est partiellement visible.
  • La vue est totalement visible, la propriété scrollView contentOffset est définie sur 0 et l'utilisateur fait glisser la vue vers le bas.

Sinon, activez le défilement.

  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
  }

REMARQUE

Si vous définissez .allowUserInteraction en tant qu'option d'animation, comme dans l'exemple de projet, vous devez donc activer le défilement lorsque la fin de l'animation est terminée si l'utilisateur fait défiler vers le haut.

Exemple de projet

J'ai créé un exemple de projet avec plus d'options sur this repo, ce qui peut vous aider à mieux comprendre comment personnaliser le flux.

Dans la démo, la fonction addBottomSheetView () contrôle quelle vue doit être utilisée comme feuille de fond.

Exemple de captures d'écran de projet

- Vue partielle

 enter image description here

- Vue complète

 enter image description here

- Voir défilement

 enter image description here

221
Ahmad Elassuty

Mise à jour du 4 décembre 2018

J'ai publié une bibliothèque basée sur cette solution https://github.com/applidium/ADOverlayContainer .

Il imite la superposition de l'application Raccourcis qui comprend:

  • Transitions douces entre un parchemin et une traduction
  • Effet bande élastique
  • Contenu déplaçable personnalisé
  • Encoches illimitées

Réponse précédente

Je pense qu'il y a un point important qui n'est pas traité dans les solutions suggérées: la transition entre le parchemin et la traduction.

 Maps transition between the scroll and the translation

Comme vous l'avez peut-être remarqué, dans Maps, lorsque la tableView atteint contentOffset.y == 0, la feuille inférieure glisse vers le haut ou vers le bas.

Le point est délicat, car nous ne pouvons pas simplement activer/désactiver le défilement lorsque notre geste panoramique commence la traduction. Cela arrêterait le défilement jusqu'à ce qu'un nouveau contact commence. C’est le cas dans la plupart des solutions proposées ici.

Voici mon essai de mettre en œuvre cette motion.

Point de départ: application Maps

Pour commencer notre enquête, visualisons la hiérarchie de vues de Maps (démarrez Maps sur un simulateur et sélectionnez Debug> Attach to process by PID or Name> Maps dans Xcode 9).

 Maps debug view hierarchy

Cela ne dit pas comment fonctionne la motion, mais cela m'a aidé à en comprendre la logique. Vous pouvez jouer avec le débogueur lldb et la hiérarchie des vues.

Nos piles ViewController

Créons une version de base de l'architecture Maps ViewController.

Nous commençons avec une BackgroundViewController (notre vue cartographique):

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

Nous plaçons la tableView dans une UIViewController dédiée:

class OverlayViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {

    lazy var tableView = UITableView()

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

    [...]
}

Nous avons maintenant besoin d’un VC pour intégrer la superposition et gérer sa traduction. Pour simplifier le problème, nous considérons qu’il peut traduire la superposition d’un point statique OverlayPosition.maximum en un autre OverlayPosition.minimum

Pour l'instant, il ne dispose que d'une méthode publique pour animer le changement de position et sa vue est transparente:

enum OverlayPosition {
    case maximum, minimum
}

class OverlayContainerViewController: UIViewController {

    let overlayViewController: OverlayViewController
    var translatedViewHeightContraint = ...

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

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

Enfin, nous avons besoin d’un ViewController pour intégrer le tout: 

class StackViewController: UIViewController {

    private var viewControllers: [UIViewController]

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

Dans notre AppDelegate, notre séquence de démarrage ressemble à ceci:

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

La difficulté derrière la traduction en superposition

Maintenant, comment traduire notre superposition?

La plupart des solutions proposées utilisent un outil de reconnaissance pan-gestes dédié, mais nous en avons déjà un: le pan-geste de la vue tableau. .__ De plus, nous devons garder le parchemin et la traduction synchronisés et la UIScrollViewDelegate contient tous les événements dont nous avons besoin!

Une implémentation naïve utiliserait un second mouvement de panoramique et essayerait de réinitialiser la contentOffset de la vue table lorsque la traduction se produirait:

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

Mais ça ne marche pas. TableView met à jour sa contentOffset lorsque sa propre action de reconnaissance du geste panoramique est déclenchée ou lorsque son rappel displayLink est appelé. Il n'y a aucune chance que notre programme de reconnaissance se déclenche juste après ceux-ci pour remplacer avec succès la variable contentOffset. Notre seule chance est soit de prendre part à la phase de mise en page (en remplaçant layoutSubviews des appels de vue de défilement à chaque image de la vue de défilement), soit de répondre à la méthode didScroll du délégué appelé chaque fois que la contentOffset est modifiée. Essayons celui-ci.

La traduction mise en œuvre

Nous ajoutons un délégué à notre OverlayVC pour envoyer les événements de scrollview à notre gestionnaire de traduction, le 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)
    }
}

Dans notre conteneur, nous gardons une trace de la traduction en utilisant un enum:

enum OverlayInFlightPosition {
    case minimum
    case maximum
    case progressing
}

Le calcul de la position actuelle ressemble à ceci:

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

Nous avons besoin de 3 méthodes pour gérer la traduction:

Le premier nous dit si nous devons commencer la traduction.

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

Le second effectue la traduction. Il utilise la méthode translation(in:) du geste de panoramique 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)
    )
}

Le troisième anime la fin de la traduction lorsque l'utilisateur relâche son doigt. Nous calculons la position en utilisant la vitesse et la position actuelle de la vue.

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

L'implémentation des délégués de notre superposition ressemble simplement à:

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

Problème final: envoyer les touches du conteneur de superposition

La traduction est maintenant assez efficace. Mais il reste un dernier problème: les touches ne sont pas livrées à notre vue d’arrière-plan. Ils sont tous interceptés par la vue du conteneur de superposition. Nous ne pouvons pas définir isUserInteractionEnabled sur false car cela désactiverait également l'interaction dans notre vue tableau. La solution est celle utilisée massivement dans l'application 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
    }
}

Il se supprime de la chaîne de répondeurs.

Dans OverlayContainerViewController:

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

Résultat

Voici le résultat:

 Result

Vous pouvez trouver le code ici .

S'il vous plaît, si vous voyez des bugs, faites le moi savoir! Notez que votre implémentation peut bien sûr utiliser un second geste de panoramique, spécialement si vous ajoutez un en-tête dans votre superposition. 

Mise à jour 23/08/18

Nous pouvons remplacer scrollViewDidEndDragging par willEndScrollingWithVelocity plutôt que enable/disable le défilement lorsque l'utilisateur finit de faire glisser:

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

Nous pouvons utiliser une animation printanière et permettre une interaction utilisateur tout en animant pour améliorer la fluidité:

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

Essayez Poulie:

Pulley est une bibliothèque de tiroirs facile à utiliser et conçue pour imiter le tiroir dans l'application Maps d'iOS 10. Il expose une API simple qui vous permet d'utiliser toute sous-classe UIViewController en tant que contenu du tiroir ou primaire contenu.

Pulley Preview

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

13
Rajee Jones

Vous pouvez essayer ma réponse https://github.com/SCENEE/FloatingPanel . Il fournit un contrôleur de vue de conteneur pour afficher une interface "de fond". 

Il est facile à utiliser et ne vous gêne pas pour la manipulation de reconnaissance de geste! Vous pouvez également suivre une vue de défilement (ou la vue jumelle) dans une feuille de fond si nécessaire.

Ceci est un exemple simple. Veuillez noter que vous devez préparer un contrôleur de vue pour afficher votre contenu dans une feuille de fond.

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

Ici est un autre exemple de code pour afficher une feuille de fond permettant de rechercher un emplacement tel que Apple Maps.

 Sample App like Apple Maps

6
SCENEE

Peut-être que vous pouvez essayer ma réponse https://github.com/AnYuan/AYPannel , inspiré par Pulley. Transition en douceur du déplacement du tiroir au défilement de la liste. J'ai ajouté un geste de panoramique sur la vue de défilement du conteneur et je devrais définir ReconnaîtreSimultanémentAvecGestureRecognizer pour renvoyer YES. Plus de détails dans mon lien github ci-dessus. Souhaite aider.

1
day

Aucune de ce qui précède ne fonctionne pour moi parce que le panoramique de la vue de la table et de la vue extérieure simultanément ne fonctionne pas correctement. J'ai donc écrit mon propre code pour obtenir le comportement souhaité dans l'application iOS Maps.

https://github.com/OfTheWolf/UBottomSheet

 ubottom sheet

1
ugur