Core Data: Часть 1

08 сентября 2015

В этом туториале вы получите базовые представления о Core Data и напишите ваше самое первое приложение с его использованием. Вы увидите как несложно начать использовать Core Data при помощи ресурсов Xcode, а к концу этого туториала вы уже будете знать как:

  1. создавать модели данных, которые вы хотите хранить в Core Data, используя Xcode редактор моделей;
  2. добавлять новые записи в Core Data;
  3. получать записи из Core Data;
  4. отображать полученные записи из Core Data в table view.

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

Поехали!

Открывайте Xcode и создайте новый проект iPhone, основанный на шаблоне Single View Application. Назовите его HitList и поставьте галочку напротив Use Core Data:

Галочка, поставленная напротив Use Core Data говорит Xcode создать шаблонный код, который известен как Core Data Stack в AppDelegate.swift. Core Data Stack состоит из набора объектов, которые облегчают сохранение и получение информации из Core Data.

Заметка

Не все шаблоны Xcode имеют опцию использования Core Data. В Xcode 6 этими шаблонами являются только Master-Detail Application и Single View Application.

Идея этого приложения очень проста. Это будет table view с вашим личным списком хитов (или имен). Вы сможете добавлять имена в этот список, используя Core Data, чтобы ваши данные были сохранены между сессиями.

Кликните на Main.storyboard, чтобы открыть его в interface builder. Выберите его единственный View Controller и измените его size class на Regular Height и Compact Width, чтобы он соответствовал портретному режиму iPhone.

Далее вставьте view controller в navigation controller. В Editor menu выберите Embed In...\ Navigation Controller.

Затем перетащите Table View из библиотеки объектов на ваш view controller так, чтобы он полностью перекрывал его.

После, перетащите из библиотеки Bar Button Item и разместите его на панели навигации (navigation bar) вашего view controller. Далее сделайте дабл-клик на добавленной кнопке, чтобы изменить ее имя и запишите Add. Теперь ваш холст должен выглядеть вот так:

Каждый раз, когда вы будете нажимать Add, в правом верхнем углу будет появляться окно, в котором вы сможете вписывать новое название в текстовое поле. При нажатии на это уведомление ваши данные будут сохраняться, а table view обновляться, отображая последние внесенные изменения.

До того, как вы сможете это сделать, вам нужно сделать ваш view controller как источник данных (data source) для вашего table view. Зажмите CTRL и перетащите курсор от table view на ваш view controller и отпустите. В появившемся окне выберите dataSource:

Если вам интересно, то мы не будем устанавливать ваш view controller в качестве делегата, так как у нас не будет происходить никаких действий при нажатии на ячейку таблицы. Проще не придумаешь!

Откройте Assistant Editor: либо просто нажав на центральную кнопку в панели инструментов редактора, либо, просто нажав Command+Option+Enter. Зажмите CTRL и перетащите от table view в ViewController.swift, внутри которого создайте IBOutlet с именем tableView. В итоге у вас должно получиться вот что:

@IBOutlet weak var tableView: UITableView!

С зажатым CTRL перетащите от кнопки Add в ViewController.swift, но в этот раз создайте метод IBAction, и название метода как addName:

@IBAction func addName(sender: AnyObject) {
  }

Теперь вы можете ссылаться на ваш table view и на вашу кнопку в вашем коде. Далее мы настроим модель для вашего table view. Добавьте следующее свойство в ViewController.swift:

//вставьте его под IBOutlet вашего tableView
  var names = [String]()

Массив names, который будет отображаться в table view.

Замените реализацию метода viewDidLoad:

override func viewDidLoad() {
    super.viewDidLoad()
    title = "\"The List\""
    tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: "Cell")
  }

Этот код установит заголовок и установит класс, который будет использован для ячеек. Вы это делаете, когда ваша ячейка выходит из очереди, и table view возвращает ячейку корректного типа.

Все так же в ViewController.swift объявите, что ViewController подписан под протокол UITableViewDataSource, изменив объявление класса:

