UINavigationBar цветная анимация синхронизирована с push-анимацией
Я хочу добиться плавной анимации между видами с другим UINavigationBar
фоновые цвета. Встроенные представления имеют тот же цвет фона, что и UINavigationBar
и я хочу имитировать анимацию перехода push/pop как:
Я подготовил пользовательский переход:
class CustomTransition: NSObject, UIViewControllerAnimatedTransitioning {
private let duration: TimeInterval
private let isPresenting: Bool
init(duration: TimeInterval = 1.0, isPresenting: Bool) {
self.duration = duration
self.isPresenting = isPresenting
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return duration
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let container = transitionContext.containerView
guard
let toVC = transitionContext.viewController(forKey: .to),
let fromVC = transitionContext.viewController(forKey: .from),
let toView = transitionContext.view(forKey: .to),
let fromView = transitionContext.view(forKey: .from)
else {
return
}
let rightTranslation = CGAffineTransform(translationX: container.frame.width, y: 0)
let leftTranslation = CGAffineTransform(translationX: -container.frame.width, y: 0)
toView.transform = isPresenting ? rightTranslation : leftTranslation
container.addSubview(toView)
container.addSubview(fromView)
fromVC.navigationController?.navigationBar.backgroundColor = .clear
fromVC.navigationController?.navigationBar.setBackgroundImage(UIImage.fromColor(color: .clear), for: .default)
UIView.animate(
withDuration: self.duration,
animations: {
fromVC.view.transform = self.isPresenting ? leftTranslation :rightTranslation
toVC.view.transform = .identity
},
completion: { _ in
fromView.transform = .identity
toVC.navigationController?.navigationBar.setBackgroundImage(
UIImage.fromColor(color: self.isPresenting ? .yellow : .lightGray),
for: .default
)
transitionContext.completeTransition(true)
}
)
}
}
И вернул его в UINavigationControllerDelegate
Реализация метода:
func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationControllerOperation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return CustomTransition(isPresenting: operation == .push)
}
В то время как push-анимация работает довольно хорошо, поп-музыка не работает.
Вопросы:
- Почему после очистки цвета NavBar перед поп-анимацией он остается желтым?
- Есть ли лучший способ достичь моей цели? (navbar не может быть просто прозрачным все время, потому что это только часть потока)
Вот ссылка на мой тестовый проект на GitHub.
РЕДАКТИРОВАТЬ
Вот рисунок, представляющий полную картину обсуждаемой проблемы и желаемого эффекта:
3 ответа
Эти компоненты всегда очень сложно настроить. Я думаю, Apple хочет, чтобы системные компоненты выглядели и вели себя одинаково в каждом приложении, потому что это позволяет поддерживать общий пользовательский опыт во всей среде iOS.
Иногда проще реализовать собственные компоненты с нуля, чем пытаться настроить системные компоненты. Настройка часто может быть сложной, потому что вы не знаете наверняка, как компоненты разработаны внутри. В результате вам придется обрабатывать множество крайних случаев и справляться с ненужными побочными эффектами.
Тем не менее, я считаю, что у меня есть решение для вашей ситуации. Я раздвоил ваш проект и реализовал поведение, которое вы описали. Вы можете найти мою реализацию на GitHub. Увидеть animation-implementation
ветка.
UINavigationBar
Основная причина поп-анимации не работает должным образом, это то, что UINavigationBar
имеет собственную внутреннюю логику анимации. когда UINavigationController's
изменения стека, UINavigationController
говорит UINavigationBar
изменить UINavigationItems
, Итак, сначала нам нужно отключить системную анимацию для UINavigationItems
, Это может быть сделано путем подклассов UINavigationBar
:
class CustomNavigationBar: UINavigationBar {
override func pushItem(_ item: UINavigationItem, animated: Bool) {
return super.pushItem(item, animated: false)
}
override func popItem(animated: Bool) -> UINavigationItem? {
return super.popItem(animated: false)
}
}
затем UINavigationController
должен быть инициализирован с CustomNavigationBar
:
let nc = UINavigationController(navigationBarClass: CustomNavigationBar.self, toolbarClass: nil)
UINavigationController
Поскольку существует необходимость сохранять анимацию плавной и синхронизированной между UINavigationBar
и представил UIViewController
нам нужно создать пользовательский объект анимации перехода для UINavigationController
и использовать CoreAnimation
с CATransaction
,
Пользовательский переход
Ваша реализация переходного аниматора практически идеальна, но, с моей точки зрения, некоторые детали были упущены. В статье " Настройка анимации перехода" вы можете найти больше информации. Также, пожалуйста, обратите внимание на методы комментариев в UIViewControllerContextTransitioning
протокол.
Итак, моя версия push-анимации выглядит следующим образом:
func animatePush(_ transitionContext: UIViewControllerContextTransitioning) {
let container = transitionContext.containerView
guard let toVC = transitionContext.viewController(forKey: .to),
let toView = transitionContext.view(forKey: .to) else {
return
}
let toViewFinalFrame = transitionContext.finalFrame(for: toVC)
toView.frame = toViewFinalFrame
container.addSubview(toView)
let viewTransition = CABasicAnimation(keyPath: "transform")
viewTransition.duration = CFTimeInterval(self.duration)
viewTransition.fromValue = CATransform3DTranslate(toView.layer.transform, container.layer.bounds.width, 0, 0)
viewTransition.toValue = CATransform3DIdentity
CATransaction.begin()
CATransaction.setAnimationDuration(CFTimeInterval(self.duration))
CATransaction.setCompletionBlock = {
let cancelled = transitionContext.transitionWasCancelled
if cancelled {
toView.removeFromSuperview()
}
transitionContext.completeTransition(cancelled == false)
}
toView.layer.add(viewTransition, forKey: nil)
CATransaction.commit()
}
Реализация поп-анимации практически одинакова. Единственная разница в CABasicAnimation
значения fromValue
а также toValue
свойства.
UINavigationBar анимация
Для того, чтобы оживить UINavigationBar
мы должны добавить CATransition
анимация на UINavigationBar
слой:
let transition = CATransition()
transition.duration = CFTimeInterval(self.duration)
transition.type = kCATransitionPush
transition.subtype = self.isPresenting ? kCATransitionFromRight : kCATransitionFromLeft
toVC.navigationController?.navigationBar.layer.add(transition, forKey: nil)
Код выше будет анимировать весь UINavigationBar
, Для того, чтобы анимировать только фон UINavigationBar
нам нужно получить фоновый вид из UINavigationBar
, А вот и хитрость: первое подвид UINavigationBar
является _UIBarBackground
представление (это могло быть исследовано, используя Xcode Debug View Hierarchy). Точный класс не важен в нашем случае, достаточно, чтобы он был наследником UIView
, Наконец, мы могли бы добавить наш анимационный переход на _UIBarBackground
Вид слоя прямо:
let backgroundView = toVC.navigationController?.navigationBar.subviews[0]
backgroundView?.layer.add(transition, forKey: nil)
Я хотел бы отметить, что мы прогнозируем, что первое подпредставление является фоновым представлением. Иерархия представления может быть изменена в будущем, просто имейте это в виду.
Важно добавить обе анимации в одну CATransaction
потому что в этом случае эти анимации будут выполняться одновременно.
Вы могли бы настроить UINavigationBar
цвет фона в viewWillAppear
метод каждого контроллера представления.
Вот как выглядит финальная анимация:
Надеюсь, это поможет.
Я бы сделал это, сделав навигационный контроллер полностью прозрачным. Таким образом, анимация встроенного контроллера представления должна давать желаемый эффект.
Редактировать: Вы также можете получить "белый контент", ограничив containerView под панелью навигации. В примере кода я это сделал. Толчок случайным образом выбирает цвет и дает виду контейнера случайным образом белый или прозрачный цвет. Вы увидите, что все сценарии в вашем GIF покрыты этим примером.
Попробуйте это на детской площадке:
import UIKit
import PlaygroundSupport
class MyViewController : UIViewController {
var containerView: UIView!
override func loadView() {
let view = UIView()
view.backgroundColor = .white
containerView = UIView()
containerView.backgroundColor = .white
containerView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(containerView)
containerView.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor).isActive = true
containerView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
containerView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
containerView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
let button = UIButton()
button.frame = CGRect(x: 150, y: 200, width: 200, height: 20)
button.setTitle("push", for: .normal)
button.setTitleColor(.black, for: .normal)
button.addTarget(self, action: #selector(push), for: .touchUpInside)
containerView.addSubview(button)
self.view = view
}
@objc func push() {
let colors: [UIColor] = [.yellow, .red, .blue, .purple, .gray, .darkGray, .green]
let controller = MyViewController()
controller.title = "Second"
let randColor = Int(arc4random()%UInt32(colors.count))
controller.view.backgroundColor = colors[randColor]
let clearColor: Bool = (arc4random()%2) == 1
controller.containerView.backgroundColor = clearColor ? .clear: .white
navigationController?.pushViewController(controller, animated: true)
}
}
// Present the view controller in the Live View window
let controller = MyViewController()
controller.view.backgroundColor = .white
let navController = UINavigationController(rootViewController: controller)
navController.navigationBar.setBackgroundImage(UIImage(), for: .default)
controller.title = "First"
PlaygroundPage.current.liveView = navController
Удалите этот код из вашего проекта:
toVC.navigationController?.navigationBar.setBackgroundImage(
UIImage.fromColor(color: self.isPresenting ? .yellow : .lightGray),
for: .default
)