تحسين تجربة المستخدم في التعامل مع الـ TableView


أهلاً  ومرحباً بكم في مدونتي الجديدة =)
مدونتي السابقة كانت g-kn.com تم إغلاقها للانتقال لهذه المنصة الجميلة ،مقالاتي السابقة تجدونها في موقع عالم البرمجة على هذا الرابط

 

 

كمطور تطبيقات جوال فإنك سوف تعتمد اعتماد كبير على TableView

تقريبا لا يخلوا تطبيق جوال من الـ TableView

أيضا جميع المستخدمين النظام معتادين على التعامل مع الـ TableView

فكيف يمكنك تحسينه وإضافة قيمة مضافة تسهل حياة المستخدم وتحببه لتطبيقك ؟

 

في حال كنت تعرض محتوى كبير في الـ TableView

كمثال تطبيقك عباره عن شبكة اجتماعية ، قارئ RSS ، تطبيق ايميل

او أي تطبيق مشابه بحيث تعرض محتوى كبير

فهذا الموضوع سوف يفيدك =)

 

بداية دعني أوضح الفكرة

الفكرة الذي نريد عملها عبارة عن خطوتين


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

 

الخطوة الثانية : تجنب نسبة الخطأ الذي قد يرتكبها المستخدم عند استخدام تطبيقك ماذا اقصد ؟

من الأمور المعروفة لذا مستخدمين الـ iOS ، هي يمكنهم الضغط على شريط الساعة للوصول لأول Cell في الـ TableView ، وبالتالي الامر سوف يكون مزعجاً عند حدوث هذا الامر عن طريق الخطأ !

لذا سوف نقوم بحل هذه المشكلة ! ، كيف ؟ سوف نجعل المستخدم يعد الى نفس الـ Cell عند الضغط على شريط الساعة مره أخرى ! ، بما يعني اول مره سوف ينتقل الى أول Cell لكن الضغطة الثانية سوف تعيده الى نفس الـ Cell التي كان متوقف عندها ! بالضبط كما يفعل تطبيق TweetBot


الشرح :

في البداية قم بعمل مشروع جديد واضيف الـ TableView وقم بعمل اللازم من توصيله بالـ delegate والـ dataSource وأيضا لا تنسى تعطي للـ Cell معرف (identity) في مثالي اعطيته معرف dataCell

 

الكود سوف يكون بالشكل الحالي :

import UIKit class ViewController: UIViewController { @IBOutlet weak var tableView: UITableView! var arrayData = [String]() override func viewDidLoad() { super.viewDidLoad() setUpData() } func setUpData(){ for n in 0...200 { arrayData.append("Cell Number: \(n)") } } } extension ViewController: UITableViewDataSource { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return arrayData.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "dataCell", for: indexPath) cell.textLabel?.text = arrayData[indexPath.row] return cell } }

كود جدول أساسي يعرض ٢٠٠ خلية في الجدول

شرح الخطوة الأول :

في هذه الخطوة كما ذكرنا سابقاً نحتاج الى حفظ اخر Cell وصل اليه المستخدم
ومن ثم عند فتح التطبيق مره اخرى سوف نقوم بعرض الجدول على نفس الـ
Cell

اسهل طريقة لحفظ بيانات بسيطه هو استخدام UserDefaults


لكن لحظة الـ UserDefaults لا يسمح بالحفظ الا بيانات معينه فقط وهذه انواعها
integer, bool, float, double ،فكيف سوف نحفظ الـ indexPath ؟

الإجابة هي لا نحتاج الى حفظ الـ indexPath كما هو ولكن فقط نحتاج نحفظ رقم الـ Row لماذا ؟ في مثالنا السابق لا يوجد غير section واحد فقط بما يعني أنه لن يتغير وفقط قيمة الـ Row سوف تتغير لذا اسهل طريقة سوف تكون حفظ قيمة الـ Row ومن ثم إعادة إنشاء الـ indexPath عندما نحتاجه. 

تستنتج بما سبق بأنك سوف تحتاج الى حفظ رقم الـ section ايضا في حال كان الـ Tableview الذي تستخدمه يحتوي على عدة sections


على أي حال عند استخدامي للـ UserDefaults فإني افضل عمل struct له ليسهل الوصول له في اي مكان من التطبيق وبدون الحاجة لإعادة كتابة نفس الأكواد عدة مرات، لذا تستطيع كتابة الكود التالي اما في ملف swift جديد أو خارج class ViewContorller

struct UserDefaultsModel { private static let scrollingIndexRowKey = "scrollingIndexRowKey" static var getScrollingIndexRow = { UserDefaults.standard.value(forKey: scrollingIndexRowKey) as? Int } static var setScrollingIndexRow = { (scrollingIndexRow: Int) in UserDefaults.standard.set(scrollingIndexRow, forKey: scrollingIndexRowKey) } }

الكود التالي عباره عن متغيرين الاول get والاخير set وضيفتهم القراءة او الحفظ في UserDefauts

الان سوف نحتاج الي كتابة كود معين يقول بحفظ اول Cell ظاهر في الـ TableView 

func helperScrollEnd(){ let visibleCells = tableView.visibleCells if let firstCell = visibleCells.first { if let indexPath = tableView.indexPath(for: firstCell) { UserDefaultsModel.setScrollingIndexRow(indexPath.row) print("lastScrollingIndex = \(indexPath)") } } }

في هذا الكود جلبنا الـ  cells الظاهره في الجدول ومن ثم اخترنا اول Cell واستخرجنا منها الـ IndexPath بعدها حفظنا الـ Row في الـ UserDefaults وطبعنا الـ indexPath في الـ output

اين سوف نستخدم هذا الـ Function ؟ ، سوف نسخدمه ضمن ميتودين من ميتودات الـ Tableview delegate


الميتودين هيا scrollViewDidEndDecelerating 

و scrollViewDidEndDragging


scrollViewDidEndDecelerating : يعمل عندما ينتقل المستخدم بشكل سريع ومن ثم يتوقف

scrollViewDidEndDragging : يعمل عندما ينتقل المستخدم بين الـ Cells بشكل طبيعي


وبما إننا لا نستطيع توقع المستخدم وطريقة استخدامه وتصفحه للـCells فالافضل استدعاء الـ helperScrollEnd في الميتودين 


وعموما دائما افضل فصل الـ delegate والـ dataSource في extension

منفصله عن بعض  بالطريقة دي الكود يكون ارتب واسهل في الوصول للكود المطلوب من اني أدمجهم مع بعض.


اضيف الكود التالي خارج الكلاس  

extension ViewController:UITableViewDelegate{ func helperScrollEnd(){ let visibleCells = tableView.visibleCells if let firstCell = visibleCells.first { if let indexPath = tableView.indexPath(for: firstCell) { UserDefaultsModel.setScrollingIndexRow(indexPath.row) print("lastScrollingIndex = \(indexPath)") } } } func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { helperScrollEnd() } func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { helperScrollEnd() } }

ضمن الـ extenstion وضعت الـ helperScrollEnd بما انه له علاقه ويستخدم هنا ومن ثم استعيت المتودين الذين تحدث عنهم سابقاً

باقي جزيئة واحده فقط ، الان حفظنا موقع اخر Cell ، نحتاج عند فتح التطبيق بأن نظهر الجدول على الـ Cell المحفوظ بدل من ظهور الجدول على اول Cell


فنحتاج نضيف Function جديد وهذه المره سوف نضيفه في نفس الـ class

func returnBackToLastPosition(_ animated:Bool){ if let indexPathRow = UserDefaultsModel.getScrollingIndexRow() { let indexPath = IndexPath(row: indexPathRow, section: 0) tableView.scrollToRow(at: indexPath, at: .top, animated: animated) } }

هنا جلبنا الـ Row من الـ UserDefault ومن ثم انشأنا الـ indexPath اعتمادا عليه ، وكما ذكرت سابقا في حالتنا هنا الـ section لن يتغير لذلك اعطيناه قيمة 0 واخر شي طلبنا انه ينتقل الى هذا الـ Row اعتمادا على الـ indexPath الذين انشأناه 

اذا لاحظت الـ function السابق اضفنا له parameter بإسم animated الغرض منه لكي نستيطع التحكم في اضافة aniamtion من عدمه ، عند فتح التطبيق نريد أن نظهر الجدول على اخر Cell ولكن بدون animation لكن في الخطوة الثانية عند الضغط على شريط الساعة نريد أن ننتقل الى الـ Cell ولكن مع animation لهذا السبب اضفنا هذا الـ parameter


سوف نستدعي هذا الـ function في viewDidApper وليس في viewDidLoad

override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) returnBackToLastPosition(false) }

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


شرح الخطوة الثانية : 

في الخطوة الثانية نحتاج نضيف متغير جديد من نوع Bool

var didScrollingToTop:Bool = false

المتغير السابق سوف نستفيد منه في معرفة هل تم الانتقال للأعلى ام لا

وأيضا سوف نعود

الى الـ extension ViewController:UITableViewDelegate

ونضيف function جديدة من ضمن الـ Functions المتوفره ضمن الـ delegate

اسمها scrollViewShouldScrollToTop وكما هو واضح من الاسم فإنها مسؤوله عن تفعيل او إغلاق ميزة الانتقال الى بداية الـ TableView
بشكل إفتراضي ترجع قيمة
true اذا جعلتها false فإنك سوف تعطل ميزة الانتقال الى بداية الـ TableView عند الضغط على شريط الساعة او كما يسمى Status Bar ونحن سوف نستفيد من الـ function هذه لصالحنا =)

func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool { if didScrollingToTop{ returnBackToLastPosition(true) didScrollingToTop = false return false }else if didScrollingToTop == false { didScrollingToTop = true return true } return true }

الكود السابق بداية سوف نتأكد من قيمة المتغير الذي اضفناه سابقا didScrollingToTop اذا كان قيمة false فإننا سوف نسمح بالانتقال الى بداية الـ TableView زمن ثم سوف نغير قيمة الـ didScrollingToTop الى true لأننا سوف نصبح في بداية الـ Tableview.

لكن في حالة كان الـ didScrollingToTop بقيمة true فمعناه اننا نريد العودة الى الـ Cell الذي كنا متوقفين عنده عند الضغط على شريط الساعة مره اخرى
لذلك هذه المره سوف نرجع قيمة
false لكي نتمكن من استدعاء 

returnBackToLastPosition(true) وايضا سوف نعيد قيمة الـ didScrollingToTop الى false 


بكذا نكون انتيهنا قم بتشغيل التطبيق ومن ثم اسحب للاسفل ومن ثم اضغط على شريط الساعه سوف ينتقل مباشرة الى بداية الـ TableView الان قم بالضغط على شريط الساعه مره اخرى وسوف يعود تلقائيا الى الـ Cell الذي كنت متوقف عندها سابقا ! كما في الصورة التالية :

Join