[] Настало время офигительных историй [1/2]
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Порой дизайнеры рисуют необычные переходы между экранами, и UIKit не поддерживает их из коробки. Но их реализация не такая сложная, как может показаться на первый взгляд.Давайте посмотрим на макеты:Извините, данный ресурс не поддреживается. :( Как вы могли заметить, есть два типа анимаций: переход между историями и закрытие/открытие историй как в Instagram (анимация Zoom In/Zoom Out). Давайте обсудим, как можно реализовать эти анимации.Анимация Zoom In/Zoom OutПервый тип анимации, который нам необходим, это открытие/закрытие экрана с историями. Идея в том, чтобы из какого-либо фрейма представлять вью-контроллер, в который он позже и закроется. Реализуем протокол для view, из которой будет представлен экран:
public protocol PreviewStoryViewProtocol: AnyObject {
var endFrame: CGRect { get }
var startFrame: CGRect { get }
}
public class PreviewStoryView: UIView, PreviewStoryViewProtocol {
public var startFrame: CGRect {
return convert(bounds, to: nil)
}
public var endFrame: CGRect {
return convert(bounds, to: nil)
}
}
startFrame и endFrame отвечают за позицию этой view на экране.Далее реализуем сам экран, отвечающий за истории. Он представляет из себя массив из нескольких контроллеров. Так как UIPageViewController не поддерживает пользовательские анимации при переходах, то реализуем эту логику на базе UINavigationController.
class StoriesNavigationController: UINavigationController {
// MARK: - Private properties
private var previewFrame: PreviewStoryViewProtocol?
// MARK: - Setup
func setup(viewControllers: [UIViewController], previewFrame: PreviewStoryViewProtocol?) {
self.previewFrame = previewFrame
self.viewControllers = viewControllers
}
// MARK: - Lifecycle
convenience init() {
self.init(nibName: nil, bundle: nil)
setupUI()
}
}
extension StoriesNavigationController {
private func setupUI() {
setNavigationBarHidden(true, animated: false)
modalPresentationStyle = .custom
}
}
Функция setup отвечает за конфигурацию нашего NavigationController’а. В нее мы передаем массив вью-контроллеров и делегат previewFrame, через который позже получим необходимые фреймы для начала и окончания анимаций.Далее перейдем к самому интересному. Каждый UIViewController имеет свой transitioningDelegate, который можно реализовать через UIViewControllerTransitioningDelegate. Каждый раз, когда мы совершаем показ или закрытие, UIKit спрашивает у делегата, какую анимацию ему отобразить. Чтобы заменить стандартную анимацию на свою, мы и реализуем UIViewControllerTransitioningDelegate.
extension StoriesNavigationController: UIViewControllerTransitioningDelegate {
public func animationController(
forPresented presented: UIViewController,
presenting: UIViewController,
source: UIViewController) -> UIViewControllerAnimatedTransitioning?
{
guard let startFrame = previewFrame?.startFrame else { return nil }
return StoriesNavigationPresentAnimator(startFrame: startFrame)
}
public func animationController(
forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning?
{
guard let endFrame = previewFrame?.endFrame else { return nil }
return StoriesNavigationDismissAnimator(endFrame: endFrame)
}
}
И не забудьте в функции setupUI указать transitioningDelegate =**self.**Эти два метода отвечают за показ и закрытие view-котроллера. Для них мы и должны реализовать два аниматора на базе UIViewControllerAnimatedTransitioning. На эти методы возлагается вся логика анимации.Рассмотрим первый аниматор StoriesNavigationPresentAnimator, отвечающий за показ.
class StoriesNavigationAnimator: NSObject, UIViewControllerAnimatedTransitioning {
private enum Spec {
static let animationDuration: TimeInterval = 0.3
}
private let startFrame: CGRect
init(startFrame: CGRect) {
self.startFrame = startFrame
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return Spec.animationDuration
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
// 1
guard let toViewController = transitionContext.viewController(forKey: .to),
let snapshot = toViewController.view.snapshotView(afterScreenUpdates: true)
else {
return
}
// 2
let containerView = transitionContext.containerView
// 3
containerView.addSubview(toViewController.view)
toViewController.view.isHidden = true
// 4
snapshot.frame = startFrame
snapshot.alpha = 0.0
containerView.addSubview(snapshot)
UIView.animate(withDuration: Spec.animationDuration, animations: {
// 5
snapshot.frame = (transitionContext.finalFrame(for: toViewController))
snapshot.alpha = 1.0
}, completion: { _ in
// 6
toViewController.view.isHidden = false
snapshot.removeFromSuperview()
// 7
if transitionContext.transitionWasCancelled {
toViewController.view.removeFromSuperview()
}
// 8
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
}
}
Первое, что необходимо сделать, это указать время длительности анимации в методе transitionDuration(using:).Затем мы реализуем саму анимацию внутри метода animateTransition.
- Получаем презентуемый вью-контроллер и снэпшотим его.
- Получаем containerView. В этом контексте будет происходить анимация во время перехода между вью-контроллерами.
- Добавляем view конечного вью-контроллера в контекст и скрываем его.
- Готовим снэпшот к анимации. Задаем ему frame view, из которого будем показывать.
- Анимированно меняем размер снэпшота до финального размера.
- После окончания анимации удаляем снэпшот, отображаем реальную view конечного view-котроллера.
- Если переход не будет выполнен (например, прерван пользователем), то необходимо удалить конечное view (toViewController.view), так как оно не будет отображено.
- И наконец-то сообщаем UIKit’у через transitionContext о состоянии перехода.
Теперь ваш аниматор готов к использованию!Аналогично реализуем аниматор для закрытия, который делает всё то же самое, но наоборот.
class StoriesNavigationDismissAnimator: NSObject, UIViewControllerAnimatedTransitioning {
private enum Spec {
static let animationDuration: TimeInterval = 0.3
}
private let endFrame: CGRect
init(endFrame: CGRect) {
self.endFrame = endFrame
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return Spec.animationDuration
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let fromViewController = transitionContext.viewController(forKey: .from),
let snapshot = fromViewController.view.snapshotView(afterScreenUpdates: true)
else {
return
}
let containerView = transitionContext.containerView
containerView.addSubview(snapshot)
fromViewController.view.isHidden = true
UIView.animate(withDuration: Spec.animationDuration, delay: 0, options: .curveEaseOut, animations: {
snapshot.frame = self.endFrame
snapshot.alpha = 0
}, completion: { _ in
fromViewController.view.isHidden = false
snapshot.removeFromSuperview()
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
}
}
Чтобы посмотреть результат, реализуем простой StoryBaseViewController, который отвечает за экран с одной историей.
class StoryBaseViewController: UIViewController {
// MARK: - Constants
private enum Spec {
enum CloseImage {
static let size: CGSize = CGSize(width: 40, height: 40)
static var original: CGPoint = CGPoint(x: 24, y: 50)
}
}
// MARK: - UI components
private lazy var closeButton: UIButton = {
let button = UIButton(type: .custom)
button.setImage(#imageLiteral(resourceName: "closeImage"), for: .normal)
button.addTarget(self, action: #selector(closeButtonAction(sender:)), for: .touchUpInside)
button.frame = CGRect(origin: Spec.CloseImage.original, size: Spec.CloseImage.size)
return button
}()
// MARK: - Lifecycle
public override func loadView() {
super.loadView()
view.addSubview(closeButton)
}
@objc
private func closeButtonAction(sender: UIButton!) {
dismiss(animated: true, completion: nil)
}
}
Завершающий этап — реализация view на стартовом ViewController'е, из которой происходит показ историй. Для этого необходимо создать массив историй (StoryBaseViewController) и отобразить в StoriesNavigationController.
class ViewController: UIViewController {
// MARK: - UI components
private lazy var previewView: PreviewStoryView = {
let preview = PreviewStoryView()
preview.frame.size = CGSize(width: 200, height: 200)
preview.backgroundColor = .black
preview.layer.cornerRadius = 10
preview.center = view.center
return preview
}()
private lazy var showButton: UIButton = {
let button = UIButton()
button.setTitle("Show", for: .normal)
button.addTarget(self, action: #selector(handleButtonAction), for: .touchUpInside)
button.frame = CGRect(origin: CGPoint(x: 0, y: 0), size: CGSize(width: 200, height: 200))
return button
}()
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
}
}
extension ViewController {
private func setupUI() {
view.backgroundColor = .darkGray
view.addSubview(previewView)
previewView.addSubview(showButton)
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(gesture:)))
previewView.addGestureRecognizer(panGesture)
}
}
extension ViewController {
@objc
func handleButtonAction(sender: UIButton!) {
var storyViewControllers: [UIViewController] {
let vc1 = StoryBaseViewController()
vc1.view.backgroundColor = .red
let vc2 = StoryBaseViewController()
vc2.view.backgroundColor = .green
let vc3 = StoryBaseViewController()
vc3.view.backgroundColor = .blue
return [vc1, vc2, vc3]
}
let storiesVC = StoriesNavigationController()
storiesVC.setup(viewControllers: storyViewControllers, previewFrame: previewView)
present(storiesVC, animated: true, completion: nil)
}
@objc
func handlePanGesture(gesture: UIPanGestureRecognizer) {
let stateIsValidate = gesture.state == .began || gesture.state == .changed
if let gestureView = gesture.view, stateIsValidate {
let translation = gesture.translation(in: self.view)
let newXPosition = gestureView.center.x + translation.x
let newYPosition = gestureView.center.y + translation.y
gestureView.center = CGPoint(x: newXPosition, y: newYPosition)
gesture.setTranslation(.zero, in: self.view)
}
}
}
Обратите внимание, что previewView выступает делегатом для StoriesNavigationController и передает startFrame и endFrame. Можно интерактивно перемещать view, и показ экрана с историями будет происходить из нового местоположения на экране.
В следующей части вы можете узнать, как реализовать анимацию перехода между историями.Весь исходный код этой статьи можете скачать тут.
===========
Источник:
habr.com
===========
Похожие новости:
- [Разработка под iOS, Xcode, Swift] 7 Кругов SPM или как сделать модульное приложение на Swift Package Manager
- [Разработка под iOS, Разработка мобильных приложений, Swift, Дизайн мобильных приложений] iOS. UI. Приëмы. Часть 1
- [Разработка под iOS, Swift] Работа с сложными JSON-объектами в Swift (Codable)
- [Разработка под iOS, Swift, Тестирование мобильных приложений] Погружение в автотестирование на iOS. Часть 4. Ожидания в XCUITest
- [Разработка под iOS, Разработка мобильных приложений, Swift] Память в Swift от 0 до 1
- [Разработка под iOS, Swift] DI в iOS: Complete guide
- [Разработка под iOS, Разработка мобильных приложений, Разработка игр, Unity] Запуск игры на Unity из приложения SwiftUI для iOS (перевод)
- [Open source, Разработка под iOS, Разработка мобильных приложений, Swift] Как мы ускоряли работу отладчика Swift
- [MySQL, Серверная оптимизация, Администрирование баз данных] Читаем EXPLAIN на максималках
- [Разработка под iOS, Разработка под Android] Как увеличить срок хранения мобильного приложения? 6 проверенных способов
Теги для поиска: #_citymobil, #_swift, #_ios_development, #_blog_kompanii_sitimobil (
Блог компании Ситимобил
)
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 13:56
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Порой дизайнеры рисуют необычные переходы между экранами, и UIKit не поддерживает их из коробки. Но их реализация не такая сложная, как может показаться на первый взгляд.Давайте посмотрим на макеты:Извините, данный ресурс не поддреживается. :( Как вы могли заметить, есть два типа анимаций: переход между историями и закрытие/открытие историй как в Instagram (анимация Zoom In/Zoom Out). Давайте обсудим, как можно реализовать эти анимации.Анимация Zoom In/Zoom OutПервый тип анимации, который нам необходим, это открытие/закрытие экрана с историями. Идея в том, чтобы из какого-либо фрейма представлять вью-контроллер, в который он позже и закроется. Реализуем протокол для view, из которой будет представлен экран: public protocol PreviewStoryViewProtocol: AnyObject {
var endFrame: CGRect { get } var startFrame: CGRect { get } } public class PreviewStoryView: UIView, PreviewStoryViewProtocol { public var startFrame: CGRect { return convert(bounds, to: nil) } public var endFrame: CGRect { return convert(bounds, to: nil) } } class StoriesNavigationController: UINavigationController {
// MARK: - Private properties private var previewFrame: PreviewStoryViewProtocol? // MARK: - Setup func setup(viewControllers: [UIViewController], previewFrame: PreviewStoryViewProtocol?) { self.previewFrame = previewFrame self.viewControllers = viewControllers } // MARK: - Lifecycle convenience init() { self.init(nibName: nil, bundle: nil) setupUI() } } extension StoriesNavigationController { private func setupUI() { setNavigationBarHidden(true, animated: false) modalPresentationStyle = .custom } } extension StoriesNavigationController: UIViewControllerTransitioningDelegate {
public func animationController( forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { guard let startFrame = previewFrame?.startFrame else { return nil } return StoriesNavigationPresentAnimator(startFrame: startFrame) } public func animationController( forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { guard let endFrame = previewFrame?.endFrame else { return nil } return StoriesNavigationDismissAnimator(endFrame: endFrame) } } class StoriesNavigationAnimator: NSObject, UIViewControllerAnimatedTransitioning {
private enum Spec { static let animationDuration: TimeInterval = 0.3 } private let startFrame: CGRect init(startFrame: CGRect) { self.startFrame = startFrame } func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { return Spec.animationDuration } func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { // 1 guard let toViewController = transitionContext.viewController(forKey: .to), let snapshot = toViewController.view.snapshotView(afterScreenUpdates: true) else { return } // 2 let containerView = transitionContext.containerView // 3 containerView.addSubview(toViewController.view) toViewController.view.isHidden = true // 4 snapshot.frame = startFrame snapshot.alpha = 0.0 containerView.addSubview(snapshot) UIView.animate(withDuration: Spec.animationDuration, animations: { // 5 snapshot.frame = (transitionContext.finalFrame(for: toViewController)) snapshot.alpha = 1.0 }, completion: { _ in // 6 toViewController.view.isHidden = false snapshot.removeFromSuperview() // 7 if transitionContext.transitionWasCancelled { toViewController.view.removeFromSuperview() } // 8 transitionContext.completeTransition(!transitionContext.transitionWasCancelled) }) } }
class StoriesNavigationDismissAnimator: NSObject, UIViewControllerAnimatedTransitioning {
private enum Spec { static let animationDuration: TimeInterval = 0.3 } private let endFrame: CGRect init(endFrame: CGRect) { self.endFrame = endFrame } func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { return Spec.animationDuration } func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { guard let fromViewController = transitionContext.viewController(forKey: .from), let snapshot = fromViewController.view.snapshotView(afterScreenUpdates: true) else { return } let containerView = transitionContext.containerView containerView.addSubview(snapshot) fromViewController.view.isHidden = true UIView.animate(withDuration: Spec.animationDuration, delay: 0, options: .curveEaseOut, animations: { snapshot.frame = self.endFrame snapshot.alpha = 0 }, completion: { _ in fromViewController.view.isHidden = false snapshot.removeFromSuperview() transitionContext.completeTransition(!transitionContext.transitionWasCancelled) }) } } class StoryBaseViewController: UIViewController {
// MARK: - Constants private enum Spec { enum CloseImage { static let size: CGSize = CGSize(width: 40, height: 40) static var original: CGPoint = CGPoint(x: 24, y: 50) } } // MARK: - UI components private lazy var closeButton: UIButton = { let button = UIButton(type: .custom) button.setImage(#imageLiteral(resourceName: "closeImage"), for: .normal) button.addTarget(self, action: #selector(closeButtonAction(sender:)), for: .touchUpInside) button.frame = CGRect(origin: Spec.CloseImage.original, size: Spec.CloseImage.size) return button }() // MARK: - Lifecycle public override func loadView() { super.loadView() view.addSubview(closeButton) } @objc private func closeButtonAction(sender: UIButton!) { dismiss(animated: true, completion: nil) } } class ViewController: UIViewController {
// MARK: - UI components private lazy var previewView: PreviewStoryView = { let preview = PreviewStoryView() preview.frame.size = CGSize(width: 200, height: 200) preview.backgroundColor = .black preview.layer.cornerRadius = 10 preview.center = view.center return preview }() private lazy var showButton: UIButton = { let button = UIButton() button.setTitle("Show", for: .normal) button.addTarget(self, action: #selector(handleButtonAction), for: .touchUpInside) button.frame = CGRect(origin: CGPoint(x: 0, y: 0), size: CGSize(width: 200, height: 200)) return button }() // MARK: - Lifecycle override func viewDidLoad() { super.viewDidLoad() setupUI() } } extension ViewController { private func setupUI() { view.backgroundColor = .darkGray view.addSubview(previewView) previewView.addSubview(showButton) let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(gesture:))) previewView.addGestureRecognizer(panGesture) } } extension ViewController { @objc func handleButtonAction(sender: UIButton!) { var storyViewControllers: [UIViewController] { let vc1 = StoryBaseViewController() vc1.view.backgroundColor = .red let vc2 = StoryBaseViewController() vc2.view.backgroundColor = .green let vc3 = StoryBaseViewController() vc3.view.backgroundColor = .blue return [vc1, vc2, vc3] } let storiesVC = StoriesNavigationController() storiesVC.setup(viewControllers: storyViewControllers, previewFrame: previewView) present(storiesVC, animated: true, completion: nil) } @objc func handlePanGesture(gesture: UIPanGestureRecognizer) { let stateIsValidate = gesture.state == .began || gesture.state == .changed if let gestureView = gesture.view, stateIsValidate { let translation = gesture.translation(in: self.view) let newXPosition = gestureView.center.x + translation.x let newYPosition = gestureView.center.y + translation.y gestureView.center = CGPoint(x: newXPosition, y: newYPosition) gesture.setTranslation(.zero, in: self.view) } } } В следующей части вы можете узнать, как реализовать анимацию перехода между историями.Весь исходный код этой статьи можете скачать тут. =========== Источник: habr.com =========== Похожие новости:
Блог компании Ситимобил ) |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 13:56
Часовой пояс: UTC + 5