تعلم Swift من الصفر: التحكم بالولوج و مراقبي الخواص |
في الدرس السابق، قمنا بإضافة القدرة على إنشاء مواد to-do. بينما قامت هذه الإضافة بجعل التطبيق أكثر فائدة، فإنه سيكون من الأفضل إضافة القدرة على تعيين المواد التي انتهت وعلى حذف المواد. هذا ما سنركز عليه هذا الدرس.
الشروط
إذا كنت ترغب بالمتابعة معي، فعليك أن تتأكد من أن Xcode التي تملكها هي 6.3 أو أعلى منصبة على الآلة. خلال وقت الكتابة، تكتب Xcode 6.3 في beta ومتاحة في Apple's iOS Dev Center للمطورين المسجلين في iOS.
السبب من وراء طلب Xcode 6.3 أو أعلى هو الحصول على استفادة من Swift 1.2، الذي قامت Apple بإنتاجه منذ عدة أيام. يقدم Swift 1.2 عدداً من الإضافات العظيمة التي ستستفيد منها إلى آخر هذه السلسلة.
1. حذف المواد
لنحذف المواد، سنحتاج إلى إنشاء طريقتان إضافيتان من بروتوكول UITableViewDataSource. سنحتاج أولاً إلى أن نخبر لائحة المشهد أي صف يمكن أن يتم تحريره من خلال إنشاء طريقة tableView(_:canEditRowAtIndexPath:). كما يمكنك أن ترى في مقطع الكود بالأسفل، فإن الإنشاء واضح جداً. سنقوم بإخبار لائحة المشهد أن كل صف يمكن تحريره من خلال الإرجاع true.
السبب من وراء طلب Xcode 6.3 أو أعلى هو الحصول على استفادة من Swift 1.2، الذي قامت Apple بإنتاجه منذ عدة أيام. يقدم Swift 1.2 عدداً من الإضافات العظيمة التي ستستفيد منها إلى آخر هذه السلسلة.
1. حذف المواد
لنحذف المواد، سنحتاج إلى إنشاء طريقتان إضافيتان من بروتوكول UITableViewDataSource. سنحتاج أولاً إلى أن نخبر لائحة المشهد أي صف يمكن أن يتم تحريره من خلال إنشاء طريقة tableView(_:canEditRowAtIndexPath:). كما يمكنك أن ترى في مقطع الكود بالأسفل، فإن الإنشاء واضح جداً. سنقوم بإخبار لائحة المشهد أن كل صف يمكن تحريره من خلال الإرجاع true.
func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
return true
}
الطريقة الثانية التي نهتم بها هي tableView(_:commitEditingStyle:forRowAtIndexPath:). ولكن إنشاء هذه أكثر تعقيداً ومع ذلك يمكن فهمها بسهولة.
func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
if editingStyle == .Delete {
// Fetch Item
let item = self.items[indexPath.row]
// Update Items
self.items.removeAtIndex(indexPath.row)
// Update Table View
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Right)
}
}
نبدأ بالتحقق من قيمة editingStyle، متعددة من نوع UITableViewCellEditingStyle. يمكننا أن نحذف مادة فقط في حال كانت قيمة editingStyle تساوي UITableViewCellEditingStyle.Delete.
ومع ذلك فإن Swift أذكى من ذلك. لأنه يعرف أن editingStyle من نوع UITableViewCellEditingStyle، فإنه يمكننا حذف UITableViewCellEditingStyle، اسم المتعددة، ونكتب. Delete (حذف)، قيمة عضو المتعددة التي نحن مهتمين بها. إذا كنت جديداً على المتعددات في Swift، فعليك قراءة درس سريعة نصيحة عن المتعددات في Swift.
في الخطوة التالية، سنجلب المادة الموافقة من خصائص المواد ونقوم بتخزين قيمها في ثابت يدعى item. سنقوم بتحديث مصدر لائحة المشهد، items (المواد)، من خلال استدعاء removeAtIndex(index: Int لخصائص المواد، ممررين بذلك الرقم التسلسلي الصحيح.
أخيراً، نقوم بتحديث لائحة المشهد من خلال استدعاء deleteRowsAtIndexPaths(_:withRowAnimation:) لـ tableView، ممررين بذلك مصفوفة indexPath و .Right من أجل تحديد نوع الإنعاش. كما لاحظنا مسبقاً، يمكننا حذف اسم المتعددة، UITableViewRowAnimation، بما أن Swift يعرف أن نوع المعاملة الثانية هو UITableViewRowAnimation.
يمكن للمستخدم الآن أن يقوم بحذف المواد من القائمة. قم ببناء وإدارة التطبيق لفحص هذا.
2. التحقق من Off Items
من أجل تعيين مادة كمنتهية، سنتقوم بإضافة علامة للصف الموافق. هذا يشير إلى أنه يتوجب علينا أن نقتفي أثر المواد التي عينها المستخدم كمنتهية. من أجل ذلك الهدف، سنعلن عن خاصية جديدة ستتولى ذلك. قم بالإعلان عن خاصية متغيرة، checkedItems، من نوع [String] وابدأها بمصفوفة فارغة.
ومع ذلك فإن Swift أذكى من ذلك. لأنه يعرف أن editingStyle من نوع UITableViewCellEditingStyle، فإنه يمكننا حذف UITableViewCellEditingStyle، اسم المتعددة، ونكتب. Delete (حذف)، قيمة عضو المتعددة التي نحن مهتمين بها. إذا كنت جديداً على المتعددات في Swift، فعليك قراءة درس سريعة نصيحة عن المتعددات في Swift.
في الخطوة التالية، سنجلب المادة الموافقة من خصائص المواد ونقوم بتخزين قيمها في ثابت يدعى item. سنقوم بتحديث مصدر لائحة المشهد، items (المواد)، من خلال استدعاء removeAtIndex(index: Int لخصائص المواد، ممررين بذلك الرقم التسلسلي الصحيح.
أخيراً، نقوم بتحديث لائحة المشهد من خلال استدعاء deleteRowsAtIndexPaths(_:withRowAnimation:) لـ tableView، ممررين بذلك مصفوفة indexPath و .Right من أجل تحديد نوع الإنعاش. كما لاحظنا مسبقاً، يمكننا حذف اسم المتعددة، UITableViewRowAnimation، بما أن Swift يعرف أن نوع المعاملة الثانية هو UITableViewRowAnimation.
يمكن للمستخدم الآن أن يقوم بحذف المواد من القائمة. قم ببناء وإدارة التطبيق لفحص هذا.
2. التحقق من Off Items
من أجل تعيين مادة كمنتهية، سنتقوم بإضافة علامة للصف الموافق. هذا يشير إلى أنه يتوجب علينا أن نقتفي أثر المواد التي عينها المستخدم كمنتهية. من أجل ذلك الهدف، سنعلن عن خاصية جديدة ستتولى ذلك. قم بالإعلان عن خاصية متغيرة، checkedItems، من نوع [String] وابدأها بمصفوفة فارغة.
var checkedItems: [String] = []
في tableView(_:cellForRowAtIndexPath:)، سنتحقق فيما إذا checkedItems ستحد المادة المختصة من خلال استخدام دالة contains، دالة عالمية معرفة في مكتبة Swift. سنمرر checkedItems على أنها المعاملة الأولى و item على أنها المعاملة الثانية. ستعود الدالة true إذا قامت checkedItems بالحد من item.
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
// Fetch Item
let item = self.items[indexPath.row]
// Dequeue Table View Cell
let tableViewCell = tableView.dequeueReusableCellWithIdentifier("TableViewCell", forIndexPath: indexPath) as! UITableViewCell
// Configure Table View Cell
tableViewCell.textLabel?.text = item
if contains(self.checkedItems, item) {
tableViewCell.accessoryType = .Checkmark
} else {
tableViewCell.accessoryType = .None
}
return tableViewCell
}
إذا وجدت item في checkedItems، سنعين خاصية الخلية accessoryType لـ .Checkmark، قيمة عضو متعددة UITableViewCellAccessoryType. إذا لم يتم إيجاد item، سنرجع إلى .None ونعينها كنوع اكسسوار الخلية.
في الخطوة التالية، يجب علينا إضافة القدرة على تعيين علامة على المادة كمنتهية من خلال إنشاء طريقة بروتوكول UITableViewDelegate .tableView(_:didSelectRowAtIndexPath:). في هذه الطريقة المفوضة، سنقوم أولاً باستدعاء deselectRowAtIndexPath(_:animated:) لـ tableView من أجل إزالة الصف الذي قام المستخدم بالنقر عليه.
// MARK: Table View Delegate Methods
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
tableView.deselectRowAtIndexPath(indexPath, animated: true)
// Fetch Item
let item = self.items[indexPath.row]
// Fetch Table View Cell
let tableViewCell = tableView.cellForRowAtIndexPath(indexPath)
// Find Index of Item
let index = find(self.checkedItems, item)
if let index = index {
self.checkedItems.removeAtIndex(index)
tableViewCell?.accessoryType = UITableViewCellAccessoryType.None
} else {
self.checkedItems.append(item)
tableViewCell?.accessoryType = UITableViewCellAccessoryType.Checkmark
}
}
ثم نقوم بعد ذلك بإحضار المادة الموافقة من items والإرجاع للخلية التي تتوافق مع الصف المنقور عليه. سنستخدم الدالة find، المعرفة في مكتبة Swift، من أجل الحصول على الرقم التسلسلي للمادة في checkedItems. تعود الدالة find باختياري Int. إن احتوى checkedItems على مادة، سنقوم بإزالتها من checkedItems وسنعين نوع اكسسوار الخلية .None في حال لم يحتوي checkedItems على مادة، سنضيفها إلى checkedItems ونعين نوع اكسسوار الخلية .Checkmark.
مع هذه الإضافات، أصبح المستخدم الآن قادراً أن يعطي المواد إشارة أنها منتهية. قم ببناء وإدارة التطبيق لكي تتأكد أن كل شيء يعمل كما هو متوقع.
3. حفظ الحالة
لا يقوم التطبيق حالياً بحفظ الحالة بين الإطلاقات. لحل هذا الأمر، سنقوم بتخزين مصفوفات items و checkedItems في قاعدة البيانات الافتراضية للتطبيق الذي يستخدمه المستخدم.
الخطوة 1: تحميل الحالة
ابدأ من خلال إنشاء طريقتين مساعدتين loadItems و loadCheckedItems. لاحظ أن الكلمة الأساسية private (خاص) تسبق كل طريقة مساعدة. إن الكلمة الأساسية private تخبر Swift أن هذه الطرق يمكن الولوج إليها فقط من داخل ملف المصدر هذا.
الخطوة 1: تحميل الحالة
ابدأ من خلال إنشاء طريقتين مساعدتين loadItems و loadCheckedItems. لاحظ أن الكلمة الأساسية private (خاص) تسبق كل طريقة مساعدة. إن الكلمة الأساسية private تخبر Swift أن هذه الطرق يمكن الولوج إليها فقط من داخل ملف المصدر هذا.
// MARK: Private Helpers
private func loadItems() {
let userDefaults = NSUserDefaults.standardUserDefaults()
if let items = userDefaults.objectForKey("items") as? [String] {
self.items = items
}
}
private func loadCheckedItems() {
let userDefaults = NSUserDefaults.standardUserDefaults()
if let checkedItems = userDefaults.objectForKey("checkedItems") as? [String] {
self.checkedItems = checkedItems
}
}
تعد الكلمة الأساسية Private جزاء من متحكم الولوج الخاص بـ Swift. كما يشير الاسم، فإن متحكم الولوج يعرف أية كود لديها وصول لأية كود. تنطبق مستويات الولوج على الطرق، الدوال، الأنواع، إلخ. تشير Apple إلى كائنات بكل بساطة. هنالك ثلاث مستويات للولوج: علني، داخلي وخاص.
علني: إن الكائنات التي تعين كعلنية يمكن الولوج إليها من خلال كائنات معرفة في نفس الجزء وكذلك بغير أجزاء. إن مستوى الولوج هذا يعد مثالياً من أجل عرض سطح إطار عمل.
داخلي: يعد هذا مستوى الولوج الافتراضي. بكلمات أخرى، إذا لم يحدد أي مستوى ولوج، يتم تطبيق هذا المستوى. يتم الولوج إلى الكائنات التي ينطبق عليها هذا المستوى من خلال كائنات معرفة في نفس الجزء.
خاص: إن الكائن المعلن عنه كخاص يمكن الولوج له فقط من خلال كائنات معرفة في نفس ملف المصدر. على سبيل المثال، يمكن الولوج إلى الطرق المساعدة الخاصة المعرفة في صنف ViewController فقط من خلال صنف ViewController.
يعد إنشاء الطرق المساعدة بسيطاً إذا كنت على معرفة بصنف NSUserDefaults. من أجل الطمأنينة بالاستخدام، سنقوم بتخزين مرجع لكائن المستخدم الافتراضي في ثابت يحمل الاسم userDefaults. في حالة loadItems، سنسأل userDefaults أن يعطينا الكائن المرتبط بالمفتاح "items" ونحوله إلى مصفوفة اختيارية من النصوص. سنقوم بإزالة الاختياري بشكل آمن، والذي يعني أننا قمنا بتخزين القيمة في ثابت items في حال لم يكن الاختياري (nil) صفر، وتعيين القيمة لخصائص items.
في حال بدت جملة إذا مربكة، الق نظرة على نسخة أبسط لطريقة loadItems في المثال التالي. إن النتيجة ستكون مطابقة، الاختلاف الوحيد هو الاختصار.
private func loadItems() {
let userDefaults = NSUserDefaults.standardUserDefaults()
let storedItems = userDefaults.objectForKey("items") as? [String]
if let items = storedItems {
self.items = items
}
}
يعد إنشاء loadCheckedItems مطابقاً أيضاً باستثناء المفتاح المستخدم لتحميل الكائن المخزن في قاعدة بيانات المستخدم الافتراضية. دعونا نضع loadItems و loadCheckedItems لنستخدمهم من خلال تحديث طريقة viewDidLoad.
override func viewDidLoad() {
super.viewDidLoad()
// Set Title
self.title = "To Do"
// Load State
self.loadItems()
self.loadCheckedItems()
// Register Class for Cell Reuse
self.tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: "TableViewCell")
}
الخطوة 2: حفظ الحالة
من أجل حفظ الحالة، سنقوم بإنشاء طريقتين مساعدتين إضافيتين، saveItems و saveCheckedItems. منطق هذا يشبه كثيراً ذاك التابع لـ loadItems و loadCheckedItems. يكمن الاختلاف في أننا سنخزن البيانات في قاعدة بيانات المستخدم الافتراضية. احرص على أن المفاتيح المستخدمة في استدعاء setObject(_:forKey:) تطابق تلك المستخدمة في loadItems و loadCheckedItems.
private func saveItems() {
let userDefaults = NSUserDefaults.standardUserDefaults()
// Update User Defaults
userDefaults.setObject(self.items, forKey: "items")
userDefaults.synchronize()
}
private func saveCheckedItems() {
let userDefaults = NSUserDefaults.standardUserDefaults()
// Update User Defaults
userDefaults.setObject(self.checkedItems, forKey: "checkedItems")
userDefaults.synchronize()
}
لا يعد قرار التزامن ضرورياً. سيحرص النظام العامل على أن البيانات التي خزنتها في قاعدة بيانات المستخدم الافتراضية قد كتبت على القرص بنفس الوقت. ولكن من خلال استدعاء التزامن، ستقوم بإخبار النظام العامل بوضوح أن يكتب أية تغيرات معلقة على القرص. هذا سيكون مفيداً خلال التطور، ذلك لأن النظام العامل لن يكتب التغيرات على القرص في حال قضيت على التطبيق. حينها ربما سيبدو ذلك وكأنما شيئاً ما لا يعمل كما ينبغي.
سنحتاج أن نستدعي saveItems و saveCheckedItems في عدة أماكن. دعونا نبدأ باستدعاء saveItems عندما يتم إضافة مادة إلى القائمة. سنقوم بذلك باستخدام طريقة التفويض التابعة لـ بروتوكول AddItemViewControllerDelegate
// MARK: Add Item View Controller Delegate Methods
func controller(controller: AddItemViewController, didAddItem: String) {
// Update Data Source
self.items.append(didAddItem)
// Save State
self.saveItems()
// Reload Table View
self.tableView.reloadData()
// Dismiss Add Item View Controller
self.dismissViewControllerAnimated(true, completion: nil)
}
عندما تتغير حالة مادة في tableView(_:didSelectRowAtIndexPath:)، سنقوم بتحديث .checkedItems أنها فكرة جيدة أن نستدعي أيضاً saveCheckedItems عند تلك النقطة.
// MARK: Table View Delegate Methods
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
tableView.deselectRowAtIndexPath(indexPath, animated: true)
// Fetch Item
let item = self.items[indexPath.row]
// Fetch Table View Cell
let tableViewCell = tableView.cellForRowAtIndexPath(indexPath)
// Find Index of Item
let index = find(self.checkedItems, item)
if let index = index {
self.checkedItems.removeAtIndex(index)
tableViewCell?.accessoryType = UITableViewCellAccessoryType.None
} else {
self.checkedItems.append(item)
tableViewCell?.accessoryType = UITableViewCellAccessoryType.Checkmark
}
// Save State
self.saveCheckedItems()
}
عندما تحذف مادة، يتم تجديد كلاً من items و checkedItems. من أجل حفظ هذا التغيير، سنستدعي كلاً من saveItems و saveCheckedItems.
func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
if editingStyle == .Delete {
// Fetch Item
let item = self.items[indexPath.row]
// Update Items
self.items.removeAtIndex(indexPath.row)
if contains(self.checkedItems, item) {
self.checkedItems.removeAtIndex(indexPath.row)
}
// Save State
self.saveItems()
self.saveCheckedItems()
// Update Table View
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.Right)
}
}
هذه هي. قم ببناء وإدارة التطبيق لفحص عملك. قم بالعبث قليلاً بالتطبيق وأجبره على التوقف عن العمل. عند اطلاق التطبيق من جديد، فإن آخر حالة معروفة يجب أن تكون محملة ومرئية.
4. مراقبي الخواص
تعد خبرة مستخدم التطبيق ناقصة بعض الشيء الآن. عند حذف كل مادة أو عند اطلاق التطبيق لأول مرة، فإن المستخدم يرى لائحة مشهد فارغة. يمكننا حل ذلك من خلال إظهار رسالة عندما لا يكون هنالك أية مواد. هذا سيتيح لي الفرصة لأريك ميزة أخرى من ميزات Swift، مراقبي الخواص.
الخطوة 1: إضافة صنف
دعونا نبدأ من خلال إضافة صنف لسطح المستخدم من أجل إظهار الرسالة. أعلن عن منفذ يدعى messageLabel من نوع UILabel في صنف ViewController، افتح Main.storyboard (اللوح. الرئيسي)، وأضف صنف لمشهد متحكم المشهد.
@IBOutlet var messageLabel: UILabel!
أضف حدود الشكل الازم للصنف وأربطها بمنفذ متحكم المشهد messageLabel في Connections Inspector (مفتش الاتصالات). ضع صنف النص You don't have any to-dos. (ليس عليك القيام بشيء). وضع نص الصنف في الوسط في Attributes Inspector.
تعلم Swift من الصفر: التحكم بالولوج و مراقبي الخواص |
الخطوة 2: إنشاء مراقب خواص
يجب على صنف الرسالة أن يكون فقط مرئياً إذا لم تحوي items على عناصر. عندما يحصل ذلك، يجب علينا أن نخفي اللائحة. يمكننا حل هذه المشكلة من خلال إضافة نقاط فحص متنوعة في صنف ViewController، ولكن يعد استخدام طريقة مراقب الخواص أكثر راحة وأناقة.
كما يوحي الاسم، فإن مراقب الخواص يراقب الخواص. يستدعى مراقب الخواص عندما يتم تغيير أية خاصية، حتى عندما تكون القيمة الجديدة نفس القديمة. هنالك نوعين من مراقبي الخواص.
- WillSet: تستدعى قيل أن تتغير القيمة.
- didSet: تستدعى بعد أن تتغير القيمة.
ومن أجل الوصول إلى غايتنا، سنقوم بإنشاء مراقب didSet لخواص items. الق نظرة على تركيبة كود المقطع التالي:
var items: [String] = [] {
didSet {
let hasItems = items.count > 0
self.tableView.hidden = !hasItems
self.messageLabel.hidden = hasItems
}
}
لربما تبدو البنية غريبة بعض الشيء في بادئ الأمر، لذا دعوني أشرح لكم ما الذي يحصل. عندما تم استدعاء مراقب didSet، بعد أن تغيرت خاصية items، قمنا بالتحقق فيما إذا احتوت خواص items على أية عناصر. بالارتكاز على قيمة الثابت hasItems، سنقوم بتحديث سطح المستخدم. أنها بتلك البساطة.
يتم تمرير مراقب didSet كـ معاملة ثابتة تحوي على قيمة القيمة القديمة للخاصية. إنها محذوفة في المثال السابق، ذلك لأننا لا نحتاجها في هذا الإنشاء. يوضح المثال كيف يمكن استخدامها.
var items: [String] = [] {
didSet(oldValue) {
if oldValue != items {
let hasItems = items.count > 0
self.tableView.hidden = !hasItems
self.messageLabel.hidden = hasItems
}
}
}
لا تنتمي معاملة oldValue إلى أية نوع واضح، ذلك لأن Swift يعرف نوع خواص items. في المثال، نقوم بتحديث سطح المستخدم في حال اختلفت القيمة القديمة عن القيمة الجديدة.
يعمل مراقب willSet بطريقة مشابهة. يكمن الاختلاف الرئيسي في أن المعاملة التي تمرر لمراقب willSet هي عبارة عن ثابت يحمل قيمة الخاصية الجديدة. عند استخدام مراقبي خواص، تذكر أنه لا يتم استدعاؤهم عندما يتم بدأ النموذج.
قم ببناء وإدارة التطبيق لتحرص على أن كل شيء يعمل بالشكل المطلوب. على الرغم من أن التطبيق لا يعتبر ممتازاً ويمكنه استخدام بعض الميزات الأخرى، فإنك قمت بإنشاء أول تطبيق لك iOS باستخدام Swift.
الخلاصة
خلال آخر ثلاثة دروس في هذه السلسلة، قمت بإنشاء تطبيق iOS فعال باستخدام ميزات Swift كائنية التوجه. في حال كانت لديك الخبرة في برمجة وتطوير التطبيقات، حينئذ لابد أنك لاحظت أن البيانات الحالية لا تخلو من بعض العيوب. لا تعد فكرة تخزين المواد كنصوص وإنشاء مصفوفة منفصلة لتخزين حالة المادة فكرة جيدة إذا كنت تبني تطبيقاً مناسباً. إن إنشاء صنف ToDo منفصل لعرض وتخزين المواد في صندوق التطبيق يعد طريقة أفضل. هذا سيكون هدفنا في التنصيب القادم من هذه السلسلة.
ليست هناك تعليقات: