Core Data / 1.1. Основы Core Data



Футбол состоит из побед и поражений, главное — не останавливаться из-за трудностей.

Неймар

В этом уроке


Зачем нужна Core Data?

Раньше мы видели множество способов для хранения данных - текстовые файлы, plist, xml, json. Но все они имеют множество недостатков для хранения на устройстве пользователя, начиная от рутины по обработке и заканчивая низкой производительностью для больших объемов данных.

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

Есть несколько основных вариантов баз данных - SQL или реляционные (хотя понятия несинонимичны, они часто существуют бок-о-бок), NoSQL и базы данных на основе графов. Каждая из них имеет свои особенности и цели применения - нельзя сказать, что какая-то из них лучше другой (если говорить о конкретных продуктах, то старайтесь избегать поделки Microsoft под названием MS SQL Server).

В основе хранения данных на устройстве пользователя лежит SQLite-база данных. Она специально предназначена для интеграции в приложения и единичное использование - при оперировании ею с помощью Core Data доступ к базе данных будет иметь только Ваше приложение, размещаться она будет в папке Library песочницы, куда у пользователя нет доступа.


Core Data и Вы

Мы создадим приложение для списка дел - мы сильно упростим его по сравнению с изначальным вариантом, который мы уже делали и сохраняли данные на диск кустарными методами.

До iOS 10 пользователям приходилось работать напрямую со стэком Core Data - такая возможность имеется и сейчас, если Вам нужен больший контроль над всем этим, однако все элементы теперь объединены в очень удобную штучку - Persistent Container.

В AppDelegate мы добавим два удобных метода для извлечения контейнера данных:

lazy var persistentContainer: NSPersistentContainer = {

    let container = NSPersistentContainer(name: "SWCoreData1")
    //Имя контейнера должно совпадать с моделью данных, которые мы создадим

    //К модели подгружается само хранилище
    container.loadPersistentStores(completionHandler: { (storeDescription, error) in

        if let error = error as NSError? {
            fatalError("Unresolved error \(error), \(error.userInfo)")
        }
    })
    return container
}()

Казалось бы - очень простой метод. И что же может пойти не так? Ответ прост - почти всё:

В полифункциональных приложениях Вы вероятно захотите вынести функционал данных в отдельный сервис-класс.

Следующий метод позволит сохранить контекст в контейнер:

func saveContext () {
    let context = persistentContainer.viewContext
    //Контекст это среда, которая отвечает за координацию объектов и их сохранения

    if context.hasChanges {
        do {
            try context.save()
        } catch {
            let nserror = error as NSError
            fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
        }
    }
}

Этот метод может сломаться по тем же причинам.


Модель данных

  1. Нажмите CMD + N для создания нового файла
  2. Найдите секцию Core Data
  3. Выберите Data Model В качестве названия укажите SWCoreData1, что позволит приложению загружать модель.

Теперь перейдите в этот файл в инспекторе файлов:

В первое знакомство это окно может вызвать чувство страха.

Core Data умеет работать не только с базами данных, но и создавать хранилища в памяти, писать в XML (только на macOS), а также в бинарный формат. До тех пор, пока не поймёте, что Вы не решите проблему без указанных вещей - не используйте их.

Нажмите по плюсу у Add Entity. Это создаст новую запись в системе - запись можно считать эквивалентной классу, только с тем исключением, что она лежит в системе контроля данных, а управлять ей могут совсем другие классы (скорее всего Вам не придётся так извращаться).

Дайте ей название List - это будет список дел.

В центральной панели у нас есть 3 большие секции:

Давайте добавим атрибут name типа String в модель.
Есть множество типов данных для модели, которые прозрачно превращаются в те, что заложены в SQLite.

Заметьте, что здесь нет простого типа Int - в Objective-C он является примитивом, а числовые типы выражаются через знакомый нам NSNumber.

Справа нам доступен новый инспектор для моделей:

Теперь Выберите сам класс целиком:

Мы хотим показать, что происходит у неё под юбкой, а потому выберем ручное создание класса. Теперь Выберите класс и нажмите в верхнем меню Editor -> Create NSManagedObject Subclass...

Давайте посмотрим содержание двух файлов:

import Foundation
import CoreData

@objc(List)
public class List: NSManagedObject {

}

NSManagedObject - это специальный наследник NSObject, который позволяет объекту жить не просто в рантайме objc, но и в среде Core Data, которая управляет его поведением - такому объекту не нужно реализовывать многие свои методы и свойства - они будут добавлены на лету.

На данный момент кодогенерация даёт нам следующий второй файл:

import Foundation
import CoreData

extension List {

    @nonobjc  public class func fetchRequest() -> NSFetchRequest<List> {
        return NSFetchRequest<List>(entityName: "List")
    }

    @NSManaged  public var name: String?
}

И здесь для нас много нового - первый метод класса позволяет конструировать запросы к этому классу в базе данных, при этом он не экспортируется в objc - в ней уже есть метод с таким же названием, который не специализируется конкретным типом и возвращает голый объект, а в этом варианте C недоступна перегрузка функций.

Второй интересный момент - атрибут @NSManaged, который говорит системе, что это свойство управляется Core Data и будет добавлено на лету, а потому за него можно не волноваться.

Заметьте, что имя сгенерировалось опционалом (в большинстве случаев) - нам просто нужно убрать знак вопроса у строки. Это известное поведение кодогенерации и остаётся лишь надеяться, что его исправят.

И хотя Core Data использует в большинстве случаев под собой базы данных - она не является базой данных или системой управления баз данных. База данных SQLite лишь выступает хранилищем для объектов Core Data. Core Data - это граф объектов в памяти с их отношениями и свойствами. То, как он будет существовать в том или ином месте - решать Core Data. Думайте о ней как об ещё одном способе сохранения объектов. Для обычных SQL баз данных хорошим способом оптимизации выступает нормализация таблиц, но здесь она может вызвать обратный эффект.


Скелет приложения

Теперь необходимо создать скелет приложения, чтобы мы могли продемонстрировать Вам весь функционал Core Data.

Для этого:

  1. Встройте наш единственный контроллер в контроллер навигации
  2. Задайте ему заголовок To Do Lists
  3. Поместите на него табличное представление:
    • Задайте ему нулевые отступы от границ
    • Делегат и источник данных - контроллер
    • Аутлет tableView
  4. В таблице сделайте одну динамическую ячейку:
    • Стиль - с подзаголовком
    • Индикатор detail
    • Идентификатор ListCell

В его файле создадим несколько свойств для удобства:

//Самое приложение
let appDelegate = UIApplication.shared.delegate as! AppDelegate

//Контейнер
var container: NSPersistentContainer!
//Контекст
var context: NSManagedObjectContext!

В реальном приложении их лучше вынести в стек Core Data

А также массив для хранения списков дел, который при получении нового значения будет перезагружать таблицу:

var lists = [List]() {
    didSet {
        tableView.reloadData()
    }
}

Реализуйте протоколы источника и делегата табличного представления (делегат пока что будет пуст, а ячейка в качестве заголовка должна содержать название списка дел):

extension ViewController: UITableViewDataSource {

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return lists.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "ListCell", for: indexPath)
        let list = lists[indexPath.row]
        cell.textLabel?.text = list.name
        return cell
    }

}

extension ViewController: UITableViewDelegate {

}

Теперь пропишем одну специальную функцию, которая будет создавать запросы по извлечению данных:

func fetchData() {

    let request: NSFetchRequest<List> = List.fetchRequest()
    //Нужно явное указание типа, чтобы разбить неопределённость между методом класса и тем, что достался от objc

    do {
        lists = try context.fetch(request)
    } catch {
        print(error)
    }
}

