Как я могу имитировать нижний лист из приложения «Карты»?

Может ли кто-нибудь сказать мне, как я могу подражать нижнему листу в новом приложении «Карты» в iOS 10?

В Android вы можете использовать BottomSheet который имитирует это поведение, но я не мог найти ничего подобного для iOS.

Это простой вид прокрутки с вложением содержимого, так что панель поиска находится внизу?

Я довольно новичок в программировании на iOS, поэтому, если кто-то может помочь мне создать этот макет, это будет высоко оценено.

Это то, что я подразумеваю под «нижним листом»:

снимок экрана сложенного нижнего листа в Картах

снимок экрана расширенного нижнего листа в Картах

Я не знаю, как именно нижний лист нового приложения Maps отвечает на взаимодействие с пользователем. Но вы можете создать собственное представление, которое будет выглядеть как на снимках экрана, и добавить его в основное представление.

Я полагаю, вы знаете, как:

1- создавать controllerы представлений либо с помощью раскадровки, либо с помощью xib-файлов.

2- используйте googleMaps или MapKit от Apple.

пример

1- Создайте 2 controllerа вида, например MapViewController и BottomSheetViewController . Первый controller будет размещать карту, а второй – нижний лист.

Настройка MapViewController

Создайте метод добавления нижнего листа.

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

И назовите его в методе viewDidAppear:

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

Настройка BottomSheetViewController

1) Подготовьте фон

Создайте метод добавления эффектов размытия и вибрации

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

вызовите этот метод в своем представленииWillAppear

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

Убедитесь, что цвет фона вашего controllerа – clearColor.

2) Анимация нижнего экрана

 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) Измените свой xib как хотите.

4) Добавьте к вашему представлению Распознаватель жжения.

В вашем методе viewDidLoad добавьте UIPanGestureRecognizer.

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

И реализовать свое поведение жестов:

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

Прокручиваемый нижний лист:

Если ваше пользовательское представление представляет собой прокрутку или любое другое представление, которое наследуется, поэтому у вас есть два варианта:

Первый:

Создайте представление с видом заголовка и добавьте panGesture в заголовок. (плохой пользовательский опыт) .

Во- вторых:

1 – Добавьте панораму в нижний лист.

2 – Внедрите UIGestureRecognizerDelegate и установите делегат panGesture на controller.

3- Реализация shouldRecognizeSimultaneousWith делегировать функцию и отключить свойство scrollView isScrollEnabled в двух случаях:

  • Вид частично виден.
  • Вид полностью виден, свойство scrollView contentOffset равно 0, и пользователь перетаскивает представление вниз.

В противном случае разрешите прокрутку.

  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 } 

ЗАМЕТКА

Если вы установите параметр .allowUserInteraction как вариант анимации, как в примере проекта, вам нужно включить прокрутку при завершении завершения анимации, если пользователь прокручивается.

Пример проекта

Я создал образец проекта с большим количеством опций на этом репо, который может дать вам более полное представление о том, как настроить stream.

В демо-функции функции addBottomSheetView (), представление которых должно использоваться как нижний лист.

Примеры скриншотов проектов

– Частичный вид

введите описание изображения здесь

– Полный обзор

введите описание изображения здесь

– Прокручиваемый вид

введите описание изображения здесь

Попробуйте Pulley :

Pulley – простая в использовании библиотека ящиков, предназначенная для подражания ящику в приложении Maps iOS 10. Он предоставляет простой API, который позволяет использовать любой подclass UIViewController в качестве содержимого ящика или основного содержимого.

Предварительный просмотр Pulley

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

Я думаю, что есть существенный момент, который не рассматривается в предлагаемых решениях: переход между свитком и переводом.

Переход между прокруткой и переводом

В Maps, как вы могли заметить, когда tableView достигает contentOffset.y == 0 , нижний лист либо скользит вверх, либо опускается вниз.

Точка сложна, потому что мы не можем просто включить / отключить прокрутку, когда наш жест передвижения начнет перевод. Это остановит прокрутку, пока не начнется новое касание. Это имеет место в большинстве предлагаемых здесь решений.

Вот моя попытка реализовать это движение.

Начальная точка: приложение Карты

Чтобы начать наше исследование, давайте визуализируем иерархию представлений Maps (запустите Maps на симуляторе и выберите Debug > Attach to process by PID or Name > Maps в Xcode).

Иерархия отображения отладки Google

Он не говорит о том, как работает движение, но это помогло мне понять логику этого. Вы можете играть с lldb и отладчиком иерархии представлений.

Наши стеки ViewController

Давайте создадим базовую версию архитектуры Maps ViewController.

Мы начинаем с BackgroundViewController (наш вид карты):

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

Мы помещаем tableView в выделенный UIViewController :

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

Теперь нам нужен VC для встраивания наложения и управления его переводом. Чтобы упростить задачу, мы считаем, что она может переводить наложение из одной статической точки OverlayPosition.maximum в другую OverlayPosition.minimum .

На данный момент у него есть только один общеansible метод, чтобы анимировать изменение позиции и иметь прозрачный вид:

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

Наконец, нам нужен ViewController, чтобы вставить все:

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

В нашей AppDelegate наша последовательность запуска выглядит так:

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

Трудность перевода наложения

Теперь, как перевести наш оверлей?

В большинстве предлагаемых решений используется специальный распознаватель жесты, но на самом деле у нас уже есть один: жест панорамы табличного представления. Кроме того, нам нужно сохранить синхронизацию и синхронизацию перевода, а UISrollViewDelegate – все необходимые нам события!

Наивная реализация будет использовать второй жест Жест и попытаться сбросить contentOffset представления таблицы при переводе:

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

Но это не работает. TableView обновляет свой contentOffset когда contentOffset его распознавателя жестов или когда вызывается обратный вызов displayLink. Нет никаких шансов, что наш распознаватель запускает сразу после того, чтобы успешно переопределить contentOffset . Наш единственный шанс – либо принять участие в фазе макета (переопределив layoutSubviews вызовов просмотра прокрутки в каждом кадре прокрутки), либо ответить на метод didScroll делегата, contentOffset вызывается каждый раз, когда contentOffset модификация contentOffset . Давайте попробуем это.

Реализация перевода

Мы добавляем делегата в наш OverlayVC для отправки событий scrollview в наш обработчик перевода, 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) } } 

В нашем контейнере мы отслеживаем перевод, используя перечисление:

 enum OverlayInFlightPosition { case minimum case maximum case progressing } 

Расчет текущей позиции выглядит следующим образом:

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

Нам нужно 3 метода обработки перевода:

Первый говорит нам, нужно ли нам начать перевод.

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

Второй выполняет перевод. Он использует метод translation(in:) для жесты панорамирования 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) ) } 

Третий анимирует конец перевода, когда пользователь отпускает свой палец. Мы вычисляем позицию с использованием скорости и текущей позиции вида.

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

Реализация делегата нашего наложения выглядит так:

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

Конечная проблема: отправьте касания контейнера наложения

Перевод теперь довольно эффективен. Но до сих пор существует окончательная проблема: штрихи не доставляются в наш фоновый вид. Все они перехватываются видом контейнера наложения. Мы не можем установить isUserInteractionEnabled в false потому что это также отключит взаимодействие в нашем представлении таблицы. Решением является тот, который широко используется в приложении «Карты», 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 } } 

Он удаляется из цепи ответчиков.

В OverlayContainerViewController :

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

результат

Вот результат:

результат

Код можно найти здесь .

Пожалуйста, если вы видите какие-либо ошибки, дайте мне знать! Обратите внимание, что ваша реализация может, конечно, использовать второй жест, особенно если вы добавляете заголовок в свой оверлей.

Обновление 23/08/18

Мы можем заменить scrollViewDidEndDragging на willEndScrollingWithVelocity а не enable / disable прокрутку, когда пользователь заканчивает drag and drop:

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

Мы можем использовать анимацию весны и позволить пользователю взаимодействовать во время анимации, чтобы улучшить движение движения:

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

Возможно, вы можете попробовать мой ответ https://github.com/AnYuan/AYPannel , вдохновленный Пули. Плавный переход от перемещения ящика к прокрутке списка. Я добавил жест панорамы в представлении прокрутки контейнера и установил shouldRecognizeSimultaneousWithGestureRecognizer, чтобы вернуть YES. Более подробно в моей ссылке github выше. Желаю помочь.

Ни одно из перечисленных выше не работает для меня, потому что панорамирование как вида таблицы, так и внешнего вида одновременно не работает. Поэтому я написал свой собственный код для достижения намеченного поведения в приложении ios Maps.

https://github.com/OfTheWolf/UBottomSheet

ubottom sheet

  • Проверьте, находится ли UIView в середине анимации
  • Обнаружить, если некоторые UIView были затронуты среди других UIViews
  • Подclass UIView со своим XIB
  • Ориентация в UIView добавлена ​​в UIWindow
  • Как получить цвет пикселя в UIView?
  • Сделать фон UIView gradleиентом без подclassа
  • UIView против UIViewController
  • Рамка UIView, границы, центр, происхождение, когда использовать что?
  • Что такое блочные методы анимации в iPhone OS 4.0?
  • Изменить размер для строки состояния вызова?
  • Назначить xib для UIView в Swift
  • Давайте будем гением компьютера.