Статьи

Основные данные и Swift: создание подклассов NSManagedObject

Ранее в этой серии мы создали простое приложение Done , чтобы узнать больше о классе NSFetchedResultsController . В этом проекте мы использовали кодирование значения ключа (KVC) и наблюдение значения ключа (KVO) для создания и обновления записей. Это прекрасно работает, но с того момента, как ваш проект будет иметь какую-либо сложность, вы быстро столкнетесь с проблемами. Синтаксис KVC не только многословен, valueForKey(_:) и setValue(_:forKey:) , но и может привести к ошибкам, которые являются результатом опечаток. Следующий фрагмент кода хорошо иллюстрирует эту проблему.

1
2
3
4
record.setValue(NSDate(), forKey: «createdat»)
record.setValue(NSDate(), forKey: «CreatedAt»)
record.setValue(NSDate(), forKey: «createdAt»)
record.setValue(NSDate(), forKey: «CREATEDAT»)

Каждый оператор в приведенном выше фрагменте кода возвращает свой результат. Фактически, каждый оператор приведет к исключению, кроме третьего, который использует правильный ключ, указанный в модели данных.

Вышеуказанная проблема легко решается с помощью строковых констант, но я не пытаюсь это сделать. Кодирование значения ключа — это здорово, но оно многословно и его трудно читать, если вы привыкли к точечному синтаксису Swift. Чтобы упростить работу с экземплярами NSManagedObject , лучше создать подкласс NSManagedObject для каждой сущности модели данных, и об этом вы узнаете в этой статье.

Чтобы сэкономить время, мы вернемся к Done , приложению, которое мы создали ранее в этой серии. Загрузите его с GitHub и откройте в Xcode.

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

Откройте модель данных проекта Done.xcdatamodeld и выберите объект Item . Выберите New> File … из меню File Xcode, выберите шаблон подкласса NSManagedObject из раздела Core Data и нажмите Next .

Создание NSManagedObject Subclass

Установите флажок правильной модели данных, Готово , в списке моделей данных и нажмите Далее .

Выбор модели данных

На следующем шаге вас попросят выбрать сущности, для которых вы хотите создать подкласс NSManagedObject . Проверьте флажок объекта Item и нажмите Next .

Выбор сущностей

Выберите расположение для хранения файлов классов подкласса NSManagedObject и убедитесь, что установлен флажок Использовать скалярные свойства для примитивных типов данных . При работе с Objective-C этот параметр является важным фактором. Но для Swift лучше всего проверить это, поскольку он делает ваш код более читабельным и менее сложным. Не забудьте установить для языка значение Swift и нажать кнопку « Создать», чтобы создать подкласс NSManagedObject для объекта Item .

Настройка подкласса NSManagedObject

Перейдите к файлам Xcode, созданным для вас, и посмотрите на их содержимое. Xcode должен создать для вас два файла:

  • Item.swift
  • Пункт + CoreDataProperties.swift

Если вы используете более раннюю версию Xcode, возможно, Xcode создал только один файл. Я рекомендую использовать Xcode 7 — предпочтительно Xcode 7.1 или выше — чтобы следовать. Содержимое Item.swift должно быть довольно легко понять.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
//
// Item.swift
// Done
//
// Created by Bart Jacobs on 24/10/15.
// Copyright © 2015 Envato Tuts+.
//
 
import Foundation
import CoreData
 
class Item: NSManagedObject {
 
// Insert code here to add functionality to your managed object subclass
 
}

Как вы можете видеть, Xcode создал для нас класс Item , который наследуется от NSManagedObject . Комментарий, который XCode добавил к реализации класса, важен. Если вы хотите добавить функциональность в класс, вы должны добавить его здесь. Давайте теперь посмотрим на содержимое Item + CoreDataProperties.swift .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
//
// Item+CoreDataProperties.swift
// Done
//
// Created by Bart Jacobs on 24/10/15.
// Copyright © 2015 Envato Tuts+.
//
// Choose «Create NSManagedObject Subclass…» from the Core Data editor menu
// to delete and recreate this implementation file for your updated model.
//
 
import Foundation
import CoreData
 
extension Item {
 
    @NSManaged var createdAt: NSTimeInterval
    @NSManaged var done: Bool
    @NSManaged var name: String?
 
}

Есть ряд важных деталей и несколько новых концепций. Первое, на что нужно обратить внимание, это то, что этот файл определяет расширение класса Item . Расширение объявляет три свойства, которые соответствуют атрибутам сущности Item , которые мы определили в модели данных. @NSManaged аналогичен @dynamic в Objective-C. @NSManaged сообщает компилятору, что хранение и реализация этих свойств будут обеспечиваться во время выполнения. Хотя это может звучать замечательно, в документации Apple четко говорится, что @NSManaged следует использовать только в контексте Core Data.

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

