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

صورة GIF متحركة لمربع دوار

إنه ضخم بعض الشيء ، لكن لا يزال على ما يرام. ولكن ماذا لو كنت تريد إدراج رسم متحرك آخر بينهما ، على سبيل المثال ، قم بتحويل العرض إلى اليمين بعد تدويره وقبل أن يتلاشى؟ بتطبيق نفس النهج ، سينتهي بك الأمر بشيء مثل هذا:

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

صورة GIF متحركة للمستطيل تدور ، وتتوقف مؤقتًا ، ثم تنزلق إلى اليمين

كلما زاد عدد الخطوات التي تضيفها إليها ، زادت تعقيدها وتعقيدها. ثم إذا قررت تغيير ترتيب خطوات معينة ، فسيتعين عليك إجراء بعض تسلسل القص واللصق غير التافه ، والذي يكون عرضة للخطأ.

حسنًا ، من الواضح أن شركة Apple قد فكرت في ذلك - فهي تقدم طريقة أفضل للقيام بذلك ، باستخدام واجهة برمجة تطبيقات الرسوم المتحركة القائمة على الإطار الرئيسي. باستخدام هذا النهج ، يمكن إعادة كتابة الكود أعلاه على النحو التالي:

 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. يظل الرمز ثابتًا بغض النظر عن عدد الخطوات التي تضيفها إليه
  2. تغيير الأمر بسيط (مع وجود تحذير واحد أدناه)

عيب هذا النهج هو أنه يجب عليك التفكير من حيث الفترات النسبية ويصبح من الصعب (أو على الأقل ليس واضحًا جدًا) تغيير التوقيت أو الترتيب المطلق للخطوات. فكر فقط في الحسابات التي يتعين عليك إجراؤها ونوع التغييرات التي سيتعين عليك إجراؤها على المدة الإجمالية والمدد النسبية / أوقات البدء لكل من الرسوم المتحركة إذا قررت جعل العرض يتلاشى في غضون ثانية واحدة بدلاً من نصف ثانية ثانيًا مع الاحتفاظ بكل شيء كما هو. ينطبق الأمر نفسه إذا كنت تريد تغيير ترتيب الخطوات - فسيتعين عليك إعادة حساب أوقات البدء النسبية.

نظرًا للعيوب ، لا أجد أيًا من الأساليب المذكورة أعلاه جيدة بما فيه الكفاية. الحل المثالي الذي أبحث عنه يجب أن يفي بالمعايير التالية:

  1. يجب أن يظل الرمز ثابتًا بغض النظر عن عدد الخطوات
  2. يجب أن أكون قادرًا على إضافة / إزالة أو إعادة ترتيب الرسوم المتحركة بسهولة وتغيير مدتها بشكل مستقل دون أي آثار جانبية للرسوم المتحركة الأخرى

تسلسل الرسوم المتحركة: طريقة RxSwift

لقد وجدت أنه باستخدام RxSwift ، يمكنني بسهولة تحقيق هذين الهدفين. RxSwift ليس إطار العمل الوحيد الذي يمكنك استخدامه للقيام بشيء من هذا القبيل - أي إطار عمل قائم على الوعد يتيح لك التفاف العمليات غير المتزامنة في طرق يمكن ربطها معًا بشكل متسلسل دون استخدام كتل الإكمال. لكن لدى RxSwift الكثير لتقدمه مع مجموعة مشغليها ، والتي سنتطرق إليها لاحقًا.

إليكم الخطوط العريضة لكيفية القيام بذلك:

  1. سوف أقوم بلف كل من الرسوم المتحركة في دالة تقوم بإرجاع نوع يمكن ملاحظته Observable<Void> .
  2. سيصدر هذا الملحوظ عنصرًا واحدًا فقط قبل إكمال التسلسل.
  3. سيتم إصدار العنصر بمجرد اكتمال الرسم المتحرك الملفوف بواسطة الوظيفة.
  4. سأقوم بربط هذه الملاحظات معًا باستخدام عامل التشغيل 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 .

صورة GIF متحركة للرسوم المتحركة المحسّنة

الآن ، أود أن أقوم بالتنفيذ خطوة إلى الأمام عن طريق لف كل منها إذا كانت وظائف الرسوم المتحركة التي أنشأتها في الامتداد "التفاعلي" لـ 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 مثالي Pixel بواسطة زميله Toptaler Roman Stetsenko.


ملاحظات المصدر (فهم الأساسيات)

  • ويكيبيديا - سويفت (لغة برمجة)
  • Lynda.com - RxSwift: أنماط تصميم لمطوري iOS
  • نحن قلب سويفت - أساسيات UIView
  • الزاوي - المراقبات