В предыдущем посте этой серии мы добавили возможность добавлять, обновлять и удалять списки покупок.  Список покупок без каких-либо предметов не очень полезен.  В этом руководстве мы добавим возможность добавлять, обновлять и удалять элементы из списка покупок.  Это означает, что мы будем работать со ссылками и классом CKReference . 
Мы также подробнее рассмотрим модель данных приложения списка покупок. Насколько легко вносить изменения в модель данных и как приложение реагирует на изменения, которые мы вносим в инструментальную панель CloudKit?
Предпосылки
Помните, что я буду использовать Xcode 9 и Swift 3 . Если вы используете более старую версию Xcode, имейте в виду, что вы используете другую версию языка программирования Swift.
В этом уроке мы продолжим с того места, на котором остановились в предыдущем посте этой серии. Вы можете скачать или клонировать проект с GitHub .
1. Список покупок
  В настоящее время пользователь может изменить имя списка покупок, коснувшись индикатора раскрытия подробностей, но пользователь также должен иметь возможность видеть содержимое списка покупок, коснувшись одного в контроллере представления списков.  Чтобы сделать это, нам сначала нужен новый подкласс UIViewController . 
  Шаг 1: Создание ListViewController 
  Класс ListViewController будет отображать содержимое списка покупок в виде таблицы.  Интерфейс класса ListViewController выглядит аналогично ListsViewController класса ListsViewController .  Мы импортируем платформы CloudKit и SVProgressHUD и согласовываем класс с протоколами UITableViewDataSource и UITableViewDelegate .  Поскольку мы будем использовать табличное представление, мы объявляем константу ItemCell , которая будет служить идентификатором повторного использования ячейки. 
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 | import UIKit import CloudKit import SVProgressHUD class ListViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {         static let ItemCell = «ItemCell»         @IBOutlet weak var messageLabel: UILabel!     @IBOutlet weak var tableView: UITableView!     @IBOutlet weak var activityIndicatorView: UIActivityIndicatorView!         var list: CKRecord!     var items = [CKRecord]()         var selection: Int?     … } | 
  Мы объявляем три выхода: messageLabel типа UILabel!  , tableView типа UITableView!  и UIActivityIndicatorView! типа UIActivityIndicatorView!  ,  Контроллер представления списка хранит ссылку на список покупок, который он отображает, в свойстве list , которое имеет тип CKRecord!  ,  Предметы в списке покупок хранятся в свойстве items , которое имеет тип [CKRecord] .  Наконец, мы используем вспомогательную переменную selection , чтобы отслеживать, какой элемент в списке покупок выбрал пользователь.  Это станет ясно позже в этом уроке. 
Шаг 2: Создание пользовательского интерфейса
  Откройте Main.storyboard , добавьте контроллер представления и установите его класс в ListViewController в Identity Inspector .  Выберите ячейку прототипа контроллера представления списков, нажмите клавишу Control и перетащите из ячейки прототипа в контроллер представления списка.  Выберите « Показать» в появившемся меню и установите для идентификатора значение « Список» в Инспекторе атрибутов . 
Добавьте представление таблицы, метку и представление индикатора активности в представление контроллера представления. Не забудьте подключить выходы контроллера представления, а также выходы таблицы.
Выберите представление таблицы и установите ячейки прототипа равными 1 в инспекторе атрибутов . Выберите ячейку прототипа и задайте для Style значение Right Detail , Identifier для ItemCell и Accessory to Disclosure Indicator . Вот как должен выглядеть контроллер вида, когда вы закончите.

Шаг 3: Настройка View Controller
  Прежде чем мы вернемся к платформе CloudKit, нам нужно подготовить контроллер представления для данных, которые он собирается получать.  Начните с обновления реализации viewDidLoad .  Мы устанавливаем заголовок контроллера представления на имя списка покупок и вызываем два вспомогательных метода, setupView и fetchItems . 