Что произойдет, если вы измените сущность и снова NSManagedObject файлы для подкласса NSManagedObject ? Это хороший вопрос, и XCode предупреждает вас об этом сценарии. В верхней части Item + CoreDataProperties.swift , ниже заявления об авторских правах, Xcode добавил комментарий для пояснения.

1
2
// Choose «Create NSManagedObject Subclass…» from the Core Data editor menu
// to delete and recreate this implementation file for your updated model.

Всякий раз, когда вы генерируете файлы для объекта, XCode будет заменять только файлы с расширением. Другими словами, если вы добавляете атрибут к сущности Item и генерируете файлы для подкласса NSManagedObject , Xcode заменяет Item + CoreDataProperties.swift , оставляя Item.swift нетронутым. Вот почему Xcode говорит вам добавить функциональность в Item.swift . Это очень важно иметь в виду.

Типы свойств могут быть немного удивительными. Свойство name имеет тип NSString? потому что атрибут помечен как необязательный в модели данных. Свойство createdAt имеет тип NSTimeInterval , поскольку мы установили флажок Использовать скалярные свойства для примитивных типов данных . То же самое верно для свойства done , которое имеет тип Bool .

Если бы мы не установили флажок, свойство createdAt будет иметь тип NSDate? а свойство done будет иметь тип NSNumber? , Хотя может быть проще использовать экземпляры NSDate , но, безусловно, менее практично работать с объектами NSNumber если все, что вам нужно, это получить и установить логические значения.

Когда класс Item готов к использованию, пришло время обновить проект, заменив все вхождения valueForKey(_:) и setValue(_:forKey:) .

Откройте файл ViewController класса ViewController , перейдите к configureCell(_:atIndexPath:) и обновите реализацию, как показано ниже.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
func configureCell(cell: ToDoCell, atIndexPath indexPath: NSIndexPath) {
    // Fetch Record
    let record = fetchedResultsController.objectAtIndexPath(indexPath) as!
     
    // Update Cell
    if let name = record.name {
        cell.nameLabel.text = name
    }
     
    cell.doneButton.selected = record.done
     
    cell.didTapButtonHandler = {
        record.done = !record.done
    }
}

Мы сделали четыре изменения. Сначала мы изменили тип переменной record на Item . Метод objectAtIndexPath(_:) класса NSFetchedResultsController возвращает экземпляр AnyObject . Однако, поскольку мы знаем, что контроллер полученных результатов возвращает экземпляр Item мы принудительно понижаем результат, используя as! оператор.

Мы также подставляем valueForKey(_:) . Вместо этого мы используем свойства объекта record , name и done . Благодаря точечному синтаксису Swift результат очень разборчивый.

Чтобы обновить запись, мы больше не вызываем setValue(_:forKey:) . Вместо этого мы используем точечный синтаксис, чтобы установить свойство done . Я уверен, что вы согласны с тем, что это намного элегантнее, чем использование прямого кодирования значения ключа. Нам также не требуется дополнительная привязка для свойства done так как свойство done имеет тип Bool .

Помните, что KVC и KVO остаются неотъемлемой частью Core Data. Базовые данные используют valueForKey(_:) и setValue(_:forKey:) под капотом, чтобы выполнить работу.

Нам также нужно внести несколько изменений в класс AddToDoViewController . В методе save(_:) нам сначала нужно обновить инициализацию экземпляра NSManagedObject . Вместо инициализации экземпляра NSManagedObject мы создаем экземпляр Item .

1
2
3
4
5
// Create Entity
let entity = NSEntityDescription.entityForName(«Item», inManagedObjectContext: self.managedObjectContext)
 
// Initialize Record
let record = Item(entity: entity!, insertIntoManagedObjectContext: self.managedObjectContext)

Чтобы заполнить запись, мы используем точечный синтаксис вместо метода setValue(_:forKey:) как показано ниже.

1
2
3
// Populate Record
record.name = name
record.createdAt = NSDate().timeIntervalSince1970

Последний класс, который нам нужно обновить, это класс UpdateToDoViewController . Давайте начнем с изменения типа свойства record на Item! ,

01
02
03
04
05
06
07
08
09
10
11
12
13
import UIKit
import CoreData
 
class UpdateToDoViewController: UIViewController {
 
    @IBOutlet weak var textField: UITextField!
     
    var record: Item!
    var managedObjectContext: NSManagedObjectContext!
     
    …
 
}

Это изменение приведет к предупреждению в классе ViewController . Чтобы увидеть, что не так, откройте ViewController.swift и перейдите к prepareForSegue(_:sender:) .

