Статьи

Создание приложения для списка покупок с CloudKit: добавление отношений

В предыдущем посте этой серии мы добавили возможность добавлять, обновлять и удалять списки покупок. Список покупок без каких-либо предметов не очень полезен. В этом руководстве мы добавим возможность добавлять, обновлять и удалять элементы из списка покупок. Это означает, что мы будем работать со ссылками и классом CKReference .

Мы также подробнее рассмотрим модель данных приложения списка покупок. Насколько легко вносить изменения в модель данных и как приложение реагирует на изменения, которые мы вносим в инструментальную панель CloudKit?

Помните, что я буду использовать Xcode 9 и Swift 3 . Если вы используете более старую версию Xcode, имейте в виду, что вы используете другую версию языка программирования Swift.

В этом уроке мы продолжим с того места, на котором остановились в предыдущем посте этой серии. Вы можете скачать или клонировать проект с GitHub .

В настоящее время пользователь может изменить имя списка покупок, коснувшись индикатора раскрытия подробностей, но пользователь также должен иметь возможность видеть содержимое списка покупок, коснувшись одного в контроллере представления списков. Чтобы сделать это, нам сначала нужен новый подкласс UIViewController .

Класс 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 , чтобы отслеживать, какой элемент в списке покупок выбрал пользователь. Это станет ясно позже в этом уроке.

Откройте Main.storyboard , добавьте контроллер представления и установите его класс в ListViewController в Identity Inspector . Выберите ячейку прототипа контроллера представления списков, нажмите клавишу Control и перетащите из ячейки прототипа в контроллер представления списка. Выберите « Показать» в появившемся меню и установите для идентификатора значение « Список» в Инспекторе атрибутов .

Добавьте представление таблицы, метку и представление индикатора активности в представление контроллера представления. Не забудьте подключить выходы контроллера представления, а также выходы таблицы.

Выберите представление таблицы и установите ячейки прототипа равными 1 в инспекторе атрибутов . Выберите ячейку прототипа и задайте для Style значение Right Detail , Identifier для ItemCell и Accessory to Disclosure Indicator . Вот как должен выглядеть контроллер вида, когда вы закончите.

Контроллер представления списка

Прежде чем мы вернемся к платформе 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() {
     
}

Мы почти готовы принять заявку на очередной тестовый запуск. Прежде чем мы это сделаем, нам нужно реализовать протокол 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
}

Чтобы связать все вместе, нам нужно вернуться к классу 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 , элементы не будут отображаться. Это то, что нам нужно исправить.

Прежде чем мы сможем извлечь элементы из бэкэнда 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 {
    …
}

Откройте 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()
}

Создайте и запустите приложение. Вы еще не увидите никаких товаров, но пользовательский интерфейс должен обновиться, чтобы отразить, что список покупок пуст.

Пришло время реализовать возможность добавлять товары в список покупок. Начните с создания нового подкласса 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)
}

Откройте Main.storyboard , добавьте элемент панели кнопок на панель навигации контроллера представления списка и установите System Item на Add в Инспекторе Атрибутов . Перетащите контроллер представления из библиотеки объектов и установите его класс AddItemViewController . Создайте переход из только что созданного элемента панели кнопок в контроллер представления добавления элемента. Выберите « Показать» в появившемся меню и установите идентификатор перехода в ItemDetail .

Добавьте два элемента панели кнопок на панель навигации контроллера добавления элемента, кнопку отмены слева и кнопку сохранения справа. Подключите каждый элемент кнопки панели к соответствующему действию. Добавьте текстовое поле в представление контроллера представления и не забудьте подключить выходы контроллера представления. Вот как должен выглядеть контроллер представления добавленного элемента, когда вы закончите.

Добавить элемент просмотра контроллера

Реализация контроллера представления добавления элемента не содержит ничего, что мы еще не рассмотрели. Однако есть одно исключение, которое мы обсудим чуть позже. Давайте начнем с настройки контроллера представления в 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()
}

Метод 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)
    }
}

У нас еще есть работа в классе 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
    }
}

Нам нужно реализовать еще две функции: обновление и удаление элементов списка покупок.

Чтобы удалить элементы, нам нужно реализовать 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)
    }
}

Пользователь может обновить элемент, нажав индикатор раскрытия информации. Это означает, что нам нужно реализовать метод 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.

Если вы когда-либо работали с 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 .

Обновить Добавить элемент View Controller

Действие 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 .