RxSwift и анимация в iOS
Опубликовано: 2022-03-11Если вы являетесь разработчиком iOS, который проделал разумный объем работы с пользовательским интерфейсом и увлечен этим, вам должны понравиться возможности UIKit, когда дело доходит до анимации. Анимировать UIView проще простого. Вам не нужно много думать о том, как заставить его исчезать, вращаться, двигаться или уменьшаться/расширяться с течением времени. Однако это немного усложняется, если вы хотите связать анимации вместе и установить зависимости между ними. Ваш код может оказаться довольно многословным и трудным для понимания, с множеством вложенных замыканий и уровней отступов.
В этой статье я расскажу, как применить мощь реактивной среды, такой как RxSwift, чтобы сделать этот код более чистым, а также более легким для чтения и выполнения. Идея пришла ко мне, когда я работал над проектом для клиента. Этот конкретный клиент очень хорошо разбирался в пользовательском интерфейсе (что идеально соответствовало моей страсти)! Они хотели, чтобы пользовательский интерфейс их приложения вел себя особым образом, со множеством плавных переходов и анимаций. Одна из их идей заключалась в том, чтобы сделать вступление к приложению, в котором рассказывалось бы, о чем оно. Они хотели, чтобы эта история рассказывалась с помощью последовательности анимаций, а не путем воспроизведения предварительно обработанного видео, чтобы его можно было легко настроить и настроить. RxSwift оказался идеальным выбором для подобной проблемы, и я надеюсь, что вы поймете это, когда закончите статью.
Краткое введение в реактивное программирование
Реактивное программирование становится основным продуктом и используется в большинстве современных языков программирования. Существует множество книг и блогов, в которых подробно объясняется, почему реактивное программирование является такой мощной концепцией и как оно помогает поощрять хороший дизайн программного обеспечения, применяя определенные принципы и шаблоны проектирования. Он также предоставляет вам набор инструментов, который может помочь вам значительно уменьшить беспорядок в коде.
Я хотел бы коснуться одного аспекта, который мне очень нравится, — легкости, с которой вы можете связывать асинхронные операции и выражать их декларативным, легко читаемым способом.
Когда дело доходит до Swift, есть две конкурирующие среды, которые помогут вам превратить его в реактивный язык программирования: ReactiveSwift и RxSwift. Я буду использовать RxSwift в своих примерах не потому, что он лучше, а потому, что я лучше с ним знаком. Я предполагаю, что вы, читатель, тоже с ним знакомы, так что я могу сразу перейти к сути.
Цепочка анимаций: старый способ
Допустим, вы хотите повернуть вид на 180°, а затем затемнить его. Вы можете использовать закрытие completion
и сделать что-то вроде этого:
UIView.animate(withDuration: 0.5, animations: { self.animatableView.transform = CGAffineTransform(rotationAngle: .pi/2) }, completion: { _ in UIView.animate(withDuration: 0.5) { self.animatableView.alpha = 0 } })
Это немного громоздко, но все же хорошо. Но что, если вы хотите вставить еще одну анимацию между ними, скажем, сместить вид вправо после того, как он повернется и до того, как он исчезнет? Применяя тот же подход, вы получите что-то вроде этого:
UIView.animate(withDuration: 0.5, animations: { self.animatableView.transform = CGAffineTransform(rotationAngle: .pi/2) }, completion: { _ in UIView.animate(withDuration: 0.5, animations: { self.animatableView.frame = self.animatableView.frame.offsetBy(dx: 50, dy: 0) }, completion: { _ in UIView.animate(withDuration: 0.5, animations: { self.animatableView.alpha = 0 }) }) })
Чем больше шагов вы добавляете к нему, тем более ступенчатым и громоздким он становится. И тогда, если вы решите изменить порядок определенных шагов, вам придется выполнить какую-то нетривиальную последовательность вырезания и вставки, которая подвержена ошибкам.
Что ж, Apple, очевидно, подумала об этом — они предлагают лучший способ сделать это, используя API анимации на основе ключевых кадров. При таком подходе приведенный выше код можно было бы переписать так:
UIView.animateKeyframes(withDuration: 1.5, delay: 0, options: [], animations: { UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.33, animations: { self.animatableView.transform = CGAffineTransform(rotationAngle: .pi/2) }) UIView.addKeyframe(withRelativeStartTime: 0.33, relativeDuration: 0.33, animations: { self.animatableView.frame = self.animatableView.frame.offsetBy(dx: 50, dy: 0) }) UIView.addKeyframe(withRelativeStartTime: 0.66, relativeDuration: 0.34, animations: { self.animatableView.alpha = 0 }) })
Это большое улучшение, основные преимущества которого заключаются в следующем:
- Код остается неизменным независимо от того, сколько шагов вы добавляете к нему.
- Изменить порядок просто (с одной оговоркой ниже)
Недостатком этого подхода является то, что вы должны мыслить в терминах относительной продолжительности, и становится сложно (или, по крайней мере, не очень просто) изменить абсолютное время или порядок шагов. Просто подумайте о том, какие расчеты вам придется выполнить и какие изменения вам придется внести в общую продолжительность и относительную продолжительность/время начала для каждой из анимаций, если вы решите, чтобы вид исчезал в течение 1 секунды, а не полсекунды. во-вторых, сохраняя все остальное то же самое. То же самое происходит, если вы хотите изменить порядок шагов — вам придется пересчитать их относительное время начала.
Учитывая недостатки, я не нахожу ни один из вышеперечисленных подходов достаточно хорошим. Идеальное решение, которое я ищу, должно удовлетворять следующим критериям:
- Код должен оставаться плоским независимо от количества шагов.
- Я должен иметь возможность легко добавлять/удалять или изменять порядок анимаций и независимо изменять их продолжительность без каких-либо побочных эффектов для других анимаций.
Цепочка анимаций: путь RxSwift
Я обнаружил, что с помощью RxSwift я могу легко достичь обеих этих целей. RxSwift — не единственный фреймворк, который вы можете использовать для чего-то подобного — подойдет любой фреймворк, основанный на промисах, который позволяет оборачивать асинхронные операции в методы, которые могут быть синтаксически связаны друг с другом без использования блоков завершения. Но RxSwift может предложить гораздо больше благодаря своему набору операторов, о которых мы поговорим чуть позже.
Вот схема того, как я собираюсь это сделать:
- Я оборачиваю каждую анимацию в функцию, которая возвращает наблюдаемое значение типа
Observable<Void>
. - Этот наблюдаемый будет испускать только один элемент до завершения последовательности.
- Элемент будет испущен, как только анимация, обернутая функцией, завершится.
- Я свяжу эти наблюдаемые вместе, используя оператор
flatMap
.
Вот как могут выглядеть мои функции:

func rotate(_ view: UIView, duration: TimeInterval) -> Observable<Void> { return Observable.create { (observer) -> Disposable in UIView.animate(withDuration: duration, animations: { view.transform = CGAffineTransform(rotationAngle: .pi/2) }, completion: { (_) in observer.onNext(()) observer.onCompleted() }) return Disposables.create() } } func shift(_ view: UIView, duration: TimeInterval) -> Observable<Void> { return Observable.create { (observer) -> Disposable in UIView.animate(withDuration: duration, animations: { view.frame = view.frame.offsetBy(dx: 50, dy: 0) }, completion: { (_) in observer.onNext(()) observer.onCompleted() }) return Disposables.create() } } func fade(_ view: UIView, duration: TimeInterval) -> Observable<Void> { return Observable.create { (observer) -> Disposable in UIView.animate(withDuration: duration, animations: { view.alpha = 0 }, completion: { (_) in observer.onNext(()) observer.onCompleted() }) return Disposables.create() } }
И вот как я все это собрал:
rotate(animatableView, duration: 0.5) .flatMap { [unowned self] in self.shift(self.animatableView, duration: 0.5) } .flatMap { [unowned self] in self.fade(self.animatableView, duration: 0.5) } .subscribe() .disposed(by: disposeBag)
Кода, безусловно, намного больше, чем в предыдущих реализациях, и он может показаться излишним для такой простой последовательности анимаций, но прелесть в том, что его можно расширить для обработки некоторых довольно сложных последовательностей анимаций, и его очень легко читать из-за декларативный характер синтаксиса.
Как только вы разберетесь с этим, вы сможете создавать анимацию столь же сложную, как фильм, и иметь в своем распоряжении большое количество удобных операторов RxSwift, которые вы можете применять для выполнения задач, которые было бы очень сложно сделать с помощью любого из вышеупомянутых подходов.
Вот как мы можем использовать оператор .concat
, чтобы сделать мой код еще более лаконичным — часть, где анимации соединяются вместе:
Observable.concat([ rotate(animatableView, duration: 0.5), shift(animatableView, duration: 0.5), fade(animatableView, duration: 0.5) ]) .subscribe() .disposed(by: disposeBag)
Вы можете вставлять задержки между анимациями следующим образом:
func delay(_ duration: TimeInterval) -> Observable<Void> { return Observable.of(()).delay(duration, scheduler: MainScheduler.instance) } Observable.concat([ rotate(animatableView, duration: 0.5), delay(0.5), shift(animatableView, duration: 0.5), delay(1), fade(animatableView, duration: 0.5) ]) .subscribe() .disposed(by: disposeBag)
Теперь давайте предположим, что мы хотим, чтобы представление вращалось определенное количество раз, прежде чем оно начнет двигаться. И мы хотим легко настроить, сколько раз он должен вращаться.
Сначала я создам метод, который непрерывно повторяет анимацию вращения и испускает элемент после каждого вращения. Я хочу, чтобы эти вращения прекратились, как только наблюдаемое будет удалено. Я мог бы сделать что-то вроде этого:
func rotateEndlessly(_ view: UIView, duration: TimeInterval) -> Observable<Void> { var disposed = false return Observable.create { (observer) -> Disposable in func animate() { UIView.animate(withDuration: duration, animations: { view.transform = view.transform.rotated(by: .pi/2) }, completion: { (_) in observer.onNext(()) if !disposed { animate() } }) } animate() return Disposables.create { disposed = true } } }
И тогда моя красивая цепочка анимаций могла бы выглядеть так:
Observable.concat([ rotateEndlessly(animatableView, duration: 0.5).take(5), shift(animatableView, duration: 0.5), fade(animatableView, duration: 0.5) ]) .subscribe() .disposed(by: disposeBag)
Вы видите, как легко контролировать, сколько раз будет вращаться вид — просто измените значение, переданное оператору take
.
Теперь я хотел бы сделать свою реализацию еще на один шаг, обернув каждую функцию анимации, которую я создал, в «реактивное» расширение UIView (доступное через суффикс .rx
). Это сделало бы его более похожим на соглашения RxSwift, где доступ к реактивным функциям обычно осуществляется через суффикс .rx
, чтобы было ясно, что они возвращают наблюдаемое.
extension Reactive where Base == UIView { func shift(duration: TimeInterval) -> Observable<Void> { return Observable.create { (observer) -> Disposable in UIView.animate(withDuration: duration, animations: { self.base.frame = self.base.frame.offsetBy(dx: 50, dy: 0) }, completion: { (_) in observer.onNext(()) observer.onCompleted() }) return Disposables.create() } } func fade(duration: TimeInterval) -> Observable<Void> { return Observable.create { (observer) -> Disposable in UIView.animate(withDuration: duration, animations: { self.base.alpha = 0 }, completion: { (_) in observer.onNext(()) observer.onCompleted() }) return Disposables.create() } } func rotateEndlessly(duration: TimeInterval) -> Observable<Void> { var disposed = false return Observable.create { (observer) -> Disposable in func animate() { UIView.animate(withDuration: duration, animations: { self.base.transform = self.base.transform.rotated(by: .pi/2) }, completion: { (_) in observer.onNext(()) if !disposed { animate() } }) } animate() return Disposables.create { disposed = true } } } }
При этом я могу собрать их вместе следующим образом:
Observable.concat([ animatableView.rx.rotateEndlessly(duration: 0.5).take(5), animatableView.rx.shift(duration: 0.5), animatableView.rx.fade(duration: 0.5) ]) .subscribe() .disposed(by: disposeBag)
Куда пойти отсюда
Как показано в этой статье, высвобождая мощь RxSwift и располагая примитивами, вы можете по-настоящему повеселиться с анимацией. Ваш код чист и легко читается, и он больше не выглядит как «код» — вы «описываете», как ваши анимации собираются вместе, и они просто оживают! Если вы хотите сделать что-то более сложное, чем описано здесь, вы, безусловно, можете сделать это, добавив больше собственных примитивов, инкапсулирующих другие типы анимации. Вы также всегда можете воспользоваться постоянно растущим арсеналом инструментов и сред, разработанных некоторыми увлеченными людьми в сообществе открытого исходного кода.
Если вы перешли на RxSwift, регулярно посещайте репозиторий RxSwiftCommunity: вы всегда можете найти что-то новое!
Если вам нужны дополнительные советы по пользовательскому интерфейсу, попробуйте прочитать статью « Как реализовать пиксель-идеальный дизайн пользовательского интерфейса iOS» от коллеги по Toptaler Романа Стеценко.
Примечания к источникам (понимание основ)
- Википедия - Swift (язык программирования)
- Lynda.com - RxSwift: шаблоны проектирования для разработчиков iOS
- We Heart Swift — основы UIView
- Угловой — наблюдаемые