تعمق في تصميم الحركة: انتقالات iOS المتقدمة
نشرت: 2020-11-09يجلب الإبداع معه رغبة مستمرة في إقناع المستخدم. لقرون ، حاول الإنسان التلاعب بالوسائل المتاحة لإعادة إنشاء التفاعل الأكثر طبيعية ، مع الأخذ بالطبيعة كمثال أساسي.
عند اكتشاف العالم ، يصبح الشخص أكثر وأكثر حساسية للتفاصيل الدقيقة للعالم من حوله ، مما يسمح له بشكل غريزي بالتمييز بين الاصطناعي والكائنات الحية. هذا الخط غير واضح مع تطور التكنولوجيا ، حيث يهدف البرنامج إلى خلق بيئة يصف فيها المستخدم تجربته في عالم تم إنشاؤه بشكل مصطنع بأنها طبيعية.

صدى الطبيعة في تصميم التطبيق
ستقدم هذه المقالة عملية طمس الحدود باستخدام مثال تحويل الشكل في الرسوم المتحركة التفاعلية للعناصر اليومية في معظم تطبيقات iOS. إحدى طرق تقليد الطبيعة هي من خلال التحولات المختلفة لموضع الشيء في الوقت المناسب. يتم عرض الوظائف النموذجية لوقت الرسوم المتحركة أدناه.

بالاقتران مع الاستخدام الصحيح للوقت عن طريق إضافة تحويل هندسي ، يمكننا الحصول على عدد لا حصر له من التأثيرات . كدليل على إمكانيات التصميم والتكنولوجيا اليوم ، تم إنشاء تطبيق Motion Patterns ، والذي يتضمن حلولًا شائعة ، طورتها شركة تطوير البرمجيات لدينا. بما أنني لست كاتبًا ، بل مبرمجًا ، ولا شيء يتحدث بشكل أفضل من الأمثلة الحية ، فلا خيار لدي سوى دعوتك إلى هذا العالم الرائع!
عينات لاكتشافها

دعونا نلقي نظرة على كيف يمكن لتصميم الحركة أن يحول التصميم العادي إلى شيء استثنائي! في الأمثلة أدناه ، يوجد على الجانب الأيسر تطبيق يستخدم الرسوم المتحركة الأساسية فقط لنظام iOS ، بينما يوجد على الجانب الأيمن نسخة من نفس التطبيق مع بعض التحسينات.
تأثير "الغوص العميق"
هذا هو انتقال باستخدام التحويل بين حالتين من وجهة النظر . مبني على أساس مجموعة ، بعد تحديد خلية معينة ، يحدث الانتقال إلى تفاصيل عنصر عن طريق تحويل عناصره الفردية *. حل إضافي هو استخدام انتقالات تفاعلية ، والتي تسهل استخدام التطبيق.
* في الواقع نسخ / تعيين عناصر البيانات في عرض مؤقت يشارك في الانتقال ، بين بدايته ونهايته ... لكنني سأشرح ذلك لاحقًا في هذه المقالة ...
تأثير "نظرة خاطفة على الحافة"
يؤدي استخدام الرسوم المتحركة لعرض التمرير في عملها إلى تحويل الصورة في شكل ثلاثي الأبعاد للحصول على تأثير مكعب . العامل الرئيسي المسؤول عن التأثير هو إزاحة عرض التمرير.
تأثير "ربط النقاط"
هذا هو الانتقال بين المشاهد التي تحول الكائن المصغر إلى الشاشة بأكملها . المجموعات المستخدمة لهذا الغرض تعمل بشكل متزامن ، التغيير على شاشة واحدة يتوافق مع تحول في الأخرى. بالإضافة إلى ذلك ، عند إدخال الصورة المصغرة في الخلفية ، يظهر تأثير اختلاف المنظر عند التمرير بين المشاهد.
تأثير "Shift the Shape"
النوع الأخير من الرسوم المتحركة هو نوع بسيط باستخدام مكتبة Lottie. إنه الاستخدام الأكثر شيوعًا لتحريك الرموز. في هذه الحالة ، هذه هي الرموز الموجودة على شريط علامات التبويب. بالإضافة إلى ذلك ، من خلال تغيير علامات التبويب المناسبة ، تم استخدام رسم متحرك للانتقال في اتجاه معين لزيادة تكثيف تأثير التفاعل.
الغوص بشكل أعمق: نمط تصميم الحركة الأول لدينا
حان الوقت الآن للوصول إلى النقطة… نحن بحاجة إلى التعمق أكثر في هيكل الآليات التي تتحكم في هذه الأمثلة.
في هذه المقالة ، سأقدم لك أول نمط لتصميم الحركة ، والذي أطلقنا عليه اسم "Dive Deeper" مع وصف تجريدي لاستخدامه ، دون الخوض في تفاصيل محددة. نخطط لإتاحة الكود الدقيق والمستودع بأكمله للجميع في المستقبل ، دون أي قيود.
لا تعتبر بنية المشروع وأنماط تصميم البرمجة الصارمة المطبقة أولوية في الوقت الحالي - فنحن نركز على الرسوم المتحركة والانتقال.
في هذه المقالة ، سنستخدم مجموعتين من الميزات المتوفرة لإدارة العروض أثناء انتقالات المشهد. وبالتالي ، أود أن أشير إلى أن هذه المقالة مخصصة للأشخاص الذين هم على دراية نسبيًا بـ UIKit وبناء جملة Swift.
https://developer.apple.com/documentation/uikit/uiviewcontrolleranimatedtransitioning
https://developer.apple.com/documentation/uikit/uipercentdriveninteractivetransition
https://developer.apple.com/library/archive/featuredarticles/ViewControllerPGforiPhoneOS/CustomizingtheTransitionAnimations.html
أولاً: الهيكل
بالنسبة للإصدار الأساسي الذي يطبق حلًا معينًا ، ستكون هناك حاجة إلى عدة فئات مساعدة ، مسؤولة عن توفير المعلومات الضرورية حول وجهات النظر المتضمنة في الانتقال ، والتحكم في الانتقال نفسه والتفاعلات.

