لا تنبثق! انتقالات المقاطعة في iOS

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



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


لقد 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 } 

في الوقت نفسه ، يجب أن تشير إلى أن إخفاء أصبح قابلاً للإدارة (لقد فعلنا هذا بالفعل في المقالة الأخيرة):


 // PanelTransition.swift func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { return driver } 

التعامل مع لفتة


لنبدأ بالإيماءة الختامية: إذا قمت بسحب اللوحة لأسفل ، ستبدأ الرسوم المتحركة للإغلاق ، وستؤثر حركة الإصبع على درجة الإغلاق.
يسمح UIPercentDrivenInteractiveTransition بالتقاط الرسوم المتحركة الانتقالية والتحكم فيها يدويًا. فقد update ، finish ، cancel الأساليب. أنها مريحة للقيام معالجة الإيماءات في فئتها الفرعية.


معالجة الإيماءات


 private func handleDismiss(recognizer r: UIPanGestureRecognizer) { switch r.state { case .began: pause() //   percentComplete   0 let isRunning = percentComplete != 0 if !isRunning { presentingController?.present(presentedController!, animated: true) } case .changed: update(percentComplete + r.incrementToBottom(maxTranslation: maxTranslation)) case .ended, .cancelled: if r.isProjectedToDownHalf(maxTranslation: maxTranslation) { finish() } else { cancel() } case .failed: cancel() default: break } } 

.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 { // Velocity value func projectedOffset(decelerationRate: UIScrollView.DecelerationRate) -> CGFloat { // Magic formula from WWDC let multiplier = 1 / (1 - decelerationRate.rawValue) / 1000 return self * multiplier } } extension CGPoint { static func +(left: CGPoint, right: CGPoint) -> CGPoint { return CGPoint(x: left.x + right.x, y: left.y + right.y) } } 

- سيحدث - إذا قمت بقفل شاشة الهاتف أو إذا اتصلوا. يمكنك التعامل معها ككتلة .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() // Pause allows to detect percentComplete if percentComplete == 0 { presentedController?.dismiss(animated: true) } 

أضغط على الزر وانتقد على الفور لإلغاء إخفاء:


التوقف عن عرض وحدة تحكم


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


إرجاع برنامج التشغيل كوحدة تحكم عرض تفاعلية:


 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 //    panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handle(recognizer:))) presentedController.view.addGestureRecognizer(panRecognizer!) //    screenEdgePanRecognizer = UIScreenEdgePanGestureRecognizer(target: self, action: #selector(handlePresentation(recognizer:))) screenEdgePanRecognizer!.edges = .bottom presentingController.view.addGestureRecognizer(screenEdgePanRecognizer!) } 

يمكنك نقل وحدات التحكم عند إنشاء PanelTransition :


 class PanelTransition: NSObject, UIViewControllerTransitioningDelegate { init(presented: UIViewController, presenting: UIViewController) { driver.linkPresentationGesture(to: presented, presentingController: presenting) } private let driver = TransitionDriver() } 

يبقى لإنشاء PanelTransition :


  1. لنقم بإنشاء وحدة تحكم viewDidLoad في viewDidLoad ، حيث أننا قد نحتاج إلى وحدة تحكم في أي وقت.
  2. إنشاء PanelTransition . في مُنشئه ، تكون الإيماءة مرتبطة بوحدة التحكم.
  3. اخماد المراحل الانتقالية لوحدة تحكم الطفل.
  4. لأغراض التدريب ، انتقد من الأسفل ، لكن هذا يتعارض مع إغلاق التطبيق على iPhone X ومركز التحكم. يؤدي استخدام preferredScreenEdgesDeferringSystemGestures ScreenEdgesDeferringSystemGestures إلى تعطيل انتقاد النظام من الأسفل.


     class ParentViewController: UIViewController { private var child: ChildViewController! private var transition: PanelTransition! override func viewDidLoad() { super.viewDidLoad() child = ChildViewController() // 1 transition = PanelTransition(presented: child, presenting: self) // 2 // Setup the child child.modalPresentationStyle = .custom child.transitioningDelegate = transition // 3 } override var preferredScreenEdgesDeferringSystemGestures: UIRectEdge { return .bottom // 4 } } 

    بعد التغيير ، اتضح أنه كانت هناك مشكلة: بعد الإغلاق الأول للوحة ، تظل إلى الأبد في حالة TransitionDirection.dismiss . قم بتعيين الحالة الصحيحة بعد إخفاء وحدة التحكم في PresentationController :


     override func dismissalTransitionDidEnd(_ completed: Bool) { super.dismissalTransitionDidEnd(completed) if completed { driver.direction = .present } } 

    يمكن عرض رمز العرض التفاعلي في موضوع منفصل . يبدو مثل هذا:




استنتاج


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


مثال يمكن رؤيته على جيثب.


اشترك في قناة دودو بيتزا موبايل.

Source: https://habr.com/ru/post/ar465073/


All Articles