Статьи

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

В первом уроке этой серии мы изучили инфраструктуру и инфраструктуру CloudKit. Мы также заложили основу для примера приложения, которое мы собираемся создать, приложения для списка покупок. В этом уроке мы сосредоточены на добавлении, редактировании и удалении списков покупок.

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

В этом уроке мы продолжим работу с проектом, который мы создали в первом уроке . Вы можете скачать его с GitHub (тег add_records ).

Приложение списка покупок будет использовать библиотеку SVProgressHUD , популярную библиотеку, созданную Сэмом Верметтом, которая позволяет легко отображать индикатор прогресса. Вы можете добавить библиотеку вручную в свой проект, но я настоятельно рекомендую использовать CocoaPods для управления зависимостями. Вы новичок в CocoaPods? Прочитайте это вводное руководство для CocoaPods, чтобы освоиться.

Откройте 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 проекта. Замените списки именем вашей цели, если ваша цель названа по-другому.

Откройте терминал , перейдите к корню вашего проекта XCode и запустите pod install . Это сделает для вас несколько вещей, таких как установка зависимостей, указанных в Podfile, и создание рабочей области Xcode.

После завершения установки CocoaPods закройте проект и откройте рабочую область CocoaPods, созданную для вас. Последнее очень важно. Откройте рабочее пространство, а не проект . Рабочая область включает два проекта: проект Lists и проект с именем Pods .

Мы готовы переориентироваться на платформу CloudKit. Во-первых, однако, мы должны сделать некоторые домашние хозяйства, переименовав Класс ListsViewController класса ListsViewController .

Начните с переименования ViewController.swift в ListsViewController.swift . Откройте ListsViewController.swift и измените имя Класс ListsViewController класса ListsViewController .

Затем откройте Main.storyboard , разверните View Scene Scene в Outline документа слева и выберите View Controller . Откройте Identity Inspector справа и измените Class на ListsViewController .

Откройте Identity Inspector справа и измените класс на ListsViewController

Когда пользователь открывает приложение, ему представляется список покупок. Мы будем отображать списки покупок в виде таблицы. Давайте начнем с настройки пользовательского интерфейса. Выберите Контроллер Представления Списков в Сцене Контроллера Представления Списков и выберите Вставить> Контроллер навигации из меню Редактора XCode.

Добавьте табличное представление в представление контроллера представления и создайте необходимые ограничения макета для него. Выбрав табличное представление, откройте инспектор атрибутов и установите ячейки прототипа в 1 . Выберите ячейку прототипа и установите для стиля значение Basic, а для идентификатора значение ListCell .

Выберите ячейку прототипа и установите для стиля значение Basic, а для идентификатора значение ListCell.

Выбрав табличное представление, откройте инспектор соединений . Подключите источник данных табличного представления и delegate выходы к контроллеру представления списков .

Хотя мы только создаем пример приложения, чтобы проиллюстрировать, как работает CloudKit, я хотел бы отобразить сообщение, если что-то идет не так или если в iCloud не было найдено списков покупок. Добавьте метку к контроллеру представления, сделайте ее такой же большой, как вид контроллера представления, создайте для нее необходимые ограничения макета и отцентрируйте текст метки.

Поскольку мы имеем дело с сетевыми запросами, я также хочу отображать представление индикатора активности, пока приложение ожидает ответа от iCloud. Добавьте представление индикатора активности в представление контроллера представления и центрируйте его в его родительском представлении. В инспекторе атрибутов установите флажок «Скрывать при остановке» .

Контроллеры Списка Представлений

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

Вернитесь к раскадровке и соедините розетки с соответствующими представлениями в сцене контроллера списков .

Прежде чем мы получим данные из 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
    }
 
}

Нам нужно сделать еще один шаг: подготовить пользовательский интерфейс. В методе 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. Если вы выполнили вышеуказанные шаги, вы должны увидеть пустое представление с представлением индикатора вращающейся активности в центре.

Занят, притворяясь, что получает данные

Прежде чем мы получим какие-либо записи, нам нужно создать тип записи для списка покупок на панели инструментов CloudKit. Информационная панель CloudKit — это веб-приложение, которое позволяет разработчикам управлять данными, хранящимися на серверах Apple iCloud.

Выберите проект в Навигаторе проектов и выберите цель Списки из списка целей. Откройте вкладку « Возможности » вверху и разверните раздел iCloud . Ниже списка контейнеров iCloud нажмите кнопку с надписью CloudKit Dashboard .

Открыть CloudKit Dashboard

Войдите в свою учетную запись разработчика и убедитесь, что приложение Lists выбрано в левом верхнем углу. Слева выберите « Типы записей» в разделе « Схема ». Каждое приложение по умолчанию имеет тип записи « Пользователи ». Чтобы создать новый тип записи, нажмите кнопку «плюс» в верхней части третьего столбца. Мы будем следовать соглашению об именовании Apple и назовем тип записи « Списки» , а не « Список» .

Добавление нового типа записи

Обратите внимание, что первое поле создается автоматически. Создать поле   имя и установите тип поля в строку . Не забудьте нажать кнопку « Сохранить» внизу, чтобы создать тип записи « Списки ». Мы вернемся к инструментальной панели CloudKit позже в этой серии.

Затем включите индексирование для свойства вашего документа, перейдя на вкладку « Индексы » и добавив новый SORTABLE и другой тип индекса QUERYABLE для имени, и нажмите « Сохранить» .

Добавление SORTABLE и QUERYABLE индексации

Наконец, перейдите на вкладку « РОЛЬ БЕЗОПАСНОСТИ » и, в целях данного упражнения, установите все флажки, чтобы убедиться, что у вашего пользователя есть доступ к таблице.

С созданным типом записи 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()
   }

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

Записей не найдено

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

Откройте Main.storyboard и добавьте контроллер представления в раскадровку. С выбранным контроллером представления, откройте Identity Inspector справа и установите Class в AddListViewController .

Добавление View Controller и установка его в AddListViewcontroller

Пользователь сможет перейти к контроллеру добавления списка, нажав кнопку в контроллере просмотра списка.

Перетащите элемент панели кнопок из библиотеки объектов на панель навигации контроллера представления списков . Выбрав элемент панели кнопок, откройте инспектор атрибутов и установите для параметра « Система» значение « Добавить» . Нажмите Control и перетащите от элемента кнопки панели к добавлению контроллера представления списка и выберите Show Detail в появившемся меню.

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

Добавление ListDetail для инспектора segue

Добавьте два элемента панели кнопок на панель навигации контроллера представления списка, один слева и один справа. Установите Системный элемент для элемента левой панели на Отмена, а для элемента на правой панели — Сохранить . Наконец, добавьте текстовое поле в контроллер представления списка добавления. Отцентрируйте текстовое поле и установите его выравнивание по центру в инспекторе атрибутов .

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

Наконец, подключите выходы и действия, созданные в AddListViewController.swift, к соответствующим элементам пользовательского интерфейса в сцене.

Прежде чем мы реализуем класс 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
       }
   }

Действие 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()
}

В классе 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
    }
}

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

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