| 01 02 03 04 05 06 07 08 09 10 11 | // MARK: — // MARK: View Life Cycle override func viewDidLoad() {     super.viewDidLoad()         // Set Title     title = list.objectForKey(«name») as?         setupView()     fetchItems() } | 
  Метод setupView идентичен методу, который мы реализовали в классе ListsViewController . 
| 1 2 3 4 5 6 7 | // MARK: — // MARK: View Methods private func setupView() {     tableView.hidden = true     messageLabel.hidden = true     activityIndicatorView.startAnimating() } | 
  Пока мы в этом, давайте также реализуем другой знакомый вспомогательный метод, updateView .  В updateView мы обновляем пользовательский интерфейс контроллера представления на основе элементов, хранящихся в свойстве items . 
| 1 2 3 4 5 6 7 | private func updateView() {     let hasRecords = items.count > 0         tableView.hidden = !hasRecords     messageLabel.hidden = hasRecords     activityIndicatorView.stopAnimating() } | 
  Я собираюсь оставить fetchItems пустыми.  Мы вернемся к этому методу, как только закончим настройку контроллера представления списка. 
| 1 2 3 4 5 | // MARK: — // MARK: Helper Methods private func fetchItems() {     } | 
Шаг 4. Методы источника данных табличного представления
  Мы почти готовы принять заявку на очередной тестовый запуск.  Прежде чем мы это сделаем, нам нужно реализовать протокол UITableViewDataSource .  Если вы читали предыдущие части этой серии статей, то реализация будет выглядеть знакомо. 
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | // MARK: — // MARK: Table View Data Source Methods func numberOfSectionsInTableView(tableView: UITableView) -> Int {     return 1; } func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {     return items.count } func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {     // Dequeue Reusable Cell     let cell = tableView.dequeueReusableCellWithIdentifier(ListViewController.ItemCell, forIndexPath: indexPath)         // Configure Cell     cell.accessoryType = .DetailDisclosureButton         // Fetch Record     let item = items[indexPath.row]         if let itemName = item.objectForKey(«name») as?         // Configure Cell         cell.textLabel?.text = itemName             } else {         cell.textLabel?.text = «-«     }         return cell } func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {     return true } | 
Шаг 5: Обработка выбора
  Чтобы связать все вместе, нам нужно вернуться к классу ListsViewController .  Начните с реализации tableView(_:didSelectRowAtIndexPath:) протокола UITableViewDelegate . 
| 1 2 3 4 5 | // MARK: — // MARK: Table View Delegate Methods func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {     tableView.deselectRowAtIndexPath(indexPath, animated: true) } | 
  Нам также нужно обновить prepareForSegue(segue:sender:) чтобы обработать переход, который мы создали несколько минут назад.  Это означает, что нам нужно добавить новый case в оператор switch . 
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 | // MARK: — // MARK: Segue Life Cycle override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {     guard let identifier = segue.identifier else { return }         switch identifier {     case SegueList:         // Fetch Destination View Controller         let listViewController = segue.destinationViewController as!                 // Fetch Selection         let list = lists[tableView.indexPathForSelectedRow!.row]                 // Configure View Controller         listViewController.list = list     case SegueListDetail:         …     default:         break     } } | 
  Чтобы удовлетворить компилятор, нам также нужно объявить константу SegueList в начале ListsViewController.swift . 
| 01 02 03 04 05 06 07 08 09 10 11 12 | import UIKit import CloudKit import SVProgressHUD let RecordTypeLists = «Lists» let SegueList = «List» let SegueListDetail = «ListDetail» class ListsViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, AddListViewControllerDelegate {     … } | 
  Создайте и запустите приложение, чтобы увидеть, все ли правильно подключено.  Поскольку мы еще не реализовали метод fetchItems , элементы не будут отображаться.  Это то, что нам нужно исправить. 
