iOS'ta RxSwift ve Animasyonlar

Yayınlanan: 2022-03-11

Makul miktarda UI çalışması yapmış ve bu konuda tutkulu bir iOS geliştiricisiyseniz, animasyonlar söz konusu olduğunda UIKit'in gücünü sevmelisiniz. Bir UIView'u canlandırmak pasta kadar kolaydır. Zamanla nasıl solduracağını, döndüreceğini, hareket ettireceğini veya küçülteceğini/genişleyeceğini düşünmek zorunda değilsin. Bununla birlikte, animasyonları birbirine zincirlemek ve aralarında bağımlılıklar kurmak istiyorsanız biraz dahil olur. Kodunuz, iç içe geçmiş birçok kapatma ve girinti düzeyiyle, oldukça ayrıntılı ve takip edilmesi zor olabilir.

Bu makalede, RxSwift gibi reaktif bir çerçevenin gücünün, bu kodun çok daha temiz görünmesini ve ayrıca okunması ve takip edilmesinin daha kolay olmasını sağlamak için nasıl uygulanacağını keşfedeceğim. Bu fikir, bir müşteri için bir proje üzerinde çalışırken aklıma geldi. Bu özel müşteri, kullanıcı arayüzü konusunda çok bilgiliydi (ki bu benim tutkumla mükemmel bir şekilde eşleşiyordu)! Uygulamalarının kullanıcı arayüzünün çok sayıda çok şık geçiş ve animasyonla çok özel bir şekilde davranmasını istediler. Fikirlerinden biri, uygulamanın ne hakkında olduğunu anlatan bir uygulamaya giriş yapmaktı. Bu hikayenin önceden oluşturulmuş bir videoyu oynatmak yerine bir dizi animasyonla anlatılmasını istediler, böylece kolayca ince ayar yapılabilsin ve ayarlanabilsin. RxSwift, böyle bir sorun için mükemmel bir seçim oldu, umarım makaleyi bitirdikten sonra farkına varırsınız.

Reaktif Programlamaya Kısa Giriş

Reaktif programlama bir temel haline geliyor ve modern programlama dillerinin çoğunda benimsendi. Reaktif programlamanın neden bu kadar güçlü bir kavram olduğunu ve belirli tasarım ilkelerini ve modellerini uygulayarak iyi yazılım tasarımını teşvik etmeye nasıl yardımcı olduğunu ayrıntılı olarak açıklayan çok sayıda kitap ve blog var. Ayrıca, kod karmaşasını önemli ölçüde azaltmanıza yardımcı olabilecek bir araç takımı da sağlar.

Gerçekten sevdiğim bir yönüne değinmek istiyorum - asenkron işlemleri zincirleme ve bunları bildirimsel, okunması kolay bir şekilde ifade etme kolaylığı.

Swift söz konusu olduğunda, onu reaktif bir programlama diline dönüştürmenize yardımcı olacak iki rakip çerçeve vardır: ReactiveSwift ve RxSwift. Örneklerimde RxSwift'i daha iyi olduğu için değil, daha aşina olduğum için kullanacağım. Okuyucu olarak sizin de aşina olduğunuzu varsayacağım, böylece doğrudan etine ulaşabilirim.

Zincirleme Animasyonlar: Eski Yol

Bir görünümü 180° döndürmek ve ardından karartmak istediğinizi varsayalım. completion kapanışını kullanabilir ve şöyle bir şey yapabilirsiniz:

 UIView.animate(withDuration: 0.5, animations: { self.animatableView.transform = CGAffineTransform(rotationAngle: .pi/2) }, completion: { _ in UIView.animate(withDuration: 0.5) { self.animatableView.alpha = 0 } }) 

Dönen bir karenin animasyonlu GIF'i

Biraz hantal, ama yine de tamam. Peki ya aralarına bir animasyon daha eklemek isterseniz, diyelim ki görünüm döndükten sonra ve kaybolmadan önce sağa kaydırın? Aynı yaklaşımı uygulayarak, şöyle bir şey elde edeceksiniz:

 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 }) }) }) 