Извлечение объектов может провалится по множеству причин, потому его надо соответственно перехватить.

Теперь во viewDidLoad мы добавим извлечение контейнера, контекста и данных:

override func viewDidLoad() {
    super.viewDidLoad()
    container = appDelegate.persistentContainer
    context = container.viewContext
    fetchData()
}

Добавление нового объекта в контекст

Все хорошо, но данных-то у нас нет и взяться им неоткуда. Создадим ещё одну форму для добавления списка дел.

Сперва создайте класс AddListTableViewController, унаследовав его от сами знаете чего. После его генерации удалите из него всю внутреннюю часть.

  1. На полотно перетащите Table View Controller
  2. Задайте ранее созданный класс
  3. На основном контролере создайте кнопку в правой части панели навигации со значком +
  4. От кнопки проведите show-сегвей к табличному контроллеру
  5. Задайте ему идентификатор ToAddList
  6. На том контроллере создайте кнопку Save с методом saveButtonPressed
  7. А заголовок Add List
  8. Переключите таблицу в статический режим
  9. Оставьте пока что лишь одну ячейку:
    1. Задайте ей высоту в 50 точек
    2. В неё поместите текстовое поле
    3. Его отступы должны быть нулевыми с учётом марджинов родительского представления
    4. Текст-заместитель - Name
    5. Аутлет - nameField

Для сохранения данных мы воспользуемся старым добрым методом с протоколом.

Создадим протокол для делегата в том же файле:

protocol AddListTableViewControllerDelegate: class {

func addListTableViewController(_ addListTableViewController: AddListTableViewController, didTappedSaveButtonForListWithName name: String)

}

Теперь создайте слабое свойство-делегат:

weak var delegate: AddListTableViewControllerDelegate?

А теперь закончим тело метода по сохранению объекта:

guard let name = nameField.text else {
    return
}

delegate?.addListTableViewController(self, didTappedSaveButtonForListWithName: name)

navigationController?.popViewController(animated: true)

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

Первый пункт Вы можете сделать самостоятельно:

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    if segue.identifier == "ToAddList", let controller = segue.destination as? AddListTableViewController {
        controller.delegate = self
    }
}

Второй же пункт несколько интереснее:

extension ViewController: AddListTableViewControllerDelegate {
    func addListTableViewController(_ addListTableViewController: AddListTableViewController, didTappedSaveButtonForListWithName name: String) {
        let list = List(context: context)
        list.name = name
        appDelegate.saveContext()
        fetchData()
    }
}

Каждый управляемый объект получает специальный конструктор для добавления в контекст, после чего нам нужно задать его свойства и сохранить контекст. Затем мы просто заново извлекаем данные.

Попробуйте создать свой список дел. Если же Вы зададите имя, меньшее 4 символов, то будет вызвана ошибка валидации примерно такого вида:

Thread 1: Fatal error: Unresolved error Error Domain=NSCocoaErrorDomain Code=1670 "The operation couldn’t be completed. (Cocoa error 1670.)" UserInfo={NSValidationErrorObject=<List: 0x6000002811d0> (entity: List; id: 0x60000022b720 <x-coredata:///List/t4471B718-67AD-4EA3-8CB8-7F5DD24AF5F44> ; data: {
    count = 0;
    name = 123;
    notes =     (
    );
}), NSLocalizedDescription=The operation couldn’t be completed. (Cocoa error 1670.), NSValidationErrorKey=name, NSValidationErrorValue=123}, ["NSValidationErro

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

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


Ограничения Core Data

Несмотря на всю свою красоту и мощь - Core Data очень слабо применима вне конечных приложений - её варианта нет для Linux-версии Swift, так как она построена поверх Objective-C и его элементов, а даже если бы она там и была - применять её для серверной части было бы неэффективно - SQLite хорошо подходит для лёгких приложений и встраиваемых систем, но не для поддержки больших сервисов.