ستكون الفئة الأساسية المسؤولة عن إدارة الانتقال ، الوكيل ، هي الرسوم المتحركة الانتقالية. إنه يقرر الطريقة التي سيتم بها النقل ويغطي الوظائف القياسية اللازمة لأداء الإجراء المقدم من قبل فريق Apple.
/// هذه فئة أساسية للانتقالات التي لها سلوك مختلف في التقديم والاستبعاد خلال مدة محددة. فتح فئة TransitionAnimator: NSObject ، UIViewControllerAnimatedTransitioning { /// للإشارة إلى ما إذا كان يتم تقديم الانتقال أو رفضه. تقديم var: منطقي = صحيح /// الفاصل الزمني الذي يحدث فيه الانتقال بالكامل. مدة السماح الخاص: TimeInterval /// المُهيئ الافتراضي للرسوم المتحركة الانتقالية بالقيم الافتراضية. /// - مدة المعلمة: الفاصل الزمني الذي يحدث فيه الانتقال بالكامل. /// - تقديم المعلمة: مؤشر ما إذا كان يعرض الانتقال أو يرفضه. public init (المدة: TimeInterval = 0.5 ، التقديم: Bool = true) { المدة الذاتية = المدة self.presenting = التقديم super.init () } /// يحدد مدة الانتقال. /// - انتقال المعلمة السياق: سياق الانتقال الحالي. /// - الإرجاع: المدة المحددة عند تهيئة الرسوم المتحركة. public func transferDuration (باستخدام transferContext: UIViewControllerContextTransitioning؟) -> TimeInterval { العودة الذاتية } /// قلب متحرك الانتقال ، في هذه الوظيفة يحدث الانتقال. /// - هام: يعد تجاوز هذه الوظيفة في نوع أكثر تحديدًا من الانتقال أمرًا بالغ الأهمية لأداء الرسوم المتحركة. /// - انتقال المعلمة السياق: سياق الانتقال. public func animateTransition (باستخدام transferContext: UIViewControllerContextTransitioning) {} }
استنادًا إلى TransitionAnimator ، نقوم بإنشاء ملف TransformTransition ، وتتمثل مهمته في إجراء انتقال محدد ، والانتقال مع التحول (الأبوة والأمومة)
/// تنفيذ التحول التحول. فتح فئة TransformTransition: TransitionAnimator { /// عرض النموذج الذي يحمل جميع المواصفات اللازمة للانتقال. عرض var الخاص النموذج: TransformViewModel /// المُهيئ الافتراضي لانتقال التحويل. /// - عرض المعلمة النموذج: عرض نموذج انتقال التحويل. /// - مدة المعلمة: مدة الانتقال. init (viewModel: TransformViewModel، المدة: TimeInterval) { self.viewModel = viewModel super.init (المدة: المدة ، التقديم: viewModel.presenting) }
يتضمن تكوين فئة TransformTransition TransformViewModel ، والتي ، كما يوحي الاسم ، تُعلم آلية نماذج العرض التي سيتم تطبيق هذا الانتقال عليها.
/// عرض نموذج تحويل التحويل الذي يحتوي على معلومات أساسية عنه. الدرجة النهائية TransformViewModel { /// يشير إلى ما إذا كان انتقال التحويل يقدم العرض أو يرفضه. دعونا نقدم: منطقية /// مصفوفة من النماذج مع مواصفات حول التحويل لكل عرض. اسمحوا النماذج: [TransformModel] /// المُهيئ الافتراضي لنموذج عرض التحويل. /// - تقديم المعلمة: يشير إلى ما إذا كان يقدم أو يرفض انتقال التحويل. /// - نماذج المعلمات: مصفوفة من النماذج ذات مواصفات حول التحويل لكل عرض. init (تقديم: منطقي ، نماذج: [TransformModel]) { self.presenting = التقديم النماذج الذاتية = النماذج } }
نموذج التحويل عبارة عن فئة مساعدة تصف العناصر المحددة لوجهات النظر المتضمنة في الانتقال الموجود في الأصل ، وعادة ما تكون وجهات نظر وحدة التحكم التي يمكن تحويلها.
في حالة الانتقال ، فهي خطوة ضرورية لأن هذا الانتقال يتكون من عمليات وجهات نظر محددة بين دول معينة.
ثانياً: التنفيذ
نقوم بتوسيع نموذج العرض الذي نبدأ منه الانتقال مع Transformable ، مما يفرض علينا تنفيذ وظيفة من شأنها إعداد جميع العناصر الضرورية. يمكن أن ينمو حجم هذه الوظيفة بسرعة كبيرة ، لذا أقترح عليك تقسيمها إلى أجزاء أصغر ، على سبيل المثال لكل عنصر.

/// بروتوكول للفئة التي تريد إجراء تحويل التحول. بروتوكول قابل للتحويل: ViewModel { /// يعد نماذج لوجهات النظر التي تشارك في عملية الانتقال. /// - المعامل fromView: العرض الذي يبدأ منه الانتقال /// - المعلمة toView: العرض الذي ينتقل إليه الانتقال. /// - تقديم المعلمة: يشير إلى ما إذا كان يتم التقديم أو الرفض. /// - العوائد: مصفوفة من الهياكل التي تحتوي على جميع المعلومات المطلوبة جاهزة لتحويل الانتقال لكل عرض. func PreparTransitionModels (fromView: UIView، toView: UIView، Presenting: Bool) -> [TransformModel] }
لا يعني الافتراض كيفية البحث عن بيانات وجهات النظر المشاركة في التحول. في المثال الخاص بي ، استخدمت العلامات التي تمثل طريقة عرض معينة. لديك مطلق الحرية في هذا الجزء من التنفيذ.
نماذج تحويلات العرض المحددة (TransformModel) هي أصغر نموذج في القائمة بأكملها. وهي تتكون من معلومات التحويل الرئيسية مثل عرض البداية وعرض الانتقال وإطار البداية وإطار النهاية ومركز البداية ومركز النهاية والرسوم المتحركة المتزامنة والعملية النهائية. لا يلزم استخدام معظم المعلمات أثناء التحويل ، لذلك يكون لها قيمها الافتراضية الخاصة. للحصول على الحد الأدنى من النتائج ، يكفي استخدام تلك المطلوبة فقط.
/// المُهيئ الافتراضي لنموذج التحويل بالقيم الافتراضية. /// - عرض المعلمة الأولي: العرض الذي يبدأ منه الانتقال. /// - Parameter phantomView: العرض الذي يتم تقديمه أثناء انتقال التحويل. /// - Parameter initialFrame: إطار العرض الذي يبدأ تحويل التحويل. /// - Parameter finalFrame: إطار الرؤية الذي سيتم تقديمه في نهاية انتقال التحويل. /// - Parameter initialCenter: مطلوبة عندما تكون نقطة العرض المركزية الأولية مختلفة عن مركز العرض الأولي. /// - Parameter finalCenter: مطلوب عندما تكون وجهة نظر المركز النهائية مختلفة عن مركز العرض النهائي. /// - ParameterallelAnimation: الرسوم المتحركة الإضافية للعرض التي يتم إجراؤها أثناء انتقال التحويل. /// - إكمال المعلمة: تم تشغيل كتلة التعليمات البرمجية بعد انتقال التحويل. /// - ملاحظة: العرض المبدئي فقط هو المطلوب لتنفيذ أكثر نسخة من تحويل التحويل. init (initialView: UIView، phantomView: UIView = UIView () ، initialFrame: CGRect = CGRect () ، الإطار النهائي: CGRect = CGRect () ، initialCenter: CGPoint؟ = لا شيء ، finalCenter: CGPoint؟ = لا شيء ، متوازي الرسوم المتحركة: (() -> باطل)؟ = لا شيء ، الإكمال: (() -> باطل)؟ = لا شيء) { self.initialView = عرض أولي self.phantomView = phantomView self.initialFrame = الإطار الأولي self.finalFrame = finalFrame self.parallelAnimation = الرسوم المتحركة المتوازية self.completion = الانتهاء self.initialCenter = initialCenter ؟؟ CGPoint (x: initialFrame.midX، y: initialFrame.midY) self.finalCenter = finalCenter ؟؟ CGPoint (x: finalFrame.midX، y: finalFrame.midY) }
ربما يكون قد جذب انتباهك بواسطة العرض الوهمي. هذه هي اللحظة التي سأشرح فيها سير العمل لانتقالات iOS. في أقصر شكل ممكن ...

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