Мы запрашиваем у контроллера полученных результатов запись по выбранному пути индекса. Тип переменной recordNSManagedObject , но класс UpdateToDoViewController ожидает экземпляр Item . Решение очень простое, как вы можете видеть ниже.

1
2
3
4
5
6
7
8
if let indexPath = tableView.indexPathForSelectedRow {
    // Fetch Record
    let record = fetchedResultsController.objectAtIndexPath(indexPath) as!
     
    // Configure View Controller
    viewController.record = record
    viewController.managedObjectContext = managedObjectContext
}

Вернитесь к классу UpdateToDoViewController и обновите метод viewDidLoad() как показано ниже.

1
2
3
4
5
6
7
override func viewDidLoad() {
    super.viewDidLoad()
     
    if let name = record.name {
        textField.text = name
    }
}

Нам также необходимо обновить метод save(_:) , в котором мы заменяем setValue(_:forKey:) на синтаксис точки.

1
2
// Update Record
record.name = name

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

Текущая модель данных не содержит никаких отношений, но давайте добавим несколько, чтобы увидеть, как выглядит подкласс NSManagedObject со связями. Как мы видели в предыдущей статье этой серии, нам сначала нужно создать новую версию модели данных. Выберите модель данных в Навигаторе проекта и выберите « Добавить версию модели …» в меню « Редактор» . Установите для Version Version значение Done 2 и основывайте модель на текущей модели данных, Done . Нажмите Готово, чтобы создать новую версию модели данных.

Откройте Done 2.xcdatamodel , создайте новый объект с именем User и добавьте имя атрибута типа String . Добавьте элементы отношений и установите пункт назначения Item . Оставьте пока обратное отношение пустым. Выбрав взаимосвязь элементов , откройте инспектор модели данных справа и установите для типа взаимосвязи значение « Многие» . Пользователь может иметь более одного элемента, связанного с ним.

Добавление объекта пользователя

Выберите объект Item , создайте пользователя- отношения и установите для пункта назначения значение « Пользователь» . Установите обратное отношение к элементам . Это автоматически установит обратную связь отношений элементов сущности User . Обратите внимание, что пользовательские отношения не являются обязательными.

Обновление объекта Entity

Прежде чем мы создадим подклассы NSManagedObject для обеих сущностей, нам нужно сообщить модели данных, какую версию модели данных она должна использовать. Выберите Done.xcdatamodeld , откройте инспектор файлов справа и установите текущую версию модели на Done 2 . Когда вы сделаете это, дважды проверьте, что вы выбрали Done.xcdatamodeld , а не Done.xcdatamodel .

Выберите New> File … в меню File и выберите шаблон подкласса NSManagedObject в разделе Core Data . В списке моделей данных выберите Готово 2 .

Выбор версии модели данных

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

Выбор сущностей

Давайте сначала посмотрим на изменения недавно сгенерированного класса Item . Как вы можете видеть ниже, расширение класса Item ( Item + CoreDataProperties.swift ) содержит одно дополнительное свойство user типа User? ,

01
02
03
04
05
06
07
08
09
10
11
import Foundation
import CoreData
 
extension Item {
 
    @NSManaged var createdAt: NSTimeInterval
    @NSManaged var done: Bool
    @NSManaged var name: String?
    @NSManaged var user: User?
 
}

Вот как выглядит отношение To One в подклассе NSManagedObject . Xcode достаточно умен, чтобы сделать вывод, что тип user свойства — User? , подкласс NSManagedObject мы создали минуту назад. Даже если отношение пользователя помечено как необязательное в модели данных, тип user свойства — User? , Не ясно, является ли это преднамеренным или ошибкой в ​​Xcode.

Реализация класса Item ( Item.swift ) не была обновлена ​​Xcode. Помните, что Xcode не трогает этот файл, если он уже существует.

Благодаря тому, что мы узнали, класс User легко понять. Посмотрите на расширение класса User ( User + CoreDataProperties.swift ). Первое, что вы заметите, это то, что тип свойства itemsNSSet? , Это не должно быть сюрпризом, потому что мы уже знали, что Core Data использует наборы, экземпляры класса NSSet , для хранения членов To Many отношения. Поскольку отношение помечено как необязательное в модели данных, типом является NSSet? ,

1
2
3
4
5
6
7
8
9
import Foundation
import CoreData
 
extension User {
 
    @NSManaged var name: String?
    @NSManaged var items: NSSet?
 
}

Вот как легко работать со связями в подклассах NSManagedObject .

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

Подклассы NSManagedObject очень распространены при работе с Core Data. Это не только повышает безопасность типов, но и значительно облегчает работу с отношениями.

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