В первом уроке этой серии мы изучили инфраструктуру и инфраструктуру CloudKit. Мы также заложили основу для примера приложения, которое мы собираемся создать, приложения для списка покупок. В этом уроке мы сосредоточены на добавлении, редактировании и удалении списков покупок.
Предпосылки
Как я упоминал в предыдущем уроке , я буду использовать Xcode 9 и Swift 4 . Если вы используете более старую версию Xcode, имейте в виду, что вы можете использовать другую версию языка программирования Swift.
В этом уроке мы продолжим работу с проектом, который мы создали в первом уроке . Вы можете скачать его с GitHub (тег add_records ).
1. Настройка CocoaPods
Приложение списка покупок будет использовать библиотеку SVProgressHUD , популярную библиотеку, созданную Сэмом Верметтом, которая позволяет легко отображать индикатор прогресса. Вы можете добавить библиотеку вручную в свой проект, но я настоятельно рекомендую использовать CocoaPods для управления зависимостями. Вы новичок в CocoaPods? Прочитайте это вводное руководство для CocoaPods, чтобы освоиться.
Шаг 1: Создание подфайла
Откройте Finder и перейдите к корню вашего проекта XCode. Создайте новый файл, назовите его Podfile и добавьте в него следующие строки Ruby.
01
02
03
04
05
06
07
08
09
10
|
# Uncomment the next line to define a global platform for your project
# platform :ios, ‘9.0’
target ‘Lists’ do
# Comment the next line if you’re not using Swift and don’t want to use dynamic frameworks
use_frameworks!
pod ‘SVProgressHUD’, ‘~> 1.1’
end
|
Первая строка указывает платформу iOS и цель развертывания проекта, iOS 9.0 . Вторая строка важна, если вы используете Swift. Swift не поддерживает статические библиотеки, но CocoaPods предоставляет возможность с версии 0.36 использовать фреймворки. Затем мы указываем зависимости для цели Lists проекта. Замените списки именем вашей цели, если ваша цель названа по-другому.
Шаг 2: Установка зависимостей
Откройте терминал , перейдите к корню вашего проекта XCode и запустите pod install
. Это сделает для вас несколько вещей, таких как установка зависимостей, указанных в Podfile, и создание рабочей области Xcode.
После завершения установки CocoaPods закройте проект и откройте рабочую область CocoaPods, созданную для вас. Последнее очень важно. Откройте рабочее пространство, а не проект . Рабочая область включает два проекта: проект Lists и проект с именем Pods .
2. Список покупок
Шаг 1: Уборка
Мы готовы переориентироваться на платформу CloudKit. Во-первых, однако, мы должны сделать некоторые домашние хозяйства, переименовав Класс ListsViewController
класса ListsViewController
.
Начните с переименования ViewController.swift в ListsViewController.swift . Откройте ListsViewController.swift и измените имя Класс ListsViewController
класса ListsViewController
.
Затем откройте Main.storyboard , разверните View Scene Scene в Outline документа слева и выберите View Controller . Откройте Identity Inspector справа и измените Class на ListsViewController .
Шаг 2: Добавление табличного представления
Когда пользователь открывает приложение, ему представляется список покупок. Мы будем отображать списки покупок в виде таблицы. Давайте начнем с настройки пользовательского интерфейса. Выберите Контроллер Представления Списков в Сцене Контроллера Представления Списков и выберите Вставить> Контроллер навигации из меню Редактора XCode.
Добавьте табличное представление в представление контроллера представления и создайте необходимые ограничения макета для него. Выбрав табличное представление, откройте инспектор атрибутов и установите ячейки прототипа в 1 . Выберите ячейку прототипа и установите для стиля значение Basic, а для идентификатора значение ListCell .
Выбрав табличное представление, откройте инспектор соединений . Подключите источник данных табличного представления и delegate
выходы к контроллеру представления списков .
Шаг 3: Пустое состояние
Хотя мы только создаем пример приложения, чтобы проиллюстрировать, как работает CloudKit, я хотел бы отобразить сообщение, если что-то идет не так или если в iCloud не было найдено списков покупок. Добавьте метку к контроллеру представления, сделайте ее такой же большой, как вид контроллера представления, создайте для нее необходимые ограничения макета и отцентрируйте текст метки.
Поскольку мы имеем дело с сетевыми запросами, я также хочу отображать представление индикатора активности, пока приложение ожидает ответа от iCloud. Добавьте представление индикатора активности в представление контроллера представления и центрируйте его в его родительском представлении. В инспекторе атрибутов установите флажок «Скрывать при остановке» .
Шаг 4: Подключение розеток
Откройте ListsViewController.swift и объявите выход для метки, представления таблицы и представления индикатора активности. Это также хорошее время, чтобы ListsViewController
класс ListsViewController
соответствие с протоколами UITableViewDataSource
и UITableViewDelegate
.
Обратите внимание, что я также добавил оператор импорта для инфраструктуры SVProgressHUD и объявил статическую константу для идентификатора повторного использования ячейки прототипа, которую мы создали в раскадровке.
01
02
03
04
05
06
07
08
09
10
11
12
|
import UIKit
import CloudKit
import SVProgressHUD
class ListsViewController: UIViewController, UITableViewDelegate, UITableViewDataSource{
static let ListCell = «ListCell»
@IBOutlet weak var tableView: UITableView!
@IBOutlet weak var messageLabel: UILabel!
@IBOutlet weak var activityIndicatorView: UIActivityIndicatorView!
…
}
|
Вернитесь к раскадровке и соедините розетки с соответствующими представлениями в сцене контроллера списков .
Шаг 5: Подготовка табличного представления
Прежде чем мы получим данные из iCloud, мы должны убедиться, что табличное представление готово для отображения данных. Сначала нам нужно создать свойство, lists
для хранения записей, которые мы собираемся получить. Помните, что записи являются экземплярами класса CKRecord
. Это означает, что свойство, которое будет хранить данные из iCloud, имеет тип [CKRecord]
, массив экземпляров CKRecord
.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
import UIKit
import CloudKit
import SVProgressHUD
class ListsViewController: UIViewController, UITableViewDelegate, UITableViewDataSource{
static let ListCell = «ListCell»
@IBOutlet weak var tableView: UITableView!
@IBOutlet weak var messageLabel: UILabel!
@IBOutlet weak var activityIndicatorView: UIActivityIndicatorView!
var lists = [CKRecord]()
…
}
|
Для начала нам нужно реализовать три метода протокола UITableViewDataSource
:
-
numberOfSectionsInTableView(_:)
-
numberOfRowsInSection(_:)
-
cellForRowAtIndexPath(_:)
Если у вас есть опыт работы с табличными представлениями, то реализация каждого из этих методов проста. Однако cellForRowAtIndexPath(_:)
может потребовать некоторого объяснения. Помните, что экземпляр CKRecord
— это перегруженный словарь пар ключ-значение. Чтобы получить доступ к значению определенного ключа, вы вызываете objectForKey(_:)
CKRecord
объекта CKRecord
. Это то, что мы делаем в cellForRowAtIndexPath(_:)
. Мы выбираем запись, соответствующую строке табличного представления, и запрашиваем у нее значение для ключа "name"
. Если пара ключ-значение не существует, мы показываем тире, чтобы указать, что список еще не имеет названия.
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: UITableView Delegate Methods
extension ListsViewController{
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return lists.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// Dequeue Reusable Cell
let cell = tableView.dequeueReusableCell(withIdentifier: ListsViewController.ListCell, for: indexPath)
// Configure Cell
cell.accessoryType = .detailDisclosureButton
// Fetch Record
let list = lists[indexPath.row]
if let listName = list.object(forKey: «name») as?
// Configure Cell
cell.textLabel?.text = listName
} else {
cell.textLabel?.text = «-«
}
return cell
}
}
|
Шаг 6: Подготовка пользовательского интерфейса
Нам нужно сделать еще один шаг: подготовить пользовательский интерфейс. В методе viewDidLoad
контроллера viewDidLoad
удалите fetchUserRecordID
метода fetchUserRecordID
и вызовите setupView
, вспомогательный метод.
1
2
3
4
5
|
override func viewDidLoad() {
super.viewDidLoad()
setupView()
}
|
Метод setupView
подготавливает пользовательский интерфейс для извлечения списка записей. Мы скрываем метку и табличное представление и сообщаем представлению индикатора активности начать анимацию.
1
2
3
4
5
6
7
|
// MARK: —
// MARK: View Methods
private func setupView() {
tableView.hidden = true
messageLabel.hidden = true
activityIndicatorView.startAnimating()
}
|
Создайте и запустите приложение на устройстве или в iOS Simulator. Если вы выполнили вышеуказанные шаги, вы должны увидеть пустое представление с представлением индикатора вращающейся активности в центре.
Шаг 7: Создание типа записи
Прежде чем мы получим какие-либо записи, нам нужно создать тип записи для списка покупок на панели инструментов CloudKit. Информационная панель CloudKit — это веб-приложение, которое позволяет разработчикам управлять данными, хранящимися на серверах Apple iCloud.
Выберите проект в Навигаторе проектов и выберите цель Списки из списка целей. Откройте вкладку « Возможности » вверху и разверните раздел iCloud . Ниже списка контейнеров iCloud нажмите кнопку с надписью CloudKit Dashboard .
Войдите в свою учетную запись разработчика и убедитесь, что приложение Lists выбрано в левом верхнем углу. Слева выберите « Типы записей» в разделе « Схема ». Каждое приложение по умолчанию имеет тип записи « Пользователи ». Чтобы создать новый тип записи, нажмите кнопку «плюс» в верхней части третьего столбца. Мы будем следовать соглашению об именовании Apple и назовем тип записи « Списки» , а не « Список» .
Обратите внимание, что первое поле создается автоматически. Создать поле имя и установите тип поля в строку . Не забудьте нажать кнопку « Сохранить» внизу, чтобы создать тип записи « Списки ». Мы вернемся к инструментальной панели CloudKit позже в этой серии.
Затем включите индексирование для свойства вашего документа, перейдя на вкладку « Индексы » и добавив новый SORTABLE и другой тип индекса QUERYABLE для имени, и нажмите « Сохранить» .
Наконец, перейдите на вкладку « РОЛЬ БЕЗОПАСНОСТИ » и, в целях данного упражнения, установите все флажки, чтобы убедиться, что у вашего пользователя есть доступ к таблице.
Шаг 8: Выполнение запроса
С созданным типом записи Lists , наконец, пришло время извлечь некоторые записи из iCloud. Платформа CloudKit предоставляет два API для взаимодействия с iCloud: удобный API и API на NSOperation
класса NSOperation
. В этой серии мы будем использовать оба API-интерфейса, но пока будем упрощать и будем использовать удобный API-интерфейс.
В Xcode откройте ListsViewController.swift и вызовите метод viewDidLoad
в viewDidLoad
. Метод fetchLists
является еще одним вспомогательным методом. Давайте посмотрим на реализацию метода.
1
2
3
4
5
6
|
override func viewDidLoad() {
super.viewDidLoad()
setupView()
fetchLists()
}
|
Поскольку запись списка покупок хранится в личной базе данных пользователя, мы сначала получаем ссылку на частную базу данных контейнера по умолчанию. Чтобы получить списки покупок пользователя, нам нужно выполнить запрос к частной базе данных, используя класс CKQuery
.
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
|
private func fetchLists() {
// Fetch Private Database
let privateDatabase = CKContainer.default().privateCloudDatabase
// Initialize Query
let query = CKQuery(recordType: «Lists», predicate: NSPredicate(value: true))
// Configure Query
query.sortDescriptors = [NSSortDescriptor(key: «name», ascending: true)]
// Perform Query
privateDatabase.perform(query, inZoneWith: nil) { (records, error) in
records?.forEach({ (record) in
guard error == nil else{
print(error?.localizedDescription as Any)
return
}
print(record.value(forKey: «name») ?? «»)
self.lists.append(record)
DispatchQueue.main.sync {
self.tableView.reloadData()
self.messageLabel.text = «»
updateView()
}
})
}
}
|
Мы инициализируем экземпляр CKQuery
, вызывая init(recordType:predicate:)
, передавая тип записи и объект NSPredicate
.
Прежде чем выполнить запрос, мы устанавливаем свойство запроса sortDescriptors
. Мы создаем массив, содержащий объект NSSortDescriptor
с ключом "name"
и возрастающим значением true
.
Выполнить запрос так же просто, как вызвать performQuery(_:inZoneWithID:completionHandler:)
для privateDatabase
, передав query
в качестве первого аргумента. Второй параметр указывает идентификатор зоны записи, по которой будет выполняться запрос. Передав значение nil
, запрос выполняется в зоне базы данных по умолчанию, и мы получаем экземпляр каждой записи, возвращаемой из запроса.
В конце метода мы вызываем updateView
. В этом вспомогательном методе мы обновляем пользовательский интерфейс на основе содержимого свойства lists
.
1
2
3
4
5
6
7
|
private func updateView(){
let hasRecords = self.lists.count > 0
self.tableView.isHidden = !hasRecords
messageLabel.isHidden = hasRecords
activityIndicatorView.stopAnimating()
}
|
Создайте и запустите приложение, чтобы проверить, что у нас есть. В настоящее время у нас нет записей, но мы исправим это в следующем разделе этого урока.
3. Добавление списка покупок
Шаг 1. Создание класса AddListViewController
Поскольку добавление и редактирование списка покупок очень похожи, мы собираемся реализовать оба одновременно. Создайте новый файл и назовите его AddListViewController.swift . Откройте вновь созданный файл и создайте подкласс AddListViewController
именем AddListViewController
. Вверху добавьте операторы импорта для платформ UIKit , CloudKit и SVProgressHUD . Объявите два выхода, один из которых типа UITextField!
и один из типов UIBarButtonItem!
, И последнее, но не менее важное: создайте два действия: cancel(_:)
и save(_:)
.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
|
import UIKit
import CloudKit
import SVProgressHUD
class AddListViewController: UIViewController {
@IBOutlet weak var nameTextField: UITextField!
@IBOutlet weak var saveButton: UIBarButtonItem!
@IBAction func cancel(sender: AnyObject) {
}
@IBAction func save(sender: AnyObject) {
}
}
|
Шаг 2: Создание пользовательского интерфейса
Откройте Main.storyboard и добавьте контроллер представления в раскадровку. С выбранным контроллером представления, откройте Identity Inspector справа и установите Class в AddListViewController
.
Пользователь сможет перейти к контроллеру добавления списка, нажав кнопку в контроллере просмотра списка.
Перетащите элемент панели кнопок из библиотеки объектов на панель навигации контроллера представления списков . Выбрав элемент панели кнопок, откройте инспектор атрибутов и установите для параметра « Система» значение « Добавить» . Нажмите Control и перетащите от элемента кнопки панели к добавлению контроллера представления списка и выберите Show Detail в появившемся меню.
Выберите только что созданный переход и задайте для идентификатора значение ListDetail в Инспекторе атрибутов справа.
Добавьте два элемента панели кнопок на панель навигации контроллера представления списка, один слева и один справа. Установите Системный элемент для элемента левой панели на Отмена, а для элемента на правой панели — Сохранить . Наконец, добавьте текстовое поле в контроллер представления списка добавления. Отцентрируйте текстовое поле и установите его выравнивание по центру в инспекторе атрибутов .
Наконец, подключите выходы и действия, созданные в AddListViewController.swift, к соответствующим элементам пользовательского интерфейса в сцене.
Шаг 3: протокол AddListViewControllerDelegate
Прежде чем мы реализуем класс AddListViewController
, нам нужно объявить протокол, который мы будем использовать для связи между контроллером представления списка добавления и контроллером представления списка. Протокол определяет два метода, один для добавления и один для обновления списка покупок. Вот как выглядит протокол.
1
2
3
4
|
protocol AddListViewControllerDelegate {
func controller(controller: AddListViewController, didAddList list: CKRecord)
func controller(controller: AddListViewController, didUpdateList list: CKRecord)
}
|
Нам также нужно объявить три свойства: одно для делегата, одно для списка покупок, который создается или обновляется, и вспомогательная переменная, которая указывает, создаем ли мы новый список покупок или редактируем существующую запись.
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
|
import UIKit
import CloudKit
import SVProgressHUD
protocol AddListViewControllerDelegate {
func controller(controller: AddListViewController, didAddList list: CKRecord)
func controller(controller: AddListViewController, didUpdateList list: CKRecord)
}
class AddListViewController: UIViewController {
@IBOutlet weak var nameTextField: UITextField!
@IBOutlet weak var saveButton: UIBarButtonItem!
var delegate: AddListViewControllerDelegate?
var newList: Bool = true
var list: CKRecord?
@IBAction func cancel(sender: AnyObject) {
}
@IBAction func save(sender: AnyObject) {
}
}
|
Реализация класса AddListViewController
проста. Методы, связанные с жизненным циклом представления, короткие и простые для понимания. В viewDidLoad
мы сначала setupView
вспомогательный метод setupView
. Мы реализуем этот метод в ближайшее время. Затем мы обновляем значение вспомогательной переменной newList
на основе значения свойства list
. Если list
равен nil
, тогда мы знаем, что создаем новую запись. В viewDidLoad
мы также добавляем контроллер представления в качестве наблюдателя для уведомлений UITextFieldTextDidChangeNotification
.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
override func viewDidLoad() {
super.viewDidLoad()
self.setupView()
// Update Helper
self.newList = self.list == nil
// Add Observer
let notificationCenter = NotificationCenter.default
notificationCenter.addObserver(self, selector: #selector(AddListViewController.textFieldTextDidChange(notification:)), name: NSNotification.Name.UITextFieldTextDidChange, object: nameTextField)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
nameTextField.becomeFirstResponder()
}
|
В viewDidAppear(_:)
мы вызываемcomeFirstResponder для текстового поля, чтобы представить клавиатуру пользователю.
В setupView
мы вызываем два вспомогательных метода, updateNameTextField
и updateSaveButton
. В updateNameTextField
мы заполняем текстовое поле, если list
не nil
. Другими словами, если мы редактируем существующую запись, мы заполняем текстовое поле названием этой записи.
Метод updateSaveButton
отвечает за включение и отключение элемента панели кнопок в правом верхнем углу. Мы включаем кнопку сохранения только в том случае, если имя списка покупок не является пустой строкой.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
|
private func setupView() {
updateNameTextField()
updateSaveButton()
}
// MARK: —
private func updateNameTextField() {
if let name = list?.object(forKey: «name») as?
nameTextField.text = name
}
}
// MARK: —
private func updateSaveButton() {
let text = nameTextField.text
if let name = text {
saveButton.isEnabled = !name.isEmpty
} else {
saveButton.isEnabled = false
}
}
|
Шаг 4: Реализация действий
Действие cancel(_:)
настолько просто, насколько это возможно. Мы выталкиваем контроллер вида сверху из стека навигации. Действие save(_:)
более интересно. В этом методе мы извлекаем вводимые пользователем данные из текстового поля и получаем ссылку на частную базу данных контейнера по умолчанию.
Если мы добавляем новый список покупок, то создаем новый экземпляр CKRecord
, вызывая init(recordType:)
, передавая RecordTypeLists
в качестве типа записи. Затем мы обновляем имя списка покупок, устанавливая значение записи для ключа "name"
.
Поскольку сохранение записи включает сетевой запрос и может занять нетривиальное время, мы показываем индикатор прогресса. Чтобы сохранить новую запись или любые изменения в существующей записи, мы вызываем saveRecord(_:completionHandler:)
в privateDatabase
, передавая запись в качестве первого аргумента. Второй аргумент — это другой обработчик завершения, который вызывается при сохранении записи, успешно или безуспешно.
Обработчик завершения принимает два аргумента: необязательный CKRecord
и необязательный NSError
. Как я упоминал ранее, обработчик завершения может быть вызван в любом потоке, что означает, что мы должны кодировать это. Мы делаем это, явно вызывая метод processResponse(_: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
33
34
|
@IBAction func cancel(sender: AnyObject) {
self.dismiss(animated: true, completion: nil)
}
@IBAction func save(sender: AnyObject) {
// Helpers
let name = self.nameTextField.text!
// Fetch Private Database
let privateDatabase = CKContainer.default().privateCloudDatabase
if list == nil {
list = CKRecord(recordType: «Lists»)
}
// Configure Record
list?.setObject(name, forKey: «name»)
// Show Progress HUD
SVProgressHUD.show()
// Save Record
privateDatabase.save(list!) { (record, error) -> Void in
DispatchQueue.main.sync {
// Dismiss Progress HUD
SVProgressHUD.dismiss()
// Process Response
self.processResponse(record: record, error: error)
}
}
}
|
В processResponse(_:error:)
мы проверяем, была ли processResponse(_: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
33
|
// MARK: —
// MARK: Helper Methods
private func processResponse(record: CKRecord?, error: Error?) {
var message = «»
if let error = error {
print(error)
message = «We were not able to save your list.»
} else if record == nil {
message = «We were not able to save your list.»
}
if !message.isEmpty {
// Initialize Alert Controller
let alertController = UIAlertController(title: «Error», message: message, preferredStyle: .alert)
// Present Alert Controller
present(alertController, animated: true, completion: nil)
} else {
// Notify Delegate
if newList {
delegate?.controller(controller: self, didAddList: list!)
} else {
delegate?.controller(controller: self, didUpdateList: list!)
}
// Pop View Controller
self.dismiss(animated: true, completion: nil)
}
}
|
И последнее, но не менее важное: когда контроллер представления получает уведомление UITextFieldTextDidChangeNotification
, он вызывает updateSaveButton
для обновления кнопки сохранения.
1
2
3
4
5
|
// MARK: —
// MARK: Notification Handling
func textFieldTextDidChange(notification: NSNotification) {
updateSaveButton()
}
|
Шаг 5: связывая все вместе
В классе ListsViewController
нам все еще нужно позаботиться о нескольких вещах. Давайте начнем с согласования класса с протоколом AddListViewControllerDelegate
.
1
2
|
class ListsViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, AddListViewControllerDelegate {
…
|
Это также означает, что нам нужно реализовать методы протокола AddListViewControllerDelegate
. В методе controller(_:didAddList:)
мы добавляем новую запись в массив объектов CKRecord
. Затем мы сортируем массив записей, перезагружаем табличное представление и вызываем updateView
на контроллере представления.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
// MARK: —
// MARK: Add List View Controller Delegate Methods
func controller(controller: AddListViewController, didAddList list: CKRecord) {
// Add List to Lists
lists.append(list)
// Sort Lists
sortLists()
// Update Table View
tableView.reloadData()
// Update View
updateView()
}
|
Метод sortLists
довольно sortLists
. Мы вызываем sortInPlace
для массива записей, сортируя массив по имени записи.
01
02
03
04
05
06
07
08
09
10
11
12
13
|
private func sortLists() {
self.lists.sort {
var result = false
let name0 = $0.object(forKey: «name») as?
let name1 = $1.object(forKey: «name») as?
if let listName0 = name0, let listName1 = name1 {
result = listName0.localizedCaseInsensitiveCompare(listName1) == .orderedAscending
}
return result
}
}
|
Реализация второго метода протокола AddListViewControllerDelegate
, controller(_:didUpdateList:)
, выглядит практически идентично. Поскольку мы не добавляем запись, нам нужно только отсортировать массив записей и перезагрузить табличное представление. Нет необходимости вызывать updateView
на контроллере представления, поскольку массив записей по определению не пуст.
1
2
3
4
5
6
7
|
func controller(controller: AddListViewController, didUpdateList list: CKRecord) {
// Sort Lists
sortLists()
// Update Table View
tableView.reloadData()
}
|
Чтобы редактировать запись, пользователь должен нажать на вспомогательную кнопку строки представления таблицы. Это означает, что нам нужно реализовать метод tableView(_:accessoryButtonTappedForRowWithIndexPath:)
протокола UITableViewDelegate
. Прежде чем мы реализуем этот метод, объявите вспомогательное свойство selection
, чтобы сохранить выбор пользователя.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
class ListsViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
static let ListCell = «ListCell»
@IBOutlet weak var messageLabel: UILabel!
@IBOutlet weak var tableView: UITableView!
@IBOutlet weak var activityIndicatorView: UIActivityIndicatorView!
var lists = [CKRecord]()
var selection: Int?
…
}
|
В tableView(_:accessoryButtonTappedForRowWithIndexPath:)
мы сохраняем выбор пользователя в selection
и сообщаем контроллеру представления выполнить переход, который приводит к добавлению контроллера представления списка.
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
|
// MARK: —
// MARK: Segue Life Cycle
func tableView(_ tableView: UITableView, accessoryButtonTappedForRowWith indexPath: IndexPath) {
tableView.deselectRow(at: indexPath as IndexPath, animated: true)
// Save Selection
selection = indexPath.row
// Perform Segue
performSegue(withIdentifier: «ListDetail», sender: self)
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// Fetch Destination View Controller
let addListViewController = segue.destination as!
// Configure View Controller
addListViewController.delegate = self
if let selection = selection {
// Fetch List
let list = lists[selection]
// Configure View Controller
addListViewController.list = list
}
}
|
Мы почти там. Когда выполняется переход с идентификатором ListDetail
, нам нужно настроить экземпляр AddListViewController
который AddListViewController
в стек навигации. Мы делаем это в prepareForSegue(_:sender:)
.
В результате мы AddListViewController
ссылку на контроллер представления назначения, экземпляр AddListViewController
. Мы устанавливаем свойство delegate
и, если список покупок обновляется, мы устанавливаем свойство list
контроллера представления для выбранной записи.
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: Segue Life Cycle
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
guard let identifier = segue.identifier else { return }
switch identifier {
case SegueListDetail:
// Fetch Destination View Controller
let addListViewController = segue.destinationViewController as!
// Configure View Controller
addListViewController.delegate = self
if let selection = selection {
// Fetch List
let list = lists[selection]
// Configure View Controller
addListViewController.list = list
}
default:
break
}
}
|
Создайте и запустите приложение, чтобы увидеть результат. Теперь вы сможете добавить новый список покупок и изменить название существующих списков покупок.
4. Удаление списков покупок
Добавление возможности удалять списки покупок не так уж много дополнительной работы. Пользователь должен иметь возможность удалить список покупок, проведя строку просмотра таблицы справа налево и нажав кнопку удаления, которая отображается. Чтобы сделать это возможным, нам нужно реализовать еще два метода протокола UITableViewDataSource
:
-
tableView(_:canEditRowAtIndexPath:)
-
tableView(_:commitEditingStyle:forRowAtIndexPath:)
Реализация tableView(_:canEditRowAtIndexPath:)
тривиальна, как вы можете видеть ниже.
1
2
3
|
func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
return true
}
|
В tableView(_:commitEditingStyle:forRowAtIndexPath:)
мы выбираем правильную запись из массива записей и вызываем deleteRecord(_:)
на контроллере представления, передавая запись, которую необходимо удалить.
1
2
3
4
5
6
7
8
|
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) { guard editingStyle == .delete else { return }
// Fetch Record
let list = lists[indexPath.row]
// Delete Record
deleteRecord(list)
}
|
Метод deleteRecord(_:)
должен выглядеть уже знакомым. Мы показываем индикатор прогресса и вызываем deleteRecordWithID(_:completionHandler:)
в частной базе данных контейнера по умолчанию. Обратите внимание, что мы передаем идентификатор записи, а не саму запись. Обработчик завершения принимает два аргумента: необязательный CKRecordID
и необязательный NSError
.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
private func deleteRecord(_ list: CKRecord) {
// Fetch Private Database
let privateDatabase = CKContainer.default().privateCloudDatabase
// Show Progress HUD
SVProgressHUD.show()
// Delete List
privateDatabase.delete(withRecordID: list.recordID) { (recordID, error) -> Void in
DispatchQueue.main.sync {
SVProgressHUD.dismiss()
// Process Response
self.processResponseForDeleteRequest(list, recordID: recordID, error: error)
}
}
}
|
В обработчике завершения мы отклоняем индикатор прогресса и вызываем processResponseForDeleteRequest(_:recordID:error:)
в основном потоке. В этом методе мы проверяем значения recordID
и error
которые дал нам CloudKit API, и соответственно обновляем message
. Если запрос на удаление был успешным, то мы обновляем пользовательский интерфейс и массив записей.
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: Error?) {
var message = «»
if let error = error {
print(error)
message = «We are unable to delete the list.»
} else if recordID == nil {
message = «We are unable to delete the list.»
}
if message.isEmpty {
// Calculate Row Index
let index = self.lists.index(of: record)
if let index = index {
// Update Data Source
self.lists.remove(at: index)
if lists.count > 0 {
// Update Table View
self.tableView.deleteRows(at: [NSIndexPath(row: index, section: 0) as IndexPath], with: .right)
} else {
// Update Message Label
messageLabel.text = «No Records Found»
// Update View
updateView()
}
}
} else {
// Initialize Alert Controller
let alertController = UIAlertController(title: «Error», message: message, preferredStyle: .alert)
// Present Alert Controller
present(alertController, animated: true, completion: nil)
}
}
|
Вот и все. Пришло время правильно протестировать приложение с некоторыми данными. Запустите приложение на устройстве или в iOS Simulator и добавьте несколько списков покупок. Вы должны иметь возможность добавлять, редактировать и удалять списки покупок.
Вывод
Несмотря на то, что эта статья довольно длинная, хорошо помнить, что мы только кратко взаимодействовали с API CloudKit. Удобный API-интерфейс платформы CloudKit является легким и простым в использовании.
Однако это руководство также проиллюстрировало, что ваша работа в качестве разработчика не ограничивается взаимодействием с API CloudKit. Важно обрабатывать ошибки, показывать пользователю, когда выполняется запрос, обновлять пользовательский интерфейс и сообщать пользователю, что происходит.
В следующей статье этой серии мы подробнее рассмотрим отношения, добавив возможность заполнять список покупок предметами. Пустой список покупок не очень полезен, и, конечно же, не весел. Оставьте любые свои вопросы в комментариях ниже или обратитесь ко мне в Twitter .