Футбол состоит из побед и поражений, главное — не останавливаться из-за трудностей.
Раньше мы видели множество способов для хранения данных - текстовые файлы, plist, xml, json. Но все они имеют множество недостатков для хранения на устройстве пользователя, начиная от рутины по обработке и заканчивая низкой производительностью для больших объемов данных.
Решение пришло к нам из мира больших и стршных серверных технологий, обслуживающих миллионы пользователей в секунду, где перезапись больших блоков данных просто неприемлема. Решением являются баз данных, специальные системы по управлению данными, которые сами решают как разбить себя на конечном техническом устройстве. Часть из них позволяет даже разбить базу по разным устройствам в сети.
Есть несколько основных вариантов баз данных - SQL или реляционные (хотя понятия несинонимичны, они часто существуют бок-о-бок), NoSQL и базы данных на основе графов. Каждая из них имеет свои особенности и цели применения - нельзя сказать, что какая-то из них лучше другой (если говорить о конкретных продуктах, то старайтесь избегать поделки Microsoft под названием MS SQL Server).
В основе хранения данных на устройстве пользователя лежит SQLite-база данных. Она специально предназначена для интеграции в приложения и единичное использование - при оперировании ею с помощью Core Data доступ к базе данных будет иметь только Ваше приложение, размещаться она будет в папке Library песочницы, куда у пользователя нет доступа.
Мы создадим приложение для списка дел - мы сильно упростим его по сравнению с изначальным вариантом, который мы уже делали и сохраняли данные на диск кустарными методами.
До 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)")
}
}
}
Этот метод может сломаться по тем же причинам.
Data Model
В качестве названия укажите SWCoreData1
, что позволит приложению загружать модель.Теперь перейдите в этот файл в инспекторе файлов:
В первое знакомство это окно может вызвать чувство страха.
Core Data умеет работать не только с базами данных, но и создавать хранилища в памяти, писать в XML (только на macOS), а также в бинарный формат. До тех пор, пока не поймёте, что Вы не решите проблему без указанных вещей - не используйте их.
Нажмите по плюсу у Add Entity. Это создаст новую запись в системе - запись можно считать эквивалентной классу, только с тем исключением, что она лежит в системе контроля данных, а управлять ей могут совсем другие классы (скорее всего Вам не придётся так извращаться).
Дайте ей название List
- это будет список дел.
В центральной панели у нас есть 3 большие секции:
Attributes
- Атрибуты модели - схожи со свойствами объектовRelationships
- Отношения между моделямиFetched Properties
- специальные слабые отношения, которые позволяют подтягивать другие отношения в виде запроса - иногда они могут сильно Вас запутать, потому при необходимости не используйте их часто.Давайте добавим атрибут name
типа String
в модель.
Есть множество типов данных для модели, которые прозрачно превращаются в те, что заложены в SQLite
.
Заметьте, что здесь нет простого типа Int
- в Objective-C он является примитивом, а числовые типы выражаются через знакомый нам NSNumber.
Справа нам доступен новый инспектор для моделей:
Properties
- Вы можете сделать свойство опциональным (по-умолчанию все свойства такие) или же переходным transient
- такое свойство, хоть и управляется Core Data, не хранится в базе данных. Эти два пункта нельзя сочетать вместе. Уберите опциональность у имени списка дел.UserInfo
позволяет добавить к модели атрибуты, подставляемые во время выполнения.Теперь Выберите сам класс целиком:
Abstract Entity
- Можно сделать запись абстрактной, тогда она будет использоваться для создания других записей, но её объекты не будут созданы, ровно как и записаны на дискParent Entity
Классы в Core Data можно строить друг на основе друга - но наследование таких классов отличается от обычного - они будут жить в одной таблице в базе данных, при этом иметь общими полями все, что есть у всех моделей, просто они будут принимать нулевые значения. Потому наследуйте записи только при крайней необходимости, иначе столкнётесь с проблемой разреженных баз данных - когда таблицы будут иметь большое число NULL-значения.
NULL
- это аналог nil в мире баз данных, показывающий что какого-то значения нет.
Class Name
- имя класса может отличаться от имени записи в таблице.Codegen
- так как система построена поверх динамического диспатча Objective-C, то система может добавлять реализацию класса на лету. Доступно три варианта:Manual/None
- нужно вручную создать класс и его свойства или не создавать их вообще (но при обращении к такому классу будет ошибка)Class Definition
- будет создано два файла {Название класса}+CoreDataClass.swift
и {Название класса}+CoreDataProperties
. Первый будет содержать само определение класса и его экспозицию в objc, а второй свойства Core DataCategory/Extension
- будет создана лишь категория или расширения для класса, что позволит ему управляться через неё, не меняя базового класса.Мы хотим показать, что происходит у неё под юбкой, а потому выберем ручное создание класса. Теперь Выберите класс и нажмите в верхнем меню 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.
Для этого:
To Do Lists
tableView
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
, унаследовав его от сами знаете чего. После его генерации удалите из него всю внутреннюю часть.
+
ToAddList
Save
с методом saveButtonPressed
Add List
Name
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 очень слабо применима вне конечных приложений - её варианта нет для Linux-версии Swift, так как она построена поверх Objective-C и его элементов, а даже если бы она там и была - применять её для серверной части было бы неэффективно - SQLite хорошо подходит для лёгких приложений и встраиваемых систем, но не для поддержки больших сервисов.