*как в Кое-что-грамме или Telegram.
В конце прошлого кода я затащил в Blink чекины (аналог историй с отметкой места), и передо мной встала задача красиво переключаться между пользователями. Все мы, конечно же, хотели анимацию куба. После пары дней ресёча, я пришел к неутешительному выводу, что вменяемых готовых реализаций для этого нет. Имеется парочка библиотек на GitHub, и одну из них я решил попробовать, потому что писать своё – времени не было.
Выбор пал на CubeContainerViewController-iOS. После переделок под нашу навигацию и стиль кода, казалось, что всё очень даже неплохо. Визуально всё работало, но это лишь на первый взгляд…
Коротко, какие проблемы меня настигли:
-
Невозможность открытия куба с любой грани (открытие не первого в списке человека).
-
Скорость и углы анимации.
-
Необходимость держать в памяти все экраны.
-
Баги при быстром перелистовании по тапу.
-
Не совсем красиво.
-
Разные проблемы с логикой прочтения и сохранения стейта ранее прочитанных чекинов.
С этим решением мы прожили 3-4 месяца, параллельно наращивая функционал чекинов. Но пришло время довести это дело до ума.
Требования к новому кубу:
-
Стабильность работы.
-
Эффективность расходования памяти.
-
Гибкость настройки.
-
Удобное API.
Лучшим вариантом оказалась идея построить куб на UICollectionView. Так сразу решится проблема переиспользования экранов и добавится стабильность работы, ведь большую часть за нас будет делать коллекция.
Мы сверстали простую горизонтальную коллекцию с включенным пейджингом.
Hidden text
private let layout = Builder<UICollectionViewFlowLayout>() .minimumInteritemSpacing(0) .minimumLineSpacing(0) .sectionInset(.zero) .scrollDirection(.horizontal) .build() private(set) lazy var containerView = Builder<BaseCollectionView>() .showsHorizontalScrollIndicator(false) .showsVerticalScrollIndicator(false) .collectionViewLayout(layout) .isPagingEnabled(true) .bounces(true) .backgroundColor(.clear) .build()
В ячейке коллекции есть только поле с UIViewController и метод applyTransform
, про который поговорим чуть позже.
Hidden text
final class CubeContainerCell: BaseCollectionViewCell { var viewController: UIViewController? override func initSetup() { super.initSetup() clipsToBounds = false contentView.clipsToBounds = false } func applyTransform(_ percent: CGFloat) { ... } }
В UICollectionViewDataSource всё также стандартно, но вызываем loadViewIfNeeded
у нашего контроллера внутри ячейки. Это необходимо для того, чтобы вьюшка нашего контроллера загрузилась. Обычно мы не используем этот метод, потому что запрос на отображения контроллера уже является причиной для загрузки вьюшки. Здесь мы ничего не презентуем и нам нужно вызвать это вручную.
Hidden text
extension CubeTransitionViewController: UICollectionViewDataSource { func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { accounts.count } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { guard let cell = collectionView.dequeue(CubeContainerCell.self, for: indexPath), let context = self.accounts.at(indexPath.item) else { fatalError("wrong index") } cell.viewController = try! userCheckinsFactory.build(with: context) cell.viewController?.loadViewIfNeeded() return cell } }
В UICollectionViewDelegate отслеживаем методы показа и скрытия ячеек с экрана для добавления и удаления child.
Hidden text
extension CubeTransitionViewController: UICollectionViewDelegate { func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { guard let cell = cell as? CubeContainerCell, let viewController = cell.viewController else { return } self.addChild(vc: viewController, bindedTo: cell.contentView) } func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { guard let cell = cell as? CubeContainerCell, let viewController = cell.viewController else { return } self.removeChild(viewController) } }
А в UICollectionViewDelegateFlowLayout растягиваем нашу ячейку на весь экран.
Hidden text
extension CubeTransitionViewController: UICollectionViewDelegateFlowLayout { func collectionView(_ collectionView: UICollectionView, layout: UICollectionViewLayout, sizeForItemAt: IndexPath) -> CGSize { collectionView.frame.size } }
Вся красота начинается в UIScrollViewDelegate. В первую очередь, нам необходимо отслеживать скролл внутри метода scrollViewDidScroll
и производить трансформацию наших ячеек. Для лучшего User Experience, мы выключаем интеракцию у scrollView в методе scrollViewWillBeginDecelerating
и включаем в методах scrollViewDidEndDecelerating
и scrollViewDidEndScrollingAnimation
Hidden text
extension CubeTransitionViewController: UIScrollViewDelegate { func scrollViewDidScroll(_ scrollView: UIScrollView) { transformViewsInScrollView(scrollView) } func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView) { scrollView.isUserInteractionEnabled = false } func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { scrollView.isUserInteractionEnabled = true } func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) { scrollView.isUserInteractionEnabled = true } func transformViewsInScrollView(_ scrollView: UIScrollView) { let svWidth = scrollView.frame.width for index in 0 ..< mainView.containerView.visibleCells.count { guard let view = mainView.containerView.visibleCells[index] as? CubeContainerCell else { continue } let svCenter = scrollView.frame(in: view).center.x let cellCenter = view.frame(in: view).center.x let xDiff = svCenter - cellCenter view.applyTransform(xDiff / svWidth) } } }
Далее немного колдуем с математикой и трансформируем саму ячейку с помощью метода applyTransform
Hidden text
func applyTransform(_ percent: CGFloat) { let view = self.contentView let maxAngle: CGFloat = 60.0 let rad = percent * maxAngle * CGFloat(Double.pi / 180) var transform = CATransform3DIdentity transform.m34 = 1 / 500 transform = CATransform3DRotate(transform, rad, 0, 1, 0) view.layer.transform = transform let anchorPoint = percent > 0 ? CGPoint(x: 1, y: 0.5) : CGPoint(x: 0, y: 0.5) var newPoint = CGPoint( x: view.bounds.size.width * anchorPoint.x, y: view.bounds.size.height * anchorPoint.y ) var oldPoint = CGPoint( x: view.bounds.size.width * view.layer.anchorPoint.x, y: view.bounds.size.height * view.layer.anchorPoint.y ) newPoint = newPoint.applying(view.transform) oldPoint = oldPoint.applying(view.transform) var position = view.layer.position position.x -= oldPoint.x position.x += newPoint.x position.y -= oldPoint.y position.y += newPoint.y view.layer.position = position view.layer.anchorPoint = anchorPoint view.alpha = 1 - (-percent).clamped(0, 1) }
Та-даа-ам! Вы и ваш куб прекрасны! Как красиво теперь это выглядит:
Исходников не будет, придётся поработать ручками. По всем вопросам пишите в комментарии или мне в Telegram: t.me/zloysergunya
ссылка на оригинал статьи https://habr.com/ru/articles/828680/
Добавить комментарий