Dikdörtgenin döndürüldüğü, duraklatıldığı ve ardından sağa kaydırıldığı animasyonlu GIF

Ne kadar çok adım eklerseniz, o kadar şaşırtıcı ve hantal hale gelir. Ve sonra, belirli adımların sırasını değiştirmeye karar verirseniz, hataya açık olan bazı önemsiz olmayan kes ve yapıştır dizisi gerçekleştirmeniz gerekecektir.

Eh, Apple açıkça bunu düşündü - ana kare tabanlı bir animasyon API'si kullanarak bunu yapmanın daha iyi bir yolunu sunuyorlar. Bu yaklaşımla, yukarıdaki kod şu şekilde yeniden yazılabilir:

 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 }) })

Bu büyük bir gelişmedir, başlıca avantajları şunlardır:

  1. Kod, ona kaç adım eklediğinize bakılmaksızın düz kalır
  2. Sıralamayı değiştirmek basittir (aşağıda bir uyarı ile)

Bu yaklaşımın dezavantajı, göreceli süreler açısından düşünmek zorunda olmanız ve adımların mutlak zamanlamasını veya sırasını değiştirmenin zor (veya en azından çok basit değil) olmasıdır. Görünümü yarım saniye yerine 1 saniye içinde soldurmaya karar verdiyseniz, animasyonların her biri için toplam süre ve göreceli süreler/başlangıç ​​zamanlarında hangi hesaplamaları yapmanız gerektiğini ve ne tür değişiklikler yapmanız gerektiğini düşünün. ikinci her şeyi aynı tutarken. Adımların sırasını değiştirmek isterseniz de aynı şey geçerlidir; bunların göreli başlangıç ​​zamanlarını yeniden hesaplamanız gerekir.

Dezavantajları göz önüne alındığında, yukarıdaki yaklaşımlardan hiçbirini yeterince iyi bulmuyorum. Aradığım ideal çözüm aşağıdaki kriterleri karşılamalıdır:

  1. Adım sayısından bağımsız olarak kod düz kalmalıdır
  2. Diğer animasyonlara herhangi bir yan etki olmadan, animasyonları kolayca ekleyebilir/kaldırabilir veya yeniden sıralayabilir ve sürelerini bağımsız olarak değiştirebilmeliyim.

Zincirleme Animasyonlar: RxSwift Yolu

RxSwift kullanarak bu hedeflerin her ikisini de kolayca gerçekleştirebileceğimi buldum. RxSwift, böyle bir şey yapmak için kullanabileceğiniz tek çerçeve değildir; zaman uyumsuz işlemleri, tamamlama bloklarını kullanmadan sözdizimsel olarak birbirine zincirlenebilen yöntemlere sarmanıza izin veren herhangi bir söz tabanlı çerçeve işe yarar. Ancak RxSwift'in, biraz sonra değineceğimiz operatör dizisiyle sunacağı çok şey var.

İşte bunu nasıl yapacağımın ana hatları:

  1. Animasyonların her birini, Observable<Void> türünde bir gözlemlenebilir döndüren bir işleve saracağım.
  2. Bu gözlemlenebilir, diziyi tamamlamadan önce yalnızca bir öğe yayacaktır.
  3. Öğe, işlev tarafından sarılmış animasyon tamamlanır tamamlanmaz yayılacaktır.
  4. Bu gözlemlenebilirleri flatMap operatörünü kullanarak zincirleyeceğim.

İşlevlerim şöyle görünebilir:

 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() } }

Ve işte hepsini nasıl bir araya getirdim:

 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)

Kesinlikle önceki uygulamalardan çok daha fazla koddur ve bu kadar basit bir animasyon dizisi için biraz fazla abartılmış gibi görünebilir, ancak güzelliği, bazı oldukça karmaşık animasyon dizilerini işlemek için genişletilebilmesi ve okunması çok kolay olmasıdır. sözdiziminin bildirimsel doğası.

