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":
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.
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.
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()
}
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.
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)
}
}
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)
}
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:
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.
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.
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:
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.
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.
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).
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.
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])
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.
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()
}
}
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()
}
Voici le résultat:
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.
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)
}
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.
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.
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.
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.