2. Выбор предметов
Шаг 1. Создайте тип записи
Прежде чем мы сможем извлечь элементы из бэкэнда CloudKit, нам нужно создать новый тип записи в CloudKit Dashboard. Перейдите к панели инструментов CloudKit, создайте новый тип записи и назовите его Items . У каждого элемента должно быть имя, поэтому создайте новое поле, задайте для имени поля имя и установите для типа поля значение String .
Каждый товар должен также знать, к какому списку покупок он принадлежит. Это означает, что каждому предмету нужна ссылка на свой список покупок. Создайте новое поле, задайте имя поля для списка и установите тип поля « Ссылка» . Тип поля Reference был разработан для этой конкретной цели, управления отношениями.

Вернитесь к Xcode, откройте ListsViewController.swift и объявите новую константу вверху для типа записи Items .
| 01 02 03 04 05 06 07 08 09 10 11 12 13 | import UIKit import CloudKit import SVProgressHUD let RecordTypeLists = «Lists» let RecordTypeItems = «Items» let SegueList = «List» let SegueListDetail = «ListDetail» class ListsViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, AddListViewControllerDelegate {     … } | 
Шаг 2: выбор предметов
  Откройте ListViewController.swift и перейдите к методу fetchItems .  Реализация аналогична методу ListsViewController класса ListsViewController .  Однако есть важное отличие. 
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 | private func fetchItems() {     // Fetch Private Database     let privateDatabase = CKContainer.defaultContainer().privateCloudDatabase         // Initialize Query     let reference = CKReference(recordID: list.recordID, action: .DeleteSelf)     let query = CKQuery(recordType: RecordTypeItems, predicate: NSPredicate(format: «list == %@», reference))         // Configure Query     query.sortDescriptors = [NSSortDescriptor(key: «name», ascending: true)]         // Perform Query     privateDatabase.performQuery(query, inZoneWithID: nil) { (records, error) -> Void in         dispatch_async(dispatch_get_main_queue(), { () -> Void in             // Process Response on Main Thread             self.processResponseForQuery(records, error: error)         })     } } | 
  Разница между fetchItems и fetchLists заключается в предикате, который мы CKQuery инициализатору CKQuery .  Нас не интересует каждый элемент в личной базе данных пользователя.  Нас интересуют только те товары, которые связаны с определенным списком покупок.  Это отражено в предикате экземпляра CKQuery . 
  Мы создаем предикат, передавая экземпляр CKReference , который мы создаем, вызывая init(recordID:action:) .  Этот метод принимает два аргумента: экземпляр CKRecordID который ссылается на запись списка покупок, и экземпляр CKReferenceAction который определяет, что происходит при удалении списка покупок. 
  Справочные действия очень похожи на правила удаления в Базовых данных.  Если указанный объект (список покупок в этом примере) удален, то среда CloudKit проверяет действие ссылки, чтобы определить, что должно произойти с записями, которые содержат ссылку на удаленную запись.  CKReferenceAction имеет два значения элемента: 
-   None: если ссылка удалена, с записями, ссылающимися на удаленную запись, ничего не происходит.
-   DeleteSelf: если ссылка удалена, каждая запись,DeleteSelfна удаленную запись, также удаляется.
  Поскольку ни один элемент не должен существовать без списка покупок, мы установили действие ссылки на DeleteSelf . 
  Метод processResponseForQuery(records:error:) не содержит ничего нового.  Мы обрабатываем ответ на запрос и соответственно обновляем пользовательский интерфейс. 
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | private func processResponseForQuery(records: [CKRecord]?, error: NSError?) {     var message = «»         if let error = error {         print(error)         message = «Error Fetching Items for List»             } else if let records = records {         items = records                 if items.count == 0 {             message = «No Items Found»         }             } else {         message = «No Items Found»     }         if message.isEmpty {         tableView.reloadData()     } else {         messageLabel.text = message     }         updateView() } | 
Создайте и запустите приложение. Вы еще не увидите никаких товаров, но пользовательский интерфейс должен обновиться, чтобы отразить, что список покупок пуст.
3. Добавление предметов
  Шаг 1: Создание AddItemViewController 
  Пришло время реализовать возможность добавлять товары в список покупок.  Начните с создания нового подкласса AddItemViewController , AddItemViewController .  Интерфейс контроллера представления аналогичен AddListViewController класса AddListViewController . 
  Вверху мы импортируем фреймворки CloudKit и SVProgressHUD .  Мы объявляем протокол AddItemViewControllerDelegate , который будет служить той же цели, что и протокол AddListViewControllerDelegate .  Протокол определяет два метода, один для добавления элементов и один для обновления элементов. 
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | import UIKit import CloudKit import SVProgressHUD protocol AddItemViewControllerDelegate {     func controller(controller: AddItemViewController, didAddItem item: CKRecord)     func controller(controller: AddItemViewController, didUpdateItem item: CKRecord) } class AddItemViewController: UIViewController {         @IBOutlet weak var nameTextField: UITextField!     @IBOutlet weak var saveButton: UIBarButtonItem!         var delegate: AddItemViewControllerDelegate?     var newItem: Bool = true         var list: CKRecord!     var item: CKRecord?         …     } | 
  Мы объявляем два выхода, текстовое поле и элемент панели кнопок.  Мы также объявляем свойство для делегата и вспомогательную переменную newItem , которая помогает нам определить, создаем ли мы новый элемент или обновляем существующий элемент.  Наконец, мы объявляем list свойств для ссылки на список покупок, в который будет добавлен элемент, и элемент свойства для элемента, который мы создаем или обновляем. 
  Прежде чем создавать пользовательский интерфейс, давайте реализуем два действия, которые нам понадобятся в раскадровке: cancel(_:) и save(_:) .  Мы обновим реализацию действия save(_:) позже в этом руководстве. 
| 1 2 3 4 5 6 7 8 9 | // MARK: — // MARK: Actions @IBAction func cancel(sender: AnyObject) {     navigationController?.popViewControllerAnimated(true) } @IBAction func save(sender: AnyObject) {     navigationController?.popViewControllerAnimated(true) } | 
Шаг 2: Создание пользовательского интерфейса
Откройте Main.storyboard , добавьте элемент панели кнопок на панель навигации контроллера представления списка и установите System Item на Add в Инспекторе Атрибутов . Перетащите контроллер представления из библиотеки объектов и установите его класс AddItemViewController . Создайте переход из только что созданного элемента панели кнопок в контроллер представления добавления элемента. Выберите « Показать» в появившемся меню и установите идентификатор перехода в ItemDetail .
Добавьте два элемента панели кнопок на панель навигации контроллера добавления элемента, кнопку отмены слева и кнопку сохранения справа. Подключите каждый элемент кнопки панели к соответствующему действию. Добавьте текстовое поле в представление контроллера представления и не забудьте подключить выходы контроллера представления. Вот как должен выглядеть контроллер представления добавленного элемента, когда вы закончите.

Шаг 3: Настройка View Controller
  Реализация контроллера представления добавления элемента не содержит ничего, что мы еще не рассмотрели.  Однако есть одно исключение, которое мы обсудим чуть позже.  Давайте начнем с настройки контроллера представления в viewDidLoad . 
  Мы вызываем setupView , вспомогательный метод, и обновляем значение newItem .  Если свойство item равно nil , newItem равно true .  Это помогает нам определить, создаем ли мы или обновляем элемент списка покупок. 
  Мы также добавляем контроллер представления в качестве наблюдателя для уведомлений типа UITextFieldTextDidChangeNotification .  Это означает, что контроллер представления уведомляется, когда изменяется содержимое nameTextField . 
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 | // MARK: — // MARK: View Life Cycle override func viewDidLoad() {     super.viewDidLoad()         setupView()         // Update Helper     newItem = item == nil         // Add Observer     let notificationCenter = NSNotificationCenter.defaultCenter()     notificationCenter.addObserver(self, selector: «textFieldTextDidChange:», name: UITextFieldTextDidChangeNotification, object: nameTextField) } | 
  В viewDidAppear(animated:) мы показываем клавиатуру, вызывая becomeFirstResponder для nameTextField 
| 1 2 3 | override func viewDidAppear(animated: Bool) {     nameTextField.becomeFirstResponder() } | 
  Метод setupView вызывает два вспомогательных метода, updateNameTextField и updateSaveButton .  Реализация этих вспомогательных методов проста.  В updateNameTextField мы заполняем текстовое поле.  В updateSaveButton мы updateSaveButton или отключаем кнопку сохранения в зависимости от содержимого текстового поля. 
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | // MARK: — // MARK: View Methods private func setupView() {     updateNameTextField()     updateSaveButton() } // MARK: — private func updateNameTextField() {     if let name = item?.objectForKey(«name») as?         nameTextField.text = name     } } // MARK: — private func updateSaveButton() {     let text = nameTextField.text         if let name = text {         saveButton.enabled = !name.isEmpty     } else {         saveButton.enabled = false     } } | 
  Прежде чем мы взглянем на обновленную реализацию метода save(_:) , нам нужно реализовать метод textFieldDidChange(_:) .  Все, что мы делаем, это вызываем updateSaveButton чтобы включить или отключить кнопку сохранения. 
| 1 2 3 4 5 | // MARK: — // MARK: Notification Handling func textFieldTextDidChange(notification: NSNotification) {     updateSaveButton() } | 
Шаг 4: Сохранение предметов
  Метод save(_:) является наиболее интересным методом класса AddItemViewController , потому что он показывает нам, как работать со ссылками CloudKit.  Посмотрите на реализацию метода save(_:) ниже. 
  Большая часть его реализации должна выглядеть знакомой, поскольку мы рассмотрели сохранение записей в классе AddListViewController .  Что нас больше всего интересует, так это то, как товар хранит ссылку на свой список покупок.  Сначала мы создаем экземпляр CKReference , вызывая указанный инициализатор init(recordID:action:) .  Мы рассмотрели детали создания экземпляра CKReference несколько минут назад, когда создавали запрос для выборки элементов списка покупок. 
  Рассказать об этом предмете легко.  Мы вызываем setObjec(_:forKey:) для свойства item , передавая экземпляр CKReference в качестве значения и "list" в качестве ключа.  Ключ соответствует имени поля, которое мы присвоили в CloudKit Dashboard.  Сохранение элемента в iCloud идентично тому, что мы рассмотрели ранее.  Вот так легко работать с ссылками CloudKit. 
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | @IBAction func save(sender: AnyObject) {     // Helpers     let name = nameTextField.text         // Fetch Private Database     let privateDatabase = CKContainer.defaultContainer().privateCloudDatabase         if item == nil {         // Create Record         item = CKRecord(recordType: RecordTypeItems)                 // Initialize Reference         let listReference = CKReference(recordID: list.recordID, action: .DeleteSelf)                 // Configure Record         item?.setObject(listReference, forKey: «list»)     }         // Configure Record     item?.setObject(name, forKey: «name»)         // Show Progress HUD     SVProgressHUD.show()         // Save Record     privateDatabase.saveRecord(item!) { (record, error) -> Void in         dispatch_async(dispatch_get_main_queue(), { () -> Void in             // Dismiss Progress HUD             SVProgressHUD.dismiss()                         // Process Response             self.processResponse(record, error: error)         })     } } | 
  Реализация processResponse(record:error:) содержит ничего нового.  Мы проверяем, появились ли какие-либо ошибки, и, если ошибок не было, мы уведомляем делегата. 
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | // MARK: — // MARK: Helper Methods private func processResponse(record: CKRecord?, error: NSError?) {     var message = «»         if let error = error {         print(error)         message = «We were not able to save your item.»             } else if record == nil {         message = «We were not able to save your item.»     }         if !message.isEmpty {         // Initialize Alert Controller         let alertController = UIAlertController(title: «Error», message: message, preferredStyle: .Alert)                 // Present Alert Controller         presentViewController(alertController, animated: true, completion: nil)             } else {         // Notify Delegate         if newItem {             delegate?.controller(self, didAddItem: item!)         } else {             delegate?.controller(self, didUpdateItem: item!)         }                 // Pop View Controller         navigationController?.popViewControllerAnimated(true)     } } | 
  Шаг 5: Обновление ListViewController 
  У нас еще есть работа в классе ListViewController .  Начните с соответствия класса AddItemViewControllerDelegate протоколу AddItemViewControllerDelegate .  Это также хороший момент для объявления константы для передачи с идентификатором ItemDetail . 
| 1 2 3 4 5 6 7 8 9 | import UIKit import CloudKit import SVProgressHUD let SegueItemDetail = «ItemDetail» class ListViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, AddItemViewControllerDelegate {     … } | 
  Реализация протокола AddItemViewControllerDelegate тривиальна.  В controller(_:didAddItem:) мы добавляем новый элемент в items , сортируем items , перезагружаем табличное представление и вызываем updateView . 
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 | // MARK: — // MARK: Add Item View Controller Delegate Methods func controller(controller: AddItemViewController, didAddItem item: CKRecord) {     // Add Item to Items     items.append(item)         // Sort Items     sortItems()         // Update Table View     tableView.reloadData()         // Update View     updateView() } | 
  Реализация controller(_:didUpdateItem:) еще проще.  Мы сортируем items и перезагружаем табличное представление. 
| 1 2 3 4 5 6 7 | func controller(controller: AddItemViewController, didUpdateItem item: CKRecord) {     // Sort Items     sortItems()         // Update Table View     tableView.reloadData() } | 
  В sortItems мы сортируем массив экземпляров CKRecord по имени, используя функцию sortInPlace , метод протокола MutableCollectionType . 
| 01 02 03 04 05 06 07 08 09 10 11 12 13 | private func sortItems() {     items.sortInPlace {         var result = false         let name0 = $0.objectForKey(«name») as?         let name1 = $1.objectForKey(«name») as?                 if let itemName0 = name0, itemName1 = name1 {             result = itemName0.localizedCaseInsensitiveCompare(itemName1) == .OrderedAscending         }                 return result     } } | 
Нам нужно реализовать еще две функции: обновление и удаление элементов списка покупок.
Шаг 6: Удаление элементов
  Чтобы удалить элементы, нам нужно реализовать tableView(_:commitEditingStyle:forRowAtIndexPath:) протокола UITableViewDataSource .  Мы выбираем элемент списка покупок, который необходимо удалить, и передаем его в метод deleteRecord(_:) . 
| 1 2 3 4 5 6 7 8 9 | func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {     guard editingStyle == .Delete else { return }         // Fetch Record     let item = items[indexPath.row]         // Delete Record     deleteRecord(item) } | 
  Реализация deleteRecord(_:) не содержит ничего нового.  Мы вызываем deleteRecordWithID(_:completionHandler:) в частной базе данных и обрабатываем ответ в обработчике завершения. 
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 | private func deleteRecord(item: CKRecord) {     // Fetch Private Database     let privateDatabase = CKContainer.defaultContainer().privateCloudDatabase         // Show Progress HUD     SVProgressHUD.show()         // Delete List     privateDatabase.deleteRecordWithID(item.recordID) { (recordID, error) -> Void in         dispatch_async(dispatch_get_main_queue(), { () -> Void in             // Dismiss Progress HUD             SVProgressHUD.dismiss()                         // Process Response             self.processResponseForDeleteRequest(item, recordID: recordID, error: error)         })     } } | 
  В processResponseForDeleteRequest(record:recordID:error:) мы обновляем свойство items и пользовательский интерфейс.  Если что-то пошло не так, мы уведомляем пользователя, показывая предупреждение. 
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | private func processResponseForDeleteRequest(record: CKRecord, recordID: CKRecordID?, error: NSError?) {     var message = «»         if let error = error {         print(error)         message = «We are unable to delete the item.»             } else if recordID == nil {         message = «We are unable to delete the item.»     }         if message.isEmpty {         // Calculate Row Index         let index = items.indexOf(record)                 if let index = index {             // Update Data Source             items.removeAtIndex(index)                         if items.count > 0 {                 // Update Table View                 tableView.deleteRowsAtIndexPaths([NSIndexPath(forRow: index, inSection: 0)], withRowAnimation: .Right)                             } else {                 // Update Message Label                 messageLabel.text = «No Items Found»                                 // Update View                 updateView()             }         }             } else {         // Initialize Alert Controller         let alertController = UIAlertController(title: «Error», message: message, preferredStyle: .Alert)                 // Present Alert Controller         presentViewController(alertController, animated: true, completion: nil)     } } | 
Шаг 7: Обновление элементов
  Пользователь может обновить элемент, нажав индикатор раскрытия информации.  Это означает, что нам нужно реализовать метод tableView(_:accessoryButtonTappedForRowWithIndexPath:) .  В этом методе мы сохраняем выбор пользователя и вручную выполняем переход ListDetail .  Обратите внимание, что ничего не происходит в tableView(_:didSelectRowAtIndexPath:) .  Все, что мы делаем, это отменим выбор строки, которую нажал пользователь. 
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 | // MARK: — // MARK: Table View Delegate Methods func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {     tableView.deselectRowAtIndexPath(indexPath, animated: true) } func tableView(tableView: UITableView, accessoryButtonTappedForRowWithIndexPath indexPath: NSIndexPath) {     tableView.deselectRowAtIndexPath(indexPath, animated: true)         // Save Selection     selection = indexPath.row         // Perform Segue     performSegueWithIdentifier(SegueItemDetail, sender: self) } | 
  В prepareForSegue(_:sender:) мы выбираем элемент списка покупок, используя значение свойства selection и настраиваем целевой контроллер представления, экземпляр класса AddItemViewController . 
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | // MARK: — // MARK: Segue Life Cycle override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {     guard let identifier = segue.identifier else { return }         switch identifier {     case SegueItemDetail:         // Fetch Destination View Controller         let addItemViewController = segue.destinationViewController as!                 // Configure View Controller         addItemViewController.list = list         addItemViewController.delegate = self                 if let selection = selection {             // Fetch Item             let item = items[selection]                         // Configure View Controller             addItemViewController.item = item         }     default:         break     } } | 
Это все, что нам нужно сделать, чтобы удалить и обновить элементы списка покупок. В следующем разделе я покажу вам, как легко обновить модель данных в CloudKit Dashboard.
4. Обновление модели данных
Если вы когда-либо работали с Core Data, то знаете, что обновление модели данных следует проводить с осторожностью. Вам необходимо убедиться, что вы ничего не сломали и не повредили постоянные хранилища приложения. CloudKit немного более гибок.
Тип записи Предметов в настоящее время имеет два поля: имя и список . Я хочу показать вам, что нужно для обновления модели данных, добавив новое поле. Откройте панель управления CloudKit и добавьте новое поле в запись « Элементы» . Установите имя поля в число и установите тип поля в Int (64) . Не забудьте сохранить свои изменения.

Давайте теперь добавим возможность изменять номер элемента. Откройте AddItemViewController.swift и объявите два выхода, метку и степпер.
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | import UIKit import CloudKit import SVProgressHUD protocol AddItemViewControllerDelegate {     func controller(controller: AddItemViewController, didAddItem item: CKRecord)     func controller(controller: AddItemViewController, didUpdateItem item: CKRecord) } class AddItemViewController: UIViewController {         @IBOutlet weak var numberLabel: UILabel!     @IBOutlet weak var numberStepper: UIStepper!     @IBOutlet weak var nameTextField: UITextField!     @IBOutlet weak var saveButton: UIBarButtonItem!         var delegate: AddItemViewControllerDelegate?     var newItem: Bool = true         var list: CKRecord!     var item: CKRecord?         …     } | 
  Нам также нужно добавить действие, которое запускается при изменении значения степпера.  В numberDidChange(_:) мы обновляем содержимое numberLabel . 
| 1 2 3 4 5 6 | @IBAction func numberDidChange(sender: UIStepper) {     let number = Int(sender.value)         // Update Number Label     numberLabel.text = «\(number)» } | 
  Откройте Main.storyboard и добавьте метку и степпер в контроллер представления добавления элемента.  Подключите выходы контроллера представления к соответствующим элементам пользовательского интерфейса и подключите действие numberDidChange(_:) к степперу для события Value Changed . 

  Действие save(_:) класса AddItemViewController также немного изменяется.  Давайте посмотрим, как это выглядит. 
  Нам нужно только добавить две строки кода.  Вверху мы храним значение степпера в постоянном number .  Когда мы настраиваем item , мы устанавливаем number в качестве значения для цифровой клавиши, и это все, что нужно сделать. 
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | @IBAction func save(sender: AnyObject) {     // Helpers     let name = nameTextField.text     let number = Int(numberStepper.value)         // Fetch Private Database     let privateDatabase = CKContainer.defaultContainer().privateCloudDatabase         if item == nil {         // Create Record         item = CKRecord(recordType: RecordTypeItems)                 // Initialize Reference         let listReference = CKReference(recordID: list.recordID, action: .DeleteSelf)                 // Configure Record         item?.setObject(listReference, forKey: «list»)     }         // Configure Record     item?.setObject(name, forKey: «name»)     item?.setObject(number, forKey: «number»)         // Show Progress HUD     SVProgressHUD.show()         print(item?.recordType)         // Save Record     privateDatabase.saveRecord(item!) { (record, error) -> Void in         dispatch_async(dispatch_get_main_queue(), { () -> Void in             // Dismiss Progress HUD             SVProgressHUD.dismiss()                         // Process Response             self.processResponse(record, error: error)         })     } } | 
  Нам также необходимо реализовать вспомогательный метод для обновления пользовательского интерфейса контроллера представления добавления элемента.  Метод updateNumberStepper проверяет, имеет ли запись поле с именем number, и обновляет степпер, если это так. 
| 1 2 3 4 5 | private func updateNumberStepper() {     if let number = item?.objectForKey(«number») as?         numberStepper.value = number     } } | 
  Мы вызываем updateNumberStepper в методе AddItemViewController класса AddItemViewController . 
| 1 2 3 4 5 | private func setupView() {     updateNameTextField()     updateNumberStepper()     updateSaveButton() } | 
  Чтобы визуализировать номер каждого элемента, нам нужно сделать одно изменение в ListViewController .  В tableView(_:cellForRowAtIndexPath:) мы устанавливаем содержимое правой метки ячейки равным значению поля номера элемента. 
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {     // Dequeue Reusable Cell     let cell = tableView.dequeueReusableCellWithIdentifier(ListViewController.ItemCell, forIndexPath: indexPath)         // Configure Cell     cell.accessoryType = .DetailDisclosureButton         // Fetch Record     let item = items[indexPath.row]         if let itemName = item.objectForKey(«name») as?         // Configure Cell         cell.textLabel?.text = itemName             } else {         cell.textLabel?.text = «-«     }         if let itemNumber = item.objectForKey(«number») as?         // Configure Cell         cell.detailTextLabel?.text = «\(itemNumber)»             } else {         cell.detailTextLabel?.text = «1»     }         return cell } | 
Это все, что нам нужно сделать для реализации изменений, которые мы внесли в модель данных. Нет необходимости выполнять миграцию или что-то подобное. CloudKit заботится о мельчайших деталях.
Вывод
Теперь у вас должна быть надлежащая основа в структуре CloudKit. Я надеюсь, что вы согласны с тем, что Apple отлично поработала с этой платформой и облачной панелью управления CloudKit. Мы многое еще не рассмотрели в этой серии, но к настоящему времени вы уже достаточно научились, чтобы начать использовать CloudKit в своих собственных проектах.
Если у вас есть какие-либо вопросы или комментарии, не стесняйтесь оставлять их в комментариях ниже или обращаться ко мне в Twitter .