Может ли кто-нибудь сказать мне, как я могу подражать нижнему листу в новом приложении "Карты" в iOS 10?
Это простой вид прокрутки с вложением содержимого, так что панель поиска находится внизу?
Я новичок в программировании на iOS, поэтому, если кто-то может помочь мне создать этот макет, это будет высоко оценено.
Ответ 1
Я не знаю, как именно нижний лист нового приложения Карт реагирует на взаимодействие с пользователем. Но вы можете создать собственный вид, похожий на тот, что на скриншотах, и добавить его в основной вид.
Я полагаю, вы знаете, как:
1- создает контроллеры представления либо с помощью раскадровок, либо с использованием файлов XIB.
2- используйте googleMaps или Apple MapKit.
пример
1- Создайте 2 контроллера вида, например, MapViewController и BottomSheetViewController. Первый контроллер будет размещать карту, а второй - сам нижний лист.
Настроить 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)
}
вызовите этот метод в вашем viewWillAppear
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
prepareBackgroundView()
}
Убедитесь, что цвет фона вашего контроллера - 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) Добавьте Pan Gesture Recognizer к вашему виду.
В вашем методе 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 - Добавьте panGesture к виду нижнего листа.
2 - Реализуйте UIGestureRecognizerDelegate и установите делегат panGesture на контроллер.
3- Реализовать функцию shouldRecognizeSimralleluallyWith делегата и отключить свойство 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 в качестве опции анимации, как в примере проекта, вам необходимо включить прокрутку при закрытии завершения анимации, если пользователь прокручивает вверх.
Пример проекта
Я создал пример проекта с большим количеством опций в этом репо, который может дать вам лучшее представление о том, как настроить поток.
В демонстрационной версии функция addBottomSheetView() контролирует, какой вид следует использовать в качестве нижнего листа.
Пример проекта Скриншоты
- Частичный вид
![enter image description here]()
- Полный обзор
![enter image description here]()
- прокручиваемый вид
![enter image description here]()
Ответ 2
Обновление 4 декабря 2018
Я выпустил библиотеку на основе этого решения https://github.com/applidium/ADOverlayContainer.
Он имитирует наложение приложения "Ярлыки". Смотрите эту статью для деталей.
Основным компонентом библиотеки является OverlayContainerViewController
. Он определяет область, где контроллер представления можно перетаскивать вверх и вниз, скрывая или раскрывая содержимое под ним.
let contentController = MapsViewController()
let overlayController = SearchViewController()
let containerController = OverlayContainerViewController()
containerController.delegate = self
containerController.viewControllers = [
contentController,
overlayController
]
window?.rootViewController = containerController
OverlayContainerViewControllerDelegate
чтобы указать OverlayContainerViewControllerDelegate
количество OverlayContainerViewControllerDelegate
:
enum OverlayNotch: Int, CaseIterable {
case minimum, medium, maximum
}
func numberOfNotches(in containerViewController: OverlayContainerViewController) -> Int {
return OverlayNotch.allCases.count
}
func overlayContainerViewController(_ containerViewController: OverlayContainerViewController,
heightForNotchAt index: Int,
availableSpace: CGFloat) -> CGFloat {
switch OverlayNotch.allCases[index] {
case .maximum:
return availableSpace * 3 / 4
case .medium:
return availableSpace / 2
case .minimum:
return availableSpace * 1 / 4
}
}
Предыдущий ответ
Я думаю, что есть важный момент, который не рассматривается в предлагаемых решениях: переход между прокруткой и переводом.
![Maps transition between the scroll and the translation]()
В Картах, как вы могли заметить, когда tableView достигает contentOffset.y == 0
, нижний лист либо скользит вверх, либо опускается.
Суть хитрая, потому что мы не можем просто включить/отключить прокрутку, когда наш жест панорамирования начинает перевод. Это остановит прокрутку, пока не начнется новое касание. Это относится к большинству предлагаемых решений здесь.
Вот моя попытка реализовать это движение.
Начальная точка: приложение "Карты"
Чтобы начать наше исследование, позвольте визуализировать иерархию представлений Карт (запустите Карты на симуляторе и выберите " Debug
> " Attach to process by PID or Name
> " Maps
в Xcode 9").
![Maps debug view hierarchy]()
Это не говорит, как работает движение, но оно помогло мне понять логику этого. Вы можете играть с 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
.
На данный момент у него есть только один публичный метод для анимации изменения позиции и прозрачный вид:
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])
Сложность перевода наложения
Теперь, как перевести наш оверлей?
В большинстве предлагаемых решений используется специальный распознаватель жестов панорамирования, но у нас уже есть один: жест панорамирования представления таблицы. Более того, нам нужно синхронизировать прокрутку и перевод, а в UIScrollViewDelegate
есть все необходимые события!
Наивная реализация будет использовать второй панорамирование Gesture и попытаться сбросить contentOffset
табличного представления при выполнении перевода:
func panGestureAction(_ recognizer: UIPanGestureRecognizer) {
if isTranslating {
tableView.contentOffset = .zero
}
}
Но это не работает. TableView обновляет свой contentOffset
когда contentOffset
собственное действие распознавателя жестов панорамирования или когда вызывается его обратный вызов displayLink. Нет никаких шансов, что наш распознаватель сработает сразу после них, чтобы успешно переопределить contentOffset
. Наш единственный шанс - либо принять участие в фазе макета (переопределив layoutSubviews
представления прокрутки в каждом кадре представления прокрутки), либо ответить на метод didScroll
делегата, вызываемого каждый раз при изменении 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()
}
Результат
Вот результат:
![Result]()
Вы можете найти код здесь.
Пожалуйста, если вы видите какие-либо ошибки, дайте мне знать! Обратите внимание, что ваша реализация может, конечно, использовать второй жест панорамирования, особенно если вы добавляете заголовок в оверлей.
Обновление 23/08/18
Мы можем заменить scrollViewDidEndDragging
на willEndScrollingWithVelocity
вместо того, enable
/disable
прокрутку, когда пользователь заканчивает перетаскивание:
func scrollView(_ scrollView: UIScrollView,
willEndScrollingWithVelocity velocity: CGPoint,
targetContentOffset: UnsafeMutablePointer<CGPoint>) {
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)
}