IOS الموقت

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

يستخدم الموقت لتخطيط الإجراءات في التطبيق. يمكن أن يكون هذا إجراء لمرة واحدة أو إجراء مكرر.

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

كموقع اختبار ، سوف نستخدم التطبيق - جدولة المهام البدائية.

الابتداء


قم بتنزيل المشروع المصدر. افتحه في Xcode ، وانظر إلى هيكله ، وتجميعه وتنفيذه. سترى أبسط جدولة المهام:



أضف مهمة جديدة إليها. اضغط على أيقونة + ، وأدخل اسم المهمة ، وانقر فوق موافق.

المهام المضافة لها طابع زمني. المهمة الجديدة التي قمت بإنشائها توصف بـ ثواني معدودة. كما ترون ، هذه القيمة لا تزيد.

يمكن تمييز كل مهمة على أنها مكتملة. اضغط على المهمة. سيتم شطب اسم المهمة وسيتم تمييزه على أنه مكتمل.

إنشاء أول توقيت لدينا


دعونا إنشاء الموقت الرئيسي لتطبيقنا. فئة Timer ، المعروفة أيضًا باسم NSTimer ، هي طريقة ملائمة لجدولة إجراء معين للحظة ، الفردية والدورية.

افتح TaskListViewController.swift وأضف هذا المتغير إلى TaskListViewController :

var timer: Timer? 

ثم أضف الامتداد هناك:

 // MARK: - Timer extension TaskListViewController { } 

