250 votes

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

Quelqu'un peut-il me dire comment je peux imiter du bas de la feuille dans la nouvelle application Maps dans iOS 10?

Dans Android, vous pouvez utiliser un BottomSheet qui imite ce comportement, mais je ne pouvais pas trouver quoi que ce soit pour iOS.

C'est qu'un simple défilement de l'affichage avec un contenu en médaillon, de sorte que la barre de recherche est en bas?

Je suis assez nouveau à l'iOS de programmation donc si quelqu'un pouvait m'aider à la création de cette disposition, qui serait fortement appréciée.

C'est ce que je veux dire par "en bas de la feuille":

screenshot of the collapsed bottom sheet in Maps

screenshot of the expanded bottom sheet in Maps

388voto

Ahmad Elassuty Points 2588

Je ne sais pas exactement comment le bas de la feuille de la nouvelle app plans, répond aux interactions de l'utilisateur. Mais vous pouvez créer une vue personnalisée qui ressemble à celui dans les captures d'écran et l'ajouter à la vue principale.

Je suppose que vous savez comment faire:

1 - créer la vue des contrôleurs, soit par des story-boards ou à l'aide de fichiers xib.

2 - utiliser googleMaps ou Apple MapKit.

Exemple

1 - Créer 2 vue contrôleurs d'e.g, MapViewController et BottomSheetViewController. Le premier contrôleur sera l'hôte de la carte et le second est en bas de la feuille elle-même.

Configurer MapViewController

Créer une méthode pour ajouter la feuille inférieure de la vue.

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 de l'appeler dans viewDidAppear méthode:

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

Configurer BottomSheetViewController

1) Préparer arrière-plan

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

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

appeler cette méthode dans votre viewWillAppear

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

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

2) Animer bottomSheet apparence

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) Modifier votre xib que vous le souhaitez.

4) Ajouter Pan Geste de Reconnaissance à votre vue.

Dans votre méthode viewDidLoad ajouter UIPanGestureRecognizer.

override func viewDidLoad() {
    super.viewDidLoad()

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

}

Et de mettre en œuvre votre geste comportement:

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

Défilement En Bas De La Feuille:

Si votre vue personnalisée est un défilement de la vue ou de tout autre point de vue, qui hérite de partir, si vous avez deux options:

D'abord:

La conception de la vue avec une vue d'en-tête et ajouter le panGesture à l'en-tête. (mauvaise expérience utilisateur).

Deuxième:

1 - Ajouter la panGesture à la feuille inférieure de la vue.

2 - mettre en Œuvre les UIGestureRecognizerDelegate et définir la panGesture délégué pour le contrôleur.

3 - mise en Œuvre shouldRecognizeSimultaneouslyWith fonction de délégué et de désactiver la scrollView isScrollEnabled propriété dans deux cas:

  • La vue est partiellement visible.
  • La vue est totalement visible, la scrollView contentOffset propriété est égale à 0 et que l'utilisateur est en faisant glisser l'affichage vers le bas.

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

NOTE

Dans le cas où vous définissez .allowUserInteraction comme une animation, comme dans l'exemple de projet, donc vous devez activer le défilement sur l'animation d'achèvement de fermeture si l'utilisateur fait défiler vers le haut.

Exemple De Projet

J'ai créé un exemple de projet avec plus d'options sur ce repo qui peut vous donner de meilleures idées sur la façon de personnaliser le flux.

Dans la démo, addBottomSheetView() la fonction des contrôles de la vue qui doit être utilisé comme un drap.

Exemple De Projet Captures D'Écran

- Vue Partielle

enter image description here

- FullView

enter image description here

- Défilement De La Vue

enter image description here

87voto

GaétanZ Points 1411

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

Maps transition between the scroll and the translation

Dans les Cartes, comme vous l'avez remarqué, lors de la tableView atteint contentOffset.y == 0, le bas de la feuille, soit de glisse ou tombe en panne.

Le point est délicat car nous ne pouvons pas simplement d'activer/désactiver le défilement lors de notre poêle geste commence la traduction. Il pourrait arrêter le défilement jusqu'à ce qu'une touche nouvelle commence. C'est le cas dans la plupart des solutions proposées ici.

Voici ma essayer de mettre en œuvre cette motion.

Point de départ : Application Maps

Pour commencer notre enquête, nous allons visualiser le point de vue de la hiérarchie de Cartes (démarrer Cartes sur un simulateur et sélectionnez Debug > Attach to process by PID or Name > Maps dans Xcode).

Maps debug view hierarchy

Il ne dit pas comment le mouvement fonctionne, mais il m'a aidé à comprendre la logique. Vous pouvez jouer avec les lldb et le point de vue de la hiérarchie débogueur.

Notre ViewController piles

Nous allons créer une version de base, les Cartes ViewController architecture.