في هذه الحالة ، لدينا انتقال يحول عرضًا إلى آخر عن طريق تغيير شكله وحجمه. للقيام بذلك ، في بداية الانتقال ، أقوم بإنشاء PhantomView للعنصر المحدد وأضفه إلى الحاوية. FadeView هي طريقة عرض مساعدة لإضافة نعومة إلى الانتقال العام.
/// قلب التحول ، حيث يتم الانتقال. تجاوز `` TransitionAnimator.animateTransition (...) ''. /// - انتقال المعلمة: سياق انتقال التحويل الحالي. تجاوز فتح func animateTransition (باستخدام transferContext: UIViewControllerContextTransitioning) { الحارس اسمحوا toViewController = transferContext.view (forKey: .to) ، دع fromViewController = transferContext.view (forKey: .from) وإلا { إرجاع Log.unuableState () } اسمح لـ containerView = transferContext.containerView اسمحوا المدة = وقت الانتقال (باستخدام: TransContext) دع fadeView = toViewController.makeFadeView (عتامة: (! Presenting) .cgFloatValue) دعونا النماذج = viewModel.models دعونا عرض = تقديم؟ toViewController: fromViewController Models.forEach {$ 0.initialView.isHidden = true} تم تقديمView.isHidden = صحيح ContainerView.addSubview (toViewController) إذا كان التقديم { containerView.insertSubview (fadeView ، belowSubview: toViewController) } آخر { ContainerView.addSubview (fadeView) } containerView.addSubviews (viewModel.models.map {$ 0.phantomView})
في الخطوة التالية ، أقوم بتحويله إلى الشكل المستهدف من خلال عمليات التحويل ، واعتمادًا على ما إذا كان عرضًا تقديميًا أو استدعاءًا ، فإنه يقوم بإجراء عمليات إضافية لتنظيف طرق عرض محددة - هذه هي الوصفة الكاملة لهذا الانتقال.
السماح للرسوم المتحركة: () -> الفراغ = {[النفس الضعيفة] فيها guard let self = self else {return Log.unexpectedState ()} fadeView.alpha = self.presenting.cgFloatValue نماذج لكل { دع المركز = تمثيل الذات؟ 0 دولار ، المركز النهائي: 0 دولار ، المركز الأولي لنتحول = تمثيل الذات؟ $ 0.PresentTransform: $ 0.dismTransform $ 0.phantomView.setTransformAndCenter (تحويل ، مركز) } Models.compactMap {$ 0.parallelAnimation}. لكل {$ 0 ()} } دع الإكمال: (Bool) -> Void = {_ in TransitionContext.completeTransition (! transferContext.transitionWasCancelled) عرض View.isHidden = خطأ Models.compactMap {$ 0.completion}. forEach {$ 0 ()} Models.forEach {$ 0.initialView.isHidden = false} if! self.presenting && transferContext.transitionWasCancelled { toViewController.removeFromSuperview () fadeView.remove FromSuperview () } } UIView.animate (مع المدة: المدة ، تأخير: 0 ، باستخدام الربيع مع التخميد: 1 ، سرعةالربيع الأولي: 0.5 ، الخيارات: .curveEaseOut ، الرسوم المتحركة: الرسوم المتحركة ، الانتهاء: الانتهاء)
ثالثا: المكون الخاص
بعد تجميع جميع الوظائف والفئات والبروتوكولات معًا ، يجب أن تبدو النتيجة كما يلي:
سيكون العنصر الأخير في انتقالنا هو تفاعله الكامل. لهذا الغرض ، سنستخدم Pan Gesture المضافة في عرض وحدة التحكم ، TransitionInteractor ...
/// وسيط للتعامل مع الانتقال التفاعلي. الدرجة النهائية TransitionInteractor: UIPercentDrivenInteractiveTransition { /// يشير إلى ما إذا كان الانتقال قد بدأ. var hasStarted = خطأ /// يشير إلى ما إذا كان الانتقال يجب أن ينتهي. var shouldFinish = خطأ }
... التي نهيئها أيضًا في جسم وحدة التحكم.
/// يعالج إيماءة عموم على عناصر عرض المجموعة ، ويدير الانتقال. objc func handlePanGesture (_ gestureRecognizer: UIPanGestureRecognizer) { دعونا في المئةThreshold: CGFloat = 0.1 اسمحوا الترجمة = gestureRecognizer.translation (in: view) دع verticalMovement = translation.y / view.bounds.height Let upwardMovement = fminf (تعويم (حركة رأسية) ، 0.0) ترك upwardMovementPercent = fminf (القيمة المطلقة (upwardMovement) ، 0.9) دع التقدم = CGFloat (upwardMovementPercent) الحارس دع التفاعلي = التفاعل والتحكم آخر {العودة} التبديل لفتة Recognizer.state { بداية الحالة: Interactiveor.hasStarted = صحيح اسمحوا tapPosition = gestureRecognizer.location (in: collectionView) showDetailViewController From (الموقع: tapPosition) الحالة .changed: Interactiveor.shouldFinish = التقدم> نسبة مئوية التفاعل أو التحديث (التقدم) حالة. تم إلغاؤها: Interactiveor.hasStarted = خطأ التفاعل أو الإلغاء () تم تمديد القضية: Interactiveor.hasStarted = خطأ المتفاعل ؟ Interactiveor.finish () : Interactiveor.cancel () إفتراضي: فترة راحة } }
يجب أن يكون التفاعل الجاهز على النحو التالي:
إذا سار كل شيء وفقًا للخطة ، فسيكسب تطبيقنا الكثير في نظر مستخدميه.

اكتشف فقط قمة جبل جليدي
في المرة القادمة ، سأشرح تنفيذ القضايا ذات الصلة بتصميم الحركة في الإصدارات التالية.
يعد التطبيق والتصميم وكود المصدر ملكًا لشركة M Liquido ، وقد تم إنشاؤها بشغف بواسطة مصممين ومبرمجين موهوبين لسنا مسؤولين عن استخدامها في تطبيقاتنا. سيكون كود المصدر المفصل متاحًا في المستقبل عبر حسابنا على github - ندعوك لمتابعتنا!
شكرا لاهتمامكم ونراكم قريبا!