//Добавьте UITableViewDataSource к объявлению класса
class ViewController: UIViewController, UITableViewDataSource {

Сразу же вы получите ошибку, что ваш ViewController не соответствует протоколу.

Под viewDidLoad реализуйте методы, чтобы исправить ошибку:

// MARK: UITableViewDataSource
func tableView(tableView: UITableView,
    numberOfRowsInSection section: Int) -> Int {
    return names.count
}
 
func tableView(tableView: UITableView,
  cellForRowAtIndexPath
  indexPath: NSIndexPath) -> UITableViewCell {
 
    let cell = tableView.dequeueReusableCellWithIdentifier("Cell") as! UITableViewCell
 
    cell.textLabel!.text = names[indexPath.row]
 
  return cell
}

Если вы когда-либо работали с UITableView, то этот код покажется вам очень знакомым. Первый метод говорит нам о том, что эта таблица будет иметь столько строк, сколько имен у нас находится в массиве names.

Второй метод tableView(_:cellForRowAtIndexPath:)вытаскивает из очереди ячейки таблицы и заполняет их соответствующими значениями из массива names.

Пока не запускайте приложение. Сначала вам нужно задать способ ввода значений, чтобы table view мог отобразить их.

Реализуем addName IBAction, которые вы внесли в ваш код ранее, путем перетаскивания (с зажатым CTRL).

//Реализация addName IBAction
@IBAction func addName(sender: AnyObject) {
 
  var alert = UIAlertController(title: "New name",
      message: "Add a new name",
      preferredStyle: .Alert)
 
  let saveAction = UIAlertAction(title: "Save",
   style: .Default) { (action: UIAlertAction!) -> Void in
 
    let textField = alert.textFields![0] as! UITextField
    self.names.append(textField.text)
    self.tableView.reloadData()
  }
 
  let cancelAction = UIAlertAction(title: "Cancel",
    style: .Default) { (action: UIAlertAction!) -> Void in
  }
 
  alert.addTextFieldWithConfigurationHandler {
    (textField: UITextField!) -> Void in
  }
 
  alert.addAction(saveAction)
  alert.addAction(cancelAction)
 
  presentViewController(alert,
      animated: true,
      completion: nil)
}

Каждый раз, когда вы нажимаете Add, срабатывает этот метод, который вызывает UIAlertController с текстовым полем и двумя кнопками: Save и Cancel. Кнопка Save принимает текст, который находится в текстовом поле и сохраняет его в массив names, а затем перегружает table view. Так как names является частью table view, то все, что вы напечатаете в этом текстовом поле, попадет в table view.

Теперь время запустить ваше приложение в первый раз, нажмите кнопку Add. Должен выскочить alert controller, который будет выглядеть вот так:

Добавьте несколько имен в список. В итоге у вас должно получиться что-то вроде этого:

Ваш table view будет отображать данные, а массив хранить их, но здесь не хватает постоянства - если вы экстренно выйдете из приложения или перезагрузите свое устройство, ваш список снова станет пустым.

Core Data предоставляет это постоянство, что означает, что ваши данные могут храниться более надежно, и они сохранятся, даже если вы перезагрузите ваше устройство. Вы еще не добавили ничего из Core Data, так что все ваши данные должны стереться, если вы выйдете из приложения. Давайте это проверим: нажмите кнопку "Home", если вы тестируете это на реальном устройстве или «P», если вы используете симулятор. Это вернет вас на главный экран:

Теперь нажмите на HitList и вы увидите, что ваши данные все еще хранятся в приложении. Но как же так?

Когда вы нажимаете кнопку "Home" ваше приложение уходит на задний план. В этот момент операционная система запоминает все, что находится на данный момент в памяти, включая и ваш массив с именами. Аналогично, когда вы нажимаете на ваше приложение, но оно возвращается на передний план со всем хранящимися данными.

Apple анонсировала эти преимущества мультизадачности еще в iOS 4. Это, конечно, создает приятное впечатление для пользователей, но так же добавляет некоторые сложности для разработчиков под iOS. Но сохранился ли наш массив на самом деле?

На самом деле - нет. Если вы окончательно закрыли приложение или просто выключили телефон, то ваши имена просто исчезнут. Вы можете это проверить: нажмите быстро два раза на кнопку "Home", чтобы у вас получилось вот так:

Переместите ваше приложение наверх, чтобы закрыть его. Отныне, вашего HitList в памяти не будет. Теперь перезапустите ваше приложение и вы убедитесь в том, что все имена, которые вы ввели, отсутствуют.

Если вы работали некоторое время с iOS, то вам наверняка знакома разница между flash-freezing (флеш-заморозка) и постоянным хранением, а так же сама работа мультизадачности. В понимании пользователей здесь нет никакой разницы. Для пользователя понятия "данные пока что еще здесь" и "данные сохранены" никакой разницы не имеют, так как есть доступ к данным.

Так что реальное присутствие значений мы будем проверять после нового запуска приложения.

Создаем модель ваших данных

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

До этого момента вы использовали старые добрые строки, для хранения имен в памяти, теперь же мы заменим эти строки на объекты Core Data.

Сперва мы создадим managed object model, которая определяет как Core Data отображает данные на диске. По умолчанию Core Data использует базу данных SQLite, как постоянное хранилище. Так что вы можете считать модель данных той же схемой базы данных.

Заметка

Вы будете время от времени пересекаться со словом "managed" (что значит управляемый) в этом туториале. Если вы увидите его в имени класса, например NSManagedObjectContext, то скорее всего вы имеете дело с Core Data. Слово "Managed" относится к управлению жизненного цикла объектов Core Data.

Однако, не думайте что все классы Core Data содержат это слово. Если при создании приложения вы поставили галочку напротив использования Core Data, то Xcode автоматически создал для вас HitList.xcdatamodeld.

Нажмите на HitList.xcdatamodeld, чтобы посмотреть что внутри. Как вы можете видеть, в Xcode есть очень мощный редактор модели данных, который выглядит вот так:

В этом редакторе есть много свойств и особенностей, но сейчас давайте сфокусируемся на создании единственной сущности - Core Data.

Нажмите на Add Entity в левом нижнем углу и создайте «Новую сущность». Сделайте двойной щелчок на «Сущности», чтобы изменить имя и напечатайте Person. У вас получится вот это:

Наверное вам интересно почему редактор использует определение "Entity" (сущность). Почему бы просто не назвать это новым классом? Все очень просто - в Core Data есть свой собственный словарик. Вот некоторые из терминов, которые использует Core Data:

  1. entity (сущность) - определение класса в Core Data. Классический пример Employee или Company. В отношении базы данных сущность соответствует таблице.
  2. attribute (атрибут) - кусок информации, прикрепленный к данной сущности. Например, сущность Employee может иметь такие атрибуты как: имя, должность и жалование. В базе данных атрибут соответствует ячейке таблицы.
  3. relationship (связь) - соединение между несколькими сущностями. В Core Data связи, находящиеся между двумя сущностями называются to-one связи, а между несколькими сущностями - to-many связями. Например, Manager может иметь to-many связи с несколькими Employee, в то время как каждый Employee имеет to-one связь с Managed.

Заметка

Как вы наверное заметили, сущности похожи на классы. В то время как атрибуты/связи напоминают нам свойства. Так какая же разница? В Core Data вы можете считать сущность определением класса, а управляемые объекты (managed objects) экземплярами класса.

Теперь, когда вы понимаете как атрибуты соотносятся с моделью, давайте добавим атрибут для Person. Выберите Person с левой стороны и нажмите значок (+) под Attributes.

Установите имя атрибута на name, а тип на String:

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

Сохранение в Core Data

Импортируйте модуль Core Data вверху ViewController.swift:

//Добавьте под "import UIKit"
import CoreData

Возможно вам приходилось подключать фреймворки вручную во время фазы Build, когда вы работали с Objective-C. Но со Swift ключевого слова import вполне достаточно, чтобы начать использовать Core Data API в вашем коде.

Затем замените модель table view на следующую:

//Измените [String] на [NSManagedObject]
var name = [NSManagedObject]()

Вы будете хранить сущности Person, а не просто их имена, так что переименуйте массив names, который служит вам моделью данных, на people. Теперь он содержит сущности NSManagedObject, а не просто String. NSManagedObject отображает единственный объект, хранимый в Core Data. Вы должны использовать его для создания, редактирования, сохранения, удаления из вашего постоянного хранилища Core Data. Как вы вскоре сами убедитесь NSManagedObject может менять свою форму. Он может принимать любую форму сущности в вашей модели данных, присваивая все атрибуты и связи, которые вы определили.

//замените оба метода UITableViewDataSource
func tableView(tableView: UITableView,
    numberOfRowsInSection section: Int) -> Int {
    return people.count
}
 
func tableView(tableView: UITableView,
  cellForRowAtIndexPath
  indexPath: NSIndexPath) -> UITableViewCell {
 
    let cell = tableView.dequeueReusableCellWithIdentifier("Cell") as! UITableViewCell
 
    let person = people[indexPath.row]
    cell.textLabel!.text = person.valueForKey("name") as? String
 
    return cell
}

Самые большие изменения произошли в методе cellForRowAtIndexPath. Вместо заполнения ячеек соответствующими строками, теперь мы заполняем их соответствующими NSManagedObject.

Обратите внимание как вы берете атрибут из NSManagedObject. Это происходит здесь:

cell.textLabel!.text = person.valueForKey("name") as? String

Почему вам это нужно делать? Так получается, что NSManagedObject не имеет представления об атрибуте name, который вы определи в модели данных, таким образом мы не можем напрямую к нему обратиться. Единственный способ, который предоставляет нам Core Data для чтения - это KVC (key-value coding).

Заметка:

Если вы только начали программировать для iOS, то возможно вы не знакомы с KVC. KVC - механизм в Cocoa и Cocoa Touch для доступа к свойствам непрямым путем через использование строк, для определения свойств. В этом случае KVC заставляет вести себя NSManagedObject как словарь. KVC - механизм доступный всем классам, которые произошли от NSObject, включая и NSManagedObject. У вас не получится использовать KVC для доступа к свойствам в Swift, для объектов, которые произошли не от NSObject.

Теперь, давайте заменим действие Save в методе addName @IBAction следующим:

let saveAction = UIAlertAction(title: "Save",
 style: .Default) { (action: UIAlertAction!) -> Void in
 
  let textField = alert.textFields![0] as! UITextField
  self.saveName(textField.text)
  self.tableView.reloadData()
}

Теперь мы передаем текст из текстового поля, при помощи нового метода saveName. Добавьте saveName @IBAction к ViewController.swift как показано ниже:

func saveName(name: String) {
  //1
  let appDelegate =
  UIApplication.sharedApplication().delegate as! AppDelegate
 
  let managedContext = appDelegate.managedObjectContext!
 
  //2
  let entity =  NSEntityDescription.entityForName("Person",
    inManagedObjectContext:
    managedContext)
 
  let person = NSManagedObject(entity: entity!,
    insertIntoManagedObjectContext:managedContext)
 
  //3
  person.setValue(name, forKey: "name")
 
  //4
  var error: NSError?
  if !managedContext.save(&error) {
      println("Could not save \(error), \(error?.userInfo)")
  }  
  //5
  people.append(person)
}

Тут у нас и появляется Core Data! Вот что делает код:

  1. До того как вы сможете что-либо сохранить или получить из вашей Core Data, вам все еще нужно поработать с NSManagedObjectContext. Считайте, что управляемый контекст объекта - это "блокнот" для работы с управляемыми объектами.
  2. Считайте, что сохранение нового управляемого объекта в Core Data двухступенчатым процессом. Первая ступень - внесение вашего управляемого объекта в контекст. Вторая ступень наступает после того, как вы убедились в том, что ваш объект такой, каким вы хотите его видеть. Далее вы сохраняете изменения в вашем контексте управляемого объекта на диск.
  3. Xcode уже сгенерировал контекст управляемого объекта как часть шаблона проектирования, но это происходит только в том случае, если вы поставили галочку напротив пункта Use Core Data. Этот контекст управляемого объекта живет в качестве свойства делегата приложения. Для того чтобы получить доступ к нему, вам сначала нужно сослаться на делегата приложения.
  4. Вы создаете новый управляемый объект и вставляете его в контекст. Вы можете сделать это за один шаг, при помощи назначенного инициализатора NSManagedObject: init(entity:insertIntoManagedObjectContext:).
  5. Вам может быть интересно, что такое NSEntityDescription. Вспомните, что немного ранее мы говорили о том, что NSManagedObject является классом, который может менять форму и может отображать любую сущность. Описание сущности - фрагмент, который соединяет определение сущности из вашей модели данных с экземпляром NSManagedObject во время исполнения.
  6. При помощи NSManagedObject вы можете установить атрибут name, используя KVC. Вы должны написать ваш ключ KVC ("name" в нашем случае) точно так, как он появляется в модели данных. В противном случае ваше приложение прекратит работу во время исполнения.
  7. Вы подтверждаете свои изменения в person и сохраняете их на диск, вызывая метод save у контекста управляемого объекта. Обратите внимание, что save принимает один параметр, который является указателем NSError. Если когда-нибудь произойдет ошибка сохранения, то вы можете ее инспектировать и уведомить о ней пользователя, если в этом есть такая необходимость.
  8. Поздравляем! Ваш новый управляемый объект был расположен на постоянное хранение в Core Data. Внесите еще один новый управляемый объект в массив people, чтобы он отобразился после перезагрузки table view.

Это немного сложнее чем массив строк, но не все так плохо. Некоторый код (получение контекста управляемого объекта и сущности) мог бы быть помещен в ваш собственный метод init или в viewDidLoad, откуда мог бы быть снова использован. Для простоты вы делаете это все в одном методе и за один раз.

Запустите ваше приложение и добавьте в него несколько имен:

Заметка

Если ваше приложение прекратило работу из-за ошибки, когда вы пытались добавить первое имя, то проверьте еще раз, переименовали ли вы в Core Data: Entity в Person, Person в HitList.xcdatamodeld. Если так оно и есть, то удалите приложение с симулятора и запустите его заново, но только после того, как вы поменяете имя на Person.

Если файлы фактически хранятся в Core Data, то наше приложение HitList должно пройти тест на сохранность данных. Дважды нажмите на "Home" и завершите работу нашего приложения.

Затем запустите его заново. Что случилось? Ваша table view пуста:

Вы сохранили ваши данные в Core Data, но после повторного запуска приложения массив people пуст! На самом деле данные сидят и ждут, но вы их пока не получили.

Получение из Core Data

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

:

override func viewWillAppear(animated: Bool) {
  super.viewWillAppear(animated)
 
  //1
  let appDelegate =
  UIApplication.sharedApplication().delegate as! AppDelegate
 
  let managedContext = appDelegate.managedObjectContext!
 
  //2
  let fetchRequest = NSFetchRequest(entityName:"Person")
 
  //3
  var error: NSError?
 
  let fetchedResults =
    managedContext.executeFetchRequest(fetchRequest,
    error: &error) as? [NSManagedObject]
 
  if let results = fetchedResults {
    people = results
  } else {
    println("Could not fetch \(error), \(error!.userInfo)")
  }
} 

Разберем этот код пошагово:

  1. Как мы говорили в прошлой секции, до того как вы сможете делать что-либо с Core Data, вам нужен контекст управляемого объекта. Вот вы его и получаете.
  2. Класс NSFetchRequest своим именем подсказывает нам, что он ответственен за получение данных из Core Data. Запрос на получение данных - очень гибкий и мощный инструмент. Вы можете построить запрос таким образом, что получите данные, удовлетворяющие определенным критериям. Например, вы можете написать: «Выдай-ка нам всех работников, работающих в Москве, и которые не меняли работу по меньшей мере 3 года».
  3. Запросы на получение имеют квалификаторы, которые уточняют результаты, возвращаемые ими. На данный момент вам нужно знать, что NSEntityDescription является одним из таких квалификаторов (обязательный).
  4. Инициализируя с помощью метода, init(entityName:) возвращает нам все объекты конкретной сущности. Это именно то, что мы сделали, чтобы получить все сущности Person.
  5. Вы передаете запрос в контекст управляемого объекта, чтобы он сделал всю работу.
  6. executeFetchRequest(_:error:) возвращает опциональный массив управляемых объектов, которые удовлетворяют критерию, заданному в запросе.

Заметка

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

Запустите ваше приложение еще раз. Теперь вы увидите все ранее введенные вами имена (что-то типа "asd", "qwe" и т.д.):

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

Здесь вы можете скачать наш конечный проект!

Что дальше?

Дальше, вы можете продолжить изучать наши туториалы по мере их появления, а также, параллельно читать перевод официальной книги по языку программирования Swift. И, для более подробного изучения языка, вы можете пройти наши курсы!

Урок подготовил: Иван Акулов

Источник урока

Содержание