Bir kez ele aldıktan sonra, bir film kadar karmaşık animasyonlar oluşturabilir ve yukarıda bahsedilen yaklaşımlardan herhangi biriyle yapılması çok zor olacak şeyleri başarmak için uygulayabileceğiniz çok çeşitli kullanışlı RxSwift operatörlerine sahip olabilirsiniz.

Kodumu daha da özlü hale getirmek için .concat operatörünü şu şekilde kullanabiliriz—animasyonların zincirlendiği kısım:

 Observable.concat([ rotate(animatableView, duration: 0.5), shift(animatableView, duration: 0.5), fade(animatableView, duration: 0.5) ]) .subscribe() .disposed(by: disposeBag)

Animasyonlar arasına aşağıdaki gibi gecikmeler ekleyebilirsiniz:

 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)

Şimdi, hareket etmeye başlamadan önce görünümün belirli sayıda dönmesini istediğimizi varsayalım. Ve kaç kez dönmesi gerektiğini kolayca ayarlamak istiyoruz.

İlk önce, döndürme animasyonunu sürekli olarak tekrarlayan ve her döndürmeden sonra bir öğe yayan bir yöntem yapacağım. Bu rotasyonların, gözlemlenebilir olan yok edilir edilmez durmasını istiyorum. Bunun gibi bir şey yapabilirim:

 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 } } }

Ve sonra güzel animasyonlar zincirim şöyle görünebilir:

 Observable.concat([ rotateEndlessly(animatableView, duration: 0.5).take(5), shift(animatableView, duration: 0.5), fade(animatableView, duration: 0.5) ]) .subscribe() .disposed(by: disposeBag)

Görünümün kaç kez döndürüleceğini kontrol etmenin ne kadar kolay olduğunu görüyorsunuz - sadece take operatörüne iletilen değeri değiştirin.

Geliştirilmiş animasyonun animasyonlu GIF'i

Şimdi, oluşturduğum animasyon işlevlerinin her birini UIView'ın “Reaktif” uzantısına ( .rx son ekiyle erişilebilir) kaydırarak uygulamamı bir adım daha ileri götürmek istiyorum. Bu, reaktif işlevlere genellikle .rx son eki aracılığıyla erişilerek, bunların gözlemlenebilir bir değer döndürdüklerini açıkça gösteren RxSwift kurallarının satırları boyunca daha fazla olmasını sağlar.

 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 } } } }

Bununla, onları şu şekilde bir araya getirebilirim:

 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)

Buradan Nereye Gidilir

Bu makalenin de gösterdiği gibi, RxSwift'in gücünü açığa çıkararak ve ilkel öğelerinizi yerleştirdikten sonra, animasyonlarla gerçekten eğlenebilirsiniz. Kodunuz temiz ve okunması kolay ve artık "kod" gibi görünmüyor; animasyonlarınızın nasıl bir araya getirildiğini "açıklarsınız" ve canlanırlar! Burada açıklanandan daha ayrıntılı bir şey yapmak istiyorsanız, bunu diğer animasyon türlerini içine alan kendi ilkel öğelerinizi ekleyerek kesinlikle yapabilirsiniz. Ayrıca, açık kaynak topluluğundaki bazı tutkulu insanlar tarafından geliştirilen, sürekli büyüyen araç ve çerçevelerden her zaman yararlanabilirsiniz.

Bir RxSwift dönüştürücüsüyseniz, RxSwiftCommunity deposunu düzenli olarak ziyaret etmeye devam edin: her zaman yeni bir şeyler bulabilirsiniz!

Daha fazla kullanıcı arayüzü tavsiyesi arıyorsanız, Toptaler Roman Stetsenko'nun Piksel Mükemmelliğinde Bir iOS Kullanıcı Arayüzü Tasarımı Nasıl Uygulanır'ı okumayı deneyin.


Kaynak Notları (Temelleri Anlamak)

  • Wikipedia - Swift (programlama dili)
  • Lynda.com - RxSwift: iOS Geliştiricileri için Tasarım Modelleri
  • We Heart Swift - UIView Temelleri
  • Açısal - Gözlenebilirler