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

في مقال سابق ، نظرت إلى كيفية تحريك عرض وحدة تحكم جديدة.
لقد viewController
على حقيقة أن viewController
يمكنه إظهار وإخفاء الرسوم المتحركة:

الآن سوف نعلمه أن يستجيب لفتة الإخفاء.
الانتقال التفاعلي
إضافة لفتة وثيقة
لتعليم وحدة التحكم للإغلاق بشكل تفاعلي ، تحتاج إلى إضافة لفتة ومعالجتها. سيكون كل العمل في فئة TransitionDriver
:
class TransitionDriver: UIPercentDrivenInteractiveTransition { func link(to controller: UIViewController) { presentedController = controller panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handle(recognizer:))) presentedController?.view.addGestureRecognizer(panRecognizer!) } private var presentedController: UIViewController? private var panRecognizer: UIPanGestureRecognizer? }
يمكنك إرفاق معالج في موقع DimmPresentationController ، داخل PanelTransition:
private let driver = TransitionDriver() func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? { driver.link(to: presented) let presentationController = DimmPresentationController(presentedViewController: presented, presenting: presenting) return presentationController }
في الوقت نفسه ، يجب أن تشير إلى أن إخفاء أصبح قابلاً للإدارة (لقد فعلنا هذا بالفعل في المقالة الأخيرة):
التعامل مع لفتة
لنبدأ بالإيماءة الختامية: إذا قمت بسحب اللوحة لأسفل ، ستبدأ الرسوم المتحركة للإغلاق ، وستؤثر حركة الإصبع على درجة الإغلاق.
يسمح UIPercentDrivenInteractiveTransition
بالتقاط الرسوم المتحركة الانتقالية والتحكم فيها يدويًا. فقد update
، finish
، cancel
الأساليب. أنها مريحة للقيام معالجة الإيماءات في فئتها الفرعية.
معالجة الإيماءات
private func handleDismiss(recognizer r: UIPanGestureRecognizer) { switch r.state { case .began: pause()
.begin
ابدأ الفزع بالطريقة الأكثر شيوعًا. قمنا بحفظ الرابط إلى وحدة التحكم في link(to:)
الطريقة
.changed
حساب الزيادة وتمريرها إلى طريقة update
. يمكن أن تختلف القيمة المقبولة من 0 إلى 1 ، لذلك سنتحكم في درجة إتمام الرسوم المتحركة من interactionControllerForDismissal(using:)
method. تم إجراء الحسابات في امتداد الإيماءة ، بحيث يصبح الرمز أكثر نظافة.
حسابات لفتة private extension UIPanGestureRecognizer { func incrementToBottom(maxTranslation: CGFloat) -> CGFloat { let translation = self.translation(in: view).y setTranslation(.zero, in: nil) let percentIncrement = translation / maxTranslation return percentIncrement } }
تستند الحسابات إلى maxTranslation
، maxTranslation
وحدة التحكم المعروضة:
var maxTranslation: CGFloat { return presentedController?.view.frame.height ?? 0 }
.end
نحن ننظر إلى اكتمال لفتة. قاعدة الاكتمال: إذا كان أكثر من النصف قد تحول ، ثم إغلاق. في هذه الحالة ، يجب مراعاة الإزاحة ليس فقط من خلال الإحداثيات الحالية ، ولكن velocity
أيضًا. لذلك نحن نتفهم نية المستخدم: قد لا ينتهي حتى المنتصف ، ولكن انتقد كثيرًا. أو العكس: إنزال ، ولكن انتقد للعودة.
حسابات ProjectedLocation private extension UIPanGestureRecognizer { func isProjectedToDownHalf(maxTranslation: CGFloat) -> Bool { let endLocation = projectedLocation(decelerationRate: .fast) let isPresentationCompleted = endLocation.y > maxTranslation / 2 return isPresentationCompleted } func projectedLocation(decelerationRate: UIScrollView.DecelerationRate) -> CGPoint { let velocityOffset = velocity(in: view).projectedOffset(decelerationRate: .normal) let projectedLocation = location(in: view!) + velocityOffset return projectedLocation } } extension CGPoint { func projectedOffset(decelerationRate: UIScrollView.DecelerationRate) -> CGPoint { return CGPoint(x: x.projectedOffset(decelerationRate: decelerationRate), y: y.projectedOffset(decelerationRate: decelerationRate)) } } extension CGFloat {
- سيحدث - إذا قمت بقفل شاشة الهاتف أو إذا اتصلوا. يمكنك التعامل معها ككتلة .ended
أو إلغاء إجراء.
.failed
- سيحدث إذا تم إلغاء الإيماءة بواسطة لفتة أخرى. لذلك ، على سبيل المثال ، يمكن لفتة السحب إلغاء لفتة النقر.
.possible
- الحالة الأولية للإيماءة ، عادة لا تتطلب الكثير من العمل.
الآن يمكن أيضًا إغلاق اللوحة بضغطة واحدة ، ولكن زر الإغلاق قد dismiss
. حدث هذا بسبب وجود خاصية wantsInteractiveStart
في TransitionDriver
، وهذا true
افتراضيًا. هذا أمر طبيعي بالنسبة للسحب ، لكنه يمنع dismiss
المعتادة.
دعونا نهدم السلوك بناءً على حالة الإيماءة. إذا بدأت الإيماءة ، فهذا إغلاق تفاعلي ، وإذا لم تبدأ ، فعندئذٍ الإغلاق المعتاد:
override var wantsInteractiveStart: Bool { get { let gestureIsActive = panRecognizer?.state == .began return gestureIsActive } set { } }
الآن يمكن للمستخدم التحكم في إخفاء:

المقاطعة الانتقالية
لنفترض أننا بدأنا في إغلاق بطاقتنا ، لكننا غيرنا عقولنا ونريد العودة. الأمر بسيط: في حالة .began
، نسمي pause()
للتوقف.
لكنك تحتاج إلى فصل سيناريوهين:
- عندما نبدأ بالاختباء من الإيماءة ؛
- عندما نقطع التيار.
للقيام بذلك ، بعد التوقف ، تحقق من percentComplete:
إذا كانت 0 ، percentComplete:
إغلاق البطاقة يدويًا ، بالإضافة إلى أننا نحتاج إلى الاتصال dismiss
. إذا لم يكن الرقم 0 ، فقد بدأ الاختفاء بالفعل ، يكفي إيقاف الحركة:
case .began: pause()
أضغط على الزر وانتقد على الفور لإلغاء إخفاء:

التوقف عن عرض وحدة تحكم
الوضع العكسي: بدأت البطاقة في الظهور ، لكننا لسنا بحاجة إليها. نحن قبض عليه وإرساله عن طريق السحب لأسفل الظهر. يمكنك مقاطعة الرسوم المتحركة لشاشة التحكم في نفس الخطوات.
إرجاع برنامج التشغيل كوحدة تحكم عرض تفاعلية:
func interactionControllerForPresentation(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { return driver }
عالج الإيماءة ، ولكن باستخدام قيم التحيز العكسي وقيم الاكتمال:
private func handlePresentation(recognizer r: UIPanGestureRecognizer) { switch r.state { case .began: pause() case .changed: let increment = -r.incrementToBottom(maxTranslation: maxTranslation) update(percentComplete + increment) case .ended, .cancelled: if r.isProjectedToDownHalf(maxTranslation: maxTranslation) { cancel() } else { finish() } case .failed: cancel() default: break } }
لفصل العرض والإخفاء ، قمت بإدخال التعداد باستخدام اتجاه الرسوم المتحركة الحالي:
enum TransitionDirection { case present, dismiss }
يتم تخزين الخاصية في TransitionDriver
وتؤثر على معالج الإيماءات الذي سيتم استخدامه:
var direction: TransitionDirection = .present @objc private func handle(recognizer r: UIPanGestureRecognizer) { switch direction { case .present: handlePresentation(recognizer: r) case .dismiss: handleDismiss(recognizer: r) } }
كما يؤثر wantsInteractiveStart
. لا نخطط لإظهار وحدة التحكم بإيماءة ، لذلك نرجع false
لـ .present
:
override var wantsInteractiveStart: Bool { get { switch direction { case .present: return false case .dismiss: let gestureIsActive = panRecognizer?.state == .began return gestureIsActive } } set { } }
حسنًا ، يبقى تغيير اتجاه الإيماءة عندما تظهر وحدة التحكم بالكامل. أفضل مكان في PresentationController
:
override func presentationTransitionDidEnd(_ completed: Bool) { super.presentationTransitionDidEnd(completed) if completed { driver.direction = .dismiss } }
هل من الممكن دون التعداد؟يبدو أننا يمكن أن نعتمد على خصائص وحدة التحكم isBeingPresented
و isBeingDismissed
. لكنهم يعرضون العملية فقط ، ونحتاج أيضًا إلى توجيهات ممكنة: في بداية الإغلاق التفاعلي ، ستكون كلتا القيمتين false
، وعلينا بالفعل أن نعرف أن هذا هو الاتجاه للإغلاق. يمكن حل هذه المشكلة بشروط إضافية للتحقق من التسلسل الهرمي لوحدات التحكم ، ولكن يبدو أن التعيين الصريح عبر enum
هو الحل الأبسط.
الآن يمكنك مقاطعة الرسوم المتحركة للعرض. أضغط على الزر وانتقد على الفور:

تظهر بواسطة لفتة
إذا كنت بصدد إعداد قائمة همبرغر لأحد التطبيقات ، فمن المرجح أنك ستريد عرضها عن طريق الإيماءة. يعمل هذا تمامًا مثل الإخفاء التفاعلي ، ولكن في لفتة ، بدلاً من dismiss
ندعو إلى present
.
لنبدأ من النهاية. في handlePresentation(recognizer:)
اعرض وحدة التحكم:
case .began: pause() let isRunning = percentComplete != 0 if !isRunning { presentingController?.present(presentedController!, animated: true) }
دعنا نظهر بشكل تفاعلي:
override var wantsInteractiveStart: Bool { get { switch direction { case .present: let gestureIsActive = screenEdgePanRecognizer?.state == .began return gestureIsActive case .dismiss: … }
لكي تعمل الشفرة ، لا توجد روابط كافية presentingController
. UIScreenEdgePanGestureRecognizer
عند إنشاء الإيماءة ، أضف UIScreenEdgePanGestureRecognizer
:
func linkPresentationGesture(to presentedController: UIViewController, presentingController: UIViewController) { self.presentedController = presentedController self.presentingController = presentingController
يمكنك نقل وحدات التحكم عند إنشاء PanelTransition
:
class PanelTransition: NSObject, UIViewControllerTransitioningDelegate { init(presented: UIViewController, presenting: UIViewController) { driver.linkPresentationGesture(to: presented, presentingController: presenting) } private let driver = TransitionDriver() }
يبقى لإنشاء PanelTransition
:
- لنقم بإنشاء وحدة تحكم
viewDidLoad
في viewDidLoad
، حيث أننا قد نحتاج إلى وحدة تحكم في أي وقت. - إنشاء
PanelTransition
. في مُنشئه ، تكون الإيماءة مرتبطة بوحدة التحكم. - اخماد المراحل الانتقالية لوحدة تحكم الطفل.
لأغراض التدريب ، انتقد من الأسفل ، لكن هذا يتعارض مع إغلاق التطبيق على iPhone X ومركز التحكم. يؤدي استخدام preferredScreenEdgesDeferringSystemGestures
ScreenEdgesDeferringSystemGestures إلى تعطيل انتقاد النظام من الأسفل.
class ParentViewController: UIViewController { private var child: ChildViewController! private var transition: PanelTransition! override func viewDidLoad() { super.viewDidLoad() child = ChildViewController()
بعد التغيير ، اتضح أنه كانت هناك مشكلة: بعد الإغلاق الأول للوحة ، تظل إلى الأبد في حالة TransitionDirection.dismiss
. قم بتعيين الحالة الصحيحة بعد إخفاء وحدة التحكم في PresentationController
:
override func dismissalTransitionDidEnd(_ completed: Bool) { super.dismissalTransitionDidEnd(completed) if completed { driver.direction = .present } }
يمكن عرض رمز العرض التفاعلي في موضوع منفصل . يبدو مثل هذا:

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