والصق هذا الرمز في الملحق:

 @objc func updateTimer() { // 1 guard let visibleRowsIndexPaths = tableView.indexPathsForVisibleRows else { return } for indexPath in visibleRowsIndexPaths { // 2 if let cell = tableView.cellForRow(at: indexPath) as? TaskTableViewCell { cell.updateTime() } } } 

في هذه الطريقة ، نحن:

  1. تحقق مما إذا كانت هناك صفوف مرئية في جدول المهام.
  2. استدعاء updateTime لكل خلية مرئية. تعمل هذه الطريقة على تحديث الطابع الزمني في الخلية (انظر TaskTableViewCell.swift ).

ثم أضف هذا الرمز إلى الامتداد:

 func createTimer() { // 1 if timer == nil { // 2 timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(updateTimer), userInfo: nil, repeats: true) } } 

نحن هنا:

  1. تحقق مما إذا كان المؤقت يحتوي على مثيل لفئة المؤقت .
  2. إذا لم يكن الأمر كذلك ، قم بإنشاء جهاز توقيت يستدعي updateTimer () كل ثانية.

ثم نحتاج إلى إنشاء مؤقت بمجرد قيام المستخدم بإضافة المهمة الأولى. أضف createTimer () في بداية أسلوب presentAlertController (_ :) .

قم بتشغيل التطبيق وإنشاء اثنين من المهام الجديدة. سترى أن الطابع الزمني لكل مهمة يتغير كل ثانية.



إضافة التسامح الموقت


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

إضافة التسامح الموقت هو وسيلة سهلة لتقليل استهلاك الطاقة. يسمح هذا للنظام بإجراء إجراء مؤقت بين الوقت المخصص والوقت المخصص بالإضافة إلى وقت التسامح - ولكن ليس قبل الفاصل الزمني المحدد.

بالنسبة لأجهزة ضبط الوقت التي يتم تشغيلها مرة واحدة فقط ، يتم تجاهل قيمة التسامح.

في طريقة createTimer () ، مباشرة بعد تعيين الموقت ، أضف هذا السطر:

 timer?.tolerance = 0.1 

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



مؤقتات في الخلفية


ومن المثير للاهتمام ، ماذا يحدث لأجهزة ضبط الوقت عندما ينتقل أحد التطبيقات إلى الخلفية؟ للتعامل مع هذا ، أضف هذا الكود في بداية طريقة updateTimer () :

 if let fireDateDescription = timer?.fireDate.description { print(fireDateDescription) } 

سيسمح لنا ذلك بتتبع أحداث المؤقت في وحدة التحكم.

قم بتشغيل التطبيق ، أضف المهمة. الآن اضغط على زر الصفحة الرئيسية على جهازك ، ثم عد إلى تطبيقنا.

في وحدة التحكم ، سترى شيء مثل هذا:



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

فهم تشغيل الحلقات


حلقة التشغيل هي حلقة حدث تقوم بجدولة العمل وتتناول الأحداث الواردة. تعمل الدورة على إبقاء الخيط مشغولًا أثناء تشغيله ووضعه في حالة "نوم" عند عدم وجود عمل له.

في كل مرة تقوم فيها بتشغيل التطبيق ، يقوم النظام بإنشاء مؤشر الترابط الرئيسي للتطبيق ، ولكل مؤشر ترابط حلقة تنفيذ تم إنشاؤها تلقائيًا له.

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

هل لاحظت أن الطابع الزمني في الخلية لا يتم تحديثه عند سحب عرض الجدول؟



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

فهم تشغيل أوضاع الدورة


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

هناك ثلاثة أوضاع وقت تشغيل في نظام iOS:

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

بالنسبة لتطبيقنا ، فإن الوضع الأنسب شائع . لاستخدامها ، استبدل محتويات طريقة createTimer () بما يلي:

 if timer == nil { let timer = Timer(timeInterval: 1.0, target: self, selector: #selector(updateTimer), userInfo: nil, repeats: true) RunLoop.current.add(timer, forMode: .common) timer.tolerance = 0.1 self.timer = timer } 

الفرق الرئيسي من الكود السابق هو أنه قبل تعيين المؤقت إلى TaskListViewController ، نضيف هذا المؤقت إلى حلقة التشغيل في الوضع الشائع .

ترجمة وتشغيل التطبيق.



الآن يتم تحديث الطوابع الزمنية للخلايا حتى لو تم تمرير الجدول.

إضافة الرسوم المتحركة لإكمال جميع المهام


نضيف الآن رسمًا تهنئةًا للمستخدم لإكمال جميع المهام - سترتفع الكرة من أسفل الشاشة إلى الأعلى.

أضف هذه المتغيرات في بداية TaskListViewController:

 // 1 var animationTimer: Timer? // 2 var startTime: TimeInterval?, endTime: TimeInterval? // 3 let animationDuration = 3.0 // 4 var height: CGFloat = 0 

الغرض من هذه المتغيرات هو:

  1. تخزين مؤقت للرسوم المتحركة.
  2. تخزين بداية ونهاية وقت الرسوم المتحركة
  3. مدة الرسوم المتحركة.
  4. ارتفاع الرسوم المتحركة.

أضف الآن ملحق TaskListViewController التالي في نهاية ملف TaskListViewController.swift :

 // MARK: - Animation extension TaskListViewController { func showCongratulationAnimation() { // 1 height = UIScreen.main.bounds.height + balloon.frame.size.height // 2 balloon.center = CGPoint(x: UIScreen.main.bounds.width / 2, y: height + balloon.frame.size.height / 2) balloon.isHidden = false // 3 startTime = Date().timeIntervalSince1970 endTime = animationDuration + startTime! // 4 animationTimer = Timer.scheduledTimer(withTimeInterval: 1 / 60, repeats: true) { timer in // TODO: Animation here } } } 

هنا نقوم بما يلي:

  • حساب ارتفاع الرسوم المتحركة ، والحصول على ارتفاع شاشة الجهاز
  • توسيط الكرة خارج الشاشة وتعيين رؤيتها
  • تعيين بداية ونهاية الوقت للرسوم المتحركة
  • نبدأ الموقت الرسوم المتحركة وتحديث الرسوم المتحركة 60 مرة في الثانية الواحدة

الآن نحن بحاجة إلى إنشاء المنطق الفعلي لتحديث الرسوم المتحركة التهنئة. أضف هذا الكود بعد showCongratulationAnimation () :

 func updateAnimation() { // 1 guard let endTime = endTime, let startTime = startTime else { return } // 2 let now = Date().timeIntervalSince1970 // 3 if now >= endTime { animationTimer?.invalidate() balloon.isHidden = true } // 4 let percentage = (now - startTime) * 100 / animationDuration let y = height - ((height + balloon.frame.height / 2) / 100 * CGFloat(percentage)) // 5 balloon.center = CGPoint(x: balloon.center.x + CGFloat.random(in: -0.5...0.5), y: y) } 

ماذا نفعل:

  1. تحقق من أنه تم تعيين endTime و startTime
  2. توفير الوقت الحالي في ثابت
  3. نتأكد من أن الوقت النهائي لم يصل بعد. إذا وصل بالفعل ، قم بتحديث المؤقت وإخفاء الكرة لدينا
  4. احسب y إحداثي جديد للكرة
  5. يتم حساب الموضع الأفقي للكرة بالنسبة للموضع السابق

استبدل الآن // TODO: الرسوم المتحركة هنا في showCongratulationAnimation () بهذا الكود:

 self.updateAnimation() 

الآن يتم استدعاء updateAnimation () كلما حدث حدث مؤقت.

الصيحة ، أنشأنا للتو الرسوم المتحركة. ومع ذلك ، عند بدء تشغيل التطبيق ، لا يحدث شيء جديد ...

عرض الرسوم المتحركة


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

 func showCongratulationsIfNeeded() { if taskList.filter({ !$0.completed }).count == 0 { showCongratulationAnimation() } } 

سوف ندعو هذه الطريقة كلما قام المستخدم بتحديد المهمة المكتملة ، فإنه يتحقق مما إذا كانت جميع المهام مكتملة. إذا كان الأمر كذلك ، فسيتم استدعاء showCongratulationAnimation () .

في الختام ، أضف مكالمة إلى هذه الطريقة في نهاية tableView (_: didSelectRowAt :) :

 showCongratulationsIfNeeded() 

قم بتشغيل التطبيق ، وإنشاء اثنين من المهام ، ووضع علامة عليها مكتملة - وسترى الرسوم المتحركة لدينا!



نوقف الموقت


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

أولاً ، قم بإنشاء طريقة جديدة لإيقاف المؤقت:

 func cancelTimer() { timer?.invalidate() timer = nil } 

سيؤدي هذا إلى تحديث المؤقت وإعادة تعيينه إلى الصفر حتى نتمكن من إنشائه بشكل صحيح لاحقًا. invalidate () هي الطريقة الوحيدة لإزالة Timer من حلقة التشغيل. ستعمل حلقة التشغيل على إزالة مرجع المؤقت القوي إما مباشرة بعد استدعاء invalidate () أو بعد ذلك بقليل.

استبدل الآن طريقة showCongratulationsIfNeeded () كما يلي:

 func showCongratulationsIfNeeded() { if taskList.filter({ !$0.completed }).count == 0 { cancelTimer() showCongratulationAnimation() } else { createTimer() } } 

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

قم بتشغيل التطبيق.



الآن توقف الموقت وإعادة تشغيل كما يجب.

CADisplayLink للرسوم المتحركة على نحو سلس


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

وضعنا الموقت إلى 60HZ. وبالتالي ، يقوم الموقت بتحديث الرسوم المتحركة كل 16 مللي ثانية. النظر في الموقف عن كثب:



عند استخدام Timer ، لا نعرف الوقت المحدد لبدء الإجراء. هذا يمكن أن يحدث إما في بداية أو في نهاية الإطار. لنفترض أن المؤقت يعمل في منتصف كل إطار (النقاط الزرقاء في الصورة). الشيء الوحيد الذي نعرفه بالتأكيد هو أن المكالمة ستكون كل 16 مللي ثانية.

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

سوف CADisplayLink مساعدتنا


يتم استدعاء CADisplayLink مرة واحدة لكل إطار ويحاول مزامنة إطارات الرسوم المتحركة الحقيقية قدر الإمكان. سيكون لديك الآن 16 مللي ثانية تحت تصرفك ولن تسقط iOS إطارًا واحدًا.

لاستخدام CADisplayLink ، تحتاج إلى استبدال animationTimer بنوع جديد.

استبدال هذا الرمز

 var animationTimer: Timer? 

على هذا واحد:

 var displayLink: CADisplayLink? 

لقد استبدلت Timer بـ CADisplayLink . CADisplayLink هو عرض مؤقت مرتبط بالمسح الرأسي للعرض. هذا يعني أن وحدة معالجة الرسومات (GPU) الخاصة بالجهاز ستتوقف مؤقتًا حتى تتمكن الشاشة من متابعة معالجة أوامر GPU. بهذه الطريقة نحصل على الرسوم المتحركة على نحو سلس.

استبدال هذا الرمز

 var startTime: TimeInterval?, endTime: TimeInterval? 

على هذا واحد:

 var startTime: CFTimeInterval?, endTime: CFTimeInterval? 


لقد استبدلت TimeInterval بـ CFTimeInterval ، وهو أمر ضروري للعمل مع CADisplayLink.

استبدل نص showCongratulationAnimation () بالطريقة التالية:

 func showCongratulationAnimation() { // 1 height = UIScreen.main.bounds.height + balloon.frame.size.height balloon.center = CGPoint(x: UIScreen.main.bounds.width / 2, y: height + balloon.frame.size.height / 2) balloon.isHidden = false // 2 startTime = CACurrentMediaTime() endTime = animationDuration + startTime! // 3 displayLink = CADisplayLink(target: self, selector: #selector(updateAnimation)) displayLink?.add(to: RunLoop.main, forMode: .common) } 

ماذا نفعل هنا:

  1. اضبط ارتفاع الرسوم المتحركة وإحداثيات الكرة ووضوح الرؤية - كما كان يحدث من قبل.
  2. تهيئة startTime باستخدام CACurrentMediaTime () (بدلاً من Date ()).
  3. نقوم بإنشاء مثيل لفئة CADisplayLink وإضافته إلى حلقة التشغيل في الوضع الشائع .

استبدل الآن updateAnimation () بالكود التالي:

 // 1 @objc func updateAnimation() { guard let endTime = endTime, let startTime = startTime else { return } // 2 let now = CACurrentMediaTime() if now >= endTime { // 3 displayLink?.isPaused = true displayLink?.invalidate() balloon.isHidden = true } let percentage = (now - startTime) * 100 / animationDuration let y = height - ((height + balloon.frame.height / 2) / 100 * CGFloat(percentage)) balloon.center = CGPoint(x: balloon.center.x + CGFloat.random(in: -0.5...0.5), y: y) } 

  1. إضافة objc إلى توقيع الأسلوب (بالنسبة إلى CADisplayLink ، تتطلب المعلمة المحددة مثل هذا التوقيع).
  2. استبدال التهيئة بـ Date () لتهيئة تاريخ CoreAnimation .
  3. استبدال المكالمة animationTimer.invalidate () بإيقاف CADisplayLink وإلغاء الصلاحية. سيؤدي هذا أيضًا إلى إزالة CADisplayLink من حلقة التشغيل.

قم بتشغيل التطبيق!


عظيم! استبدلنا الرسوم المتحركة المستندة إلى Timer بنجاح بـ CADisplayLink الأكثر ملاءمة - وحصلنا على الرسوم المتحركة بسلاسة ، دون الرجيج.

استنتاج


في هذا الدليل ، تعرفت على كيفية عمل فئة Timer في iOS ، وما هي دورة التنفيذ وكيف يمكن أن تجعل تطبيقك أكثر استجابة من حيث الواجهة ، وكيفية استخدام CADisplayLink بدلاً من Timer للحصول على رسوم متحركة سلسة.

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


All Articles