Nous commençons avec un BackgroundViewController (notre map) :

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

Nous avons mis la tableView dans un dédié UIViewController :

class OverlayViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {

    lazy var tableView = UITableView()

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

    [...]
}

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

Pour l'instant, on a qu'une seule méthode publique pour animer le changement de position et il a une vue 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 incorporer 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 :

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

La difficulté derrière la superposition de traduction

Maintenant, comment traduire notre overlay ?

La plupart des solutions proposées utilisez un pan geste de reconnaissance, mais nous avons en fait déjà un : le pan geste de la vue de la table. En outre, nous avons besoin de garder le défilement et la traduction synchronisée et l' UIScrollViewDelegate a tous les événements dont nous avons besoin !

Une implémentation naïve serait d'utiliser un deuxième pan Geste et essayez de réinitialiser l' contentOffset de l'affichage de la table quand la traduction se produit :

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

Mais il ne fonctionne pas. La tableView les mises à jour de ses contentOffset lorsque sa propre pan geste de reconnaissance de l'action des déclencheurs ou lorsque son displayLink callback est appelée. Il n'y a aucune chance que notre reconnaissance déclenche juste après ceux pour réussir à remplacer l' contentOffset. Notre seule chance c'est de prendre une partie de l'aménagement de la phase (en substituant layoutSubviews de défilement de la vue des appels à chaque image de défilement de la vue) ou pour répondre à l' didScroll méthode de le délégué a appelé à chaque fois que l' contentOffset est modifié. Nous allons essayer celui-ci.

La traduction de la mise en Œuvre

Nous avons ajouter un délégué à notre OverlayVC d'expédition de la scrollview les événements de notre traduction du gestionnaire, l' 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 la trace de la traduction à l'aide d'un enum :

enum OverlayInFlightPosition {
    case minimum
    case maximum
    case progressing
}

La position actuelle de calcul ressemble :

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 que si nous avons besoin de 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
    }
}

La seconde effectue la traduction. Il utilise l' translation(in:) méthode de la scrollView pan geste.

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 le doigt. Nous calculons la position à l'aide de la vitesse et de la position actuelle de la vue.

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

Notre superposition de déléguer la mise en œuvre 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()
    }
}

Dernier problème : l'expédition de la superposition du conteneur de touche

La traduction est maintenant assez efficace. Mais il reste un dernier problème : les touches ne sont pas livrés à nos arrière-plan de la vue. Ils sont tous interceptés par la superposition du conteneur de vue. Nous ne pouvons pas définir isUserInteractionEnabled de false parce qu'il serait aussi désactiver l'interaction dans notre tableau. La solution est celle qui est utilisée massivement dans l'application Cartes, 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 retire du répondeur de la chaîne.

En OverlayContainerViewController :

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

Résultat

Voici le résultat :

Result

Vous trouverez le code ici.

S'il vous plaît si vous voyez des bugs, faites le moi savoir ! Notez que votre application peut bien sûr utiliser un deuxième pan geste, spécialement si vous ajoutez un en-tête dans votre superposition.

Mise à jour 23/08/18

Nous pouvons remplacer scrollViewDidEndDragging avec willEndScrollingWithVelocity plutôt que d' enable/disable le défilement lorsque l'utilisateur termine le faisant 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)
}

On peut utiliser un printemps de l'animation et de permettre une interaction de l'utilisateur tandis que l'animation de rendre le mouvement de flux de mieux :

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

22voto

Rajee Jones Points 144

Essayez De Poulie:

La poulie est un facile d'utiliser le tiroir de la bibliothèque censé imiter le tiroir dans iOS 10 de l'app plans. Il expose une API simple qui vous permet d'utiliser toute sous-classe UIViewController que le tiroir du contenu ou de la première contenu.

Pulley Preview

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

15voto

SCENEE Points 49

Vous pouvez essayer ma réponse https://github.com/SCENEE/FloatingPanel. Il fournit un conteneur view controller pour afficher un "drap" de l'interface.

Il est facile à utiliser et vous n'avez pas l'esprit de n'importe quel geste de reconnaissance de la manipulation! Aussi, vous pouvez suivre le défilement de la vue(ou le frère) dans une feuille inférieure si nécessaire.

C'est un exemple simple. Veuillez noter que vous avez besoin pour préparer un view controller pour afficher votre contenu dans une feuille inférieure.

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.show(contentVC, sender: nil)

        // Track a scroll view in "bottom sheet" content if needed.
        fpc.track(scrollView: contentVC.tableView)    
    }
    ...
}

Ici est un autre exemple de code pour afficher un drap à la recherche d'un endroit comme Apple Maps.

Sample App like Apple Maps

0voto

day Points 68

Peut-être que vous pouvez essayer ma réponse https://github.com/AnYuan/AYPannel , inspirée 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.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X