Зачесались у меня тут руки узнать, что это за зверь такой Swift и с чем его собственно едят. Как и ожидалось проблем и подводных камней пока оказалось много, ну либо я совсем не умею этот Swift готовить. Самая большая проблема ожидала меня при попытке подружить этот самый Swift с CoreData — штука принципиально отказывалась работать. Обильное гугление не приводило к хоть каким-либо хорошим результатам — информация была либо крайне обрывочной, либо попахивала костылями. Посему в первый вечер терзаний я капитулировал и решил использовать самое тупое решение в работе с CoreData по-старинке — хранить весь код в старом добром Objective-C и уже к нему обращаться из Swift (например в интерфейсах). Однако, перфекционизм в душе не давал покоя и требовалось реализовать чистое одноязычное решение, что я собственно и смог сделать, хотя признаться и не без костылей тоже. Кому интересен процесс прошу под кат. Также попутно предлагаю собирать баги и не самые на мой взгляд удобные вещи, которые пришли вместе с новым языком. Возможно, что-то я сделал криво — буду благодарен комментариям и поправкам, а также обсуждению лучших практик.
Кому дорого время
Можно без чтения статьи сразу просто качнуть пример отсюда https://github.com/KoNEW/CoreDataTest.git и все раскурить самому.
Синтетический пример
С чем будем ковыряться
Здесь и далее для разбора всех проблем и примеров будем использовать синтетический проект — будем делать приложение по просмотру и управлению классическими сущностями «Департамент» и «Сотрудник».
При этом департамент мы будем характеризовать такими полями как:
- название (строка — обязательное)
- внутренний номер (число — обязательное)
- номер телефона (строка — опциональная, никаких проверок корректности номера делать не будем)
А сотрудника соответственно:
- Имя (строка — обязательно)
- Фамилия (строка — обязательно)
- Пол (число — обязательно)
- Возраст (число — опционально)
Менеджер управления данными
Собственно первым шагом открываем Xcode и создаем тривиальный проект с использованием CoreData и выставленным языком Swift. Единственная правка, которую мы сделаем на этом этапе — вырезаем всю работу с CoreData из делегата приложения и переносим ее в отдельный класс, который будет у нас работать в виде синглетона. Я просто привык так делать, когда раньше занимался кодом и здесь повторюсь — заодно можно глянуть как сделать синглетон на Swift. Префикс для всех наших классов будем использовать здесь и далее CS (CoreData+Swift).
Итак, что делаем:
- Вырезаем всю работу с CoreData из AppDelegate
- Создаем класс CSDataManager
- В нем создаем свойства для работы с моделью, контекстом и непосредственным хранилищем данных
- Создаем метод для работы в виде синглетона
По итогам файл AppDelegate.swift у нас выглядит следующим образом:
import UIKit
import CoreData
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: NSDictionary?) -> Bool {
// Override point for customization after application launch.
return true
}
}
А файл CSDataManager.swft — следующим:
import UIKit
import Foundation
import CoreData
let kCSErrorDomain = "ru.novilab-mobile.cstest"
let kCSErrorLocalStorageCode = -1000
@objc(CSDataManager)
class CSDataManager:NSObject {
//Managed Model
var _managedModel: NSManagedObjectModel?
var managedModel: NSManagedObjectModel{
if !_managedModel{
_managedModel = NSManagedObjectModel.mergedModelFromBundles(nil)
}
return _managedModel!
}
//Store coordinator
var _storeCoordinator: NSPersistentStoreCoordinator?
var storeCoordinator: NSPersistentStoreCoordinator{
if !_storeCoordinator{
let _storeURL = self.applicationDocumentsDirectory.URLByAppendingPathComponent("CSDataStorage.sqlite")
_storeCoordinator = NSPersistentStoreCoordinator(managedObjectModel: self.managedModel)
func addStore() -> NSError?{
var result: NSError? = nil
if _storeCoordinator!.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: _storeURL, options: nil, error: &result) == nil{
println("Create persistent store error occurred: (result?.userInfo)")
}
return result
}
var error = addStore()
if error != nil{
println("Store scheme error. Will remove store and try again. TODO: add scheme migration.")
NSFileManager.defaultManager().removeItemAtURL(_storeURL, error: nil)
error = addStore()
if error{
println("Unresolved critical error with persistent store: (error?.userInfo)")
abort()
}
}
}
return _storeCoordinator!
}
//Managed Context
var _managedContext: NSManagedObjectContext? = nil
var managedContext: NSManagedObjectContext {
if !_managedContext {
let coordinator = self.storeCoordinator
if coordinator != nil {
_managedContext = NSManagedObjectContext()
_managedContext!.persistentStoreCoordinator = coordinator
}
}
return _managedContext!
}
//Init
init() {
super.init()
NSNotificationCenter.defaultCenter().addObserver(self, selector: "appDidEnterBackground", name: UIApplicationDidEnterBackgroundNotification, object: nil)
}
@objc(appDidEnterBackground)
func appDidEnterBackground(){
var (result:Bool, error:NSError?) = self.saveContext()
if error != nil{
println("Application did not save data with reason: (error?.userInfo)")
}
}
// Returns the URL to the application's Documents directory.
var applicationDocumentsDirectory: NSURL {
let urls = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)
return urls[urls.endIndex-1] as NSURL
}
//Save context
func saveContext() -> (Bool, NSError?){
println("Will save")
var error: NSError? = nil
var result: Bool = false
let context = self.managedContext
if context != nil{
if context.hasChanges && !context.save(&error){
println("Save context error occurred: (error?.userInfo)")
}else{
result = true
}
}else{
let errorCode = kCSErrorLocalStorageCode
let userInfo = [NSLocalizedDescriptionKey : "Managed context is nil"]
error = NSError.errorWithDomain(kCSErrorDomain, code: errorCode, userInfo: userInfo)
}
return (result, error)
}
//Singleton Instance
class func sharedInstance() -> CSDataManager{
struct wrapper{
static var shared_instance: CSDataManager? = nil
static var token: dispatch_once_t = 0
}
dispatch_once(&wrapper.token, {wrapper.shared_instance = CSDataManager()})
return wrapper.shared_instance!
}
}
За основу брался автоматически код, который генерит XCode — то есть его можно считать в какой-то мере эталонным. Из интересного в плане обучения языку, в этом файле я бы для себя выделил:
- работа с глобальными константами
- работа со свойствами класса
- статические методы класса (старые добрые class object methods)
- хранимые процедуры (допилил напильником свойство storeCoordantor, так как постоянно менял в будущем модель данных и это обеспечивало автоматическое затирание файла БД по необходимости)
- работа с Tuples на примере модифицированного метода сохранения контекста
- работа с NSNotificationCenter
- Необходимость явно дублировать свойства для решения подобных задач
- Невозможность создания просто внутренних переменных
- Невозможность создания внутренних свойств — раньше решалось будем определения property внутри файла реализации, а не заготовочного файла
Момент один
Для того, чтобы наш код нормально работал с Objective-C вызовами, нам надо его сделать совместимым — здесь в целом все по инструкции. Добавляем к названию класса и нужным нам методам волшебные атрибуты @objc()
— например вот это часть:
@objc(CSDataManager)
class CSDataManager:NSObject {
...
@objc(appDidEnterBackground)
func appDidEnterBackground(){
...
Момент два
Логичным было бы подвязать вызов от центра уведомлений на сам метод saveContext — но поскольку он у нас возвращает Tuple, то сделать мы этого не можем, подобные конструкции не определены в Objective-C. Из-за этого мы используем костыль с вызовом простого void
метода. В принципе здесь все по дзену — нет, так нет. Но в голове такие штуки при проектировании своего продукта, стоит иметь ввиду.
Создаем модель данных
Здесь все тривиально стандартными средствами XCode создаем нашу модель данных — в итоге получаем что-то такое.
Собственно проблема
А в чем собственно заключается проблема. Она простая — в XCode 6-Beta сломана кодогенерация для классов наследников от NSManagedObject. Точнее генерирутся код на Objective-C, а не на Swift, но это как-то не комильфо вобщем.
Итак если кратко, какие тут есть решения еще раз:
- Вариант A. Используем нормальную кодогенерацию, получаем на выходе привычные рабочие файлы Objective-C и через Bridging файл используем их в нашем Swift-коде основном. Как использовать Objective-C классы кастомные в Swift можно прочитать здесь — Swift and Objective-C in the Same Project
- Вариант B. Пытаемся сделать наоборот — будем писать Swift класс, который сможет вести себя как Objective-C класс и при этом будет валютно работать с CoreData. Для этого берем в правую руку напильник, а в левую крайне краткий гайд от Apple — Writing Swift Classes with Objective-C Behavior
Следуем инструкции
Рассмотрим сначала работу только с одной сущностью «Департамента», к отношениям вернемся чуть позже. Итак, следуя инструкции от Apple буква за буквой мы приходим вот к такому файлику для описания класса CSDepartment:
import Foundation
import CoreData
import UIKit
class CSDepartment : NSManagedObject{
@NSManaged var title: NSString
@NSManaged var internalID: NSNumber
@NSManaged var phone: NSString?
}
А проверять всю работу будем вот таким кодом, который я оставил у себя в AppDelegate для простоты (потом он кстати у нас изменится на боле корректную версию).
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: NSDictionary?) -> Bool {
//Get manager
let manager = CSDataManager.sharedInstance()
//Create new department
let newDepartment : AnyObject! = NSEntityDescription.insertNewObjectForEntityForName("CSDepartment", inManagedObjectContext: manager.managedContext)
//Save context
manager.saveContext()
//Get and print all departments
let request = NSFetchRequest(entityName: "CSDepartment")
let departments = manager.managedContext.executeFetchRequest(request, error: nil)
println("Departments: (departments)")
return true
}
Запускаем, смотрим логи и печалимся. Важные моменты:
- В первую очередь мы получаем ошибку сохранения данных в CoreData по причине того, что ряд полей у нас выставлены как обязательные (title и internalID), а мы их не указали. Здесь типовое решение выставить некоторые значения по умолчанию в файлике модели или использовать метод awakeFromInsert. Первое решение рабочее, но не спортивное, второе нерабочее в таком виде — метод просто не вызывается, что навело меня в уныние.
- Второй важный момент, в логах мы видим что-то примерно такое Departments:
[<NSManagedObject: 0xb264030> (entity: CSDepartment; id: 0xb264090 <x-coredata:///CSDepartment/t3403D9E7-F910-4E2D-989E-95D9C984C1762> ; data: {employees = (); internalID = nil;phone = nil;title = nil;})]
Из этого лога следует важный вывод — наша переменная инстанцировалась как экземпляр NSManagedObject, а не как CSDepartment. В результате чего указать значения полей мы также не можем — так как жесткое приведение типов в духе
let newDepartment : CSDepartment = NSEntityDescription.insertNewObjectForEntityForName("CSDepartment", inManagedObjectContext: manager.managedContext) as CSDepartment
не сработает, приложение просто вылетает.
Пускаем в ход напильник
Побродив по сети, я нашел ряд телодвижений, которые приводят к тому, чтобы код стал рабочим. Итак, что надо сделать:
Шаг 1. Совместимость с Objective-C.
Где-то под капотом у нас снова работает чистый Objective-C вызовы, так что нам надо сделать наш новый класс совместимым с Objective-C вызовами. Делаем это уже привычным образом за счет директивы @objc()
Шаг 2. Шлифуем файл модели.
Это неочевидный шаг — нам надо снова выбрать файлик нашей модели и врукопашную прописать в конфигурации модели какой класс использовать для отображения сущности.
Шаг 3. Косметический.
После двух предыдущих шагов, все должно заработать, но я еще таки добавил метод awakeFromInsert, который теперь также заработал. И также добавил метод description, чтобы в лог выводилась более красивая и понятная строка данных.
В результате код нашего класса стал выглядеть таким образом:
import Foundation
import CoreData
import UIKit
@objc(CSDepartment)
class CSDepartment : NSManagedObject{
@NSManaged var title: NSString
@NSManaged var internalID: NSNumber
@NSManaged var phone: NSString?
override func awakeFromInsert() {
self.title = "New department"
self.internalID = 0
}
func description() -> NSString{
return "Department: className=(self.dynamicType.description()), title=(self.title), id=[(self.internalID)] and phone=(self.phone)"
}
}
Снова прогоняем наши тесты — все работает, можно радоваться.
Работаем с отношениями
Итак с тривиальными сущностями разобрались. По аналогии можно сделать и описание сущности CSEmployee
. Осталось сделать только одну вещь — заставить нашу систему работать корректно с сущностями — уметь добавлять и удалять связи. Связь между департаментом и сотрудниками у нас вида один-ко-многим. Здесь новый язык и XCode повели себя двояко.
Для реализации связи от сотрудника к департаменту все оказалось тривиально — просто добавляем в список его свойств еще одно, которое указывает на сам департамент. Итого класс сотрудника у нас начал выглядеть вот таким образом (от себя еще добавил разданную генерацию имени и фамилии из глобальных массивов):
import Foundation
import CoreData
let st_fNames = ["John", "David", "Michael", "Bob"]
let st_lNames = ["Lim", "Jobs", "Kyler"]
@objc(CSEmployee)
class CSEmployee:NSManagedObject{
@NSManaged var firstName: NSString
@NSManaged var lastName: NSString
@NSManaged var age: NSNumber?
@NSManaged var department: CSDepartment
override func awakeFromInsert() {
super.awakeFromInsert()
self.firstName = st_fNames[Int(arc4random_uniform(UInt32(st_fNames.count)))]
self.lastName = st_lNames[Int(arc4random_uniform(UInt32(st_lNames.count)))]
}
func description() -> NSString{
return "Employee: name= (self.firstName) (self.lastName), age=(self.age) years"
}
}
А вот для реализации поддержки механизма на стороне департамента пришлось взять напильник в руку покрепче — так как опять таки ввиду сломанной кодогенерации волшебные методы для добавления дочерних сущностей не были созданы. Итого делаем следующую вещь:
- Добавляем свойство для хранения сотрудников с классом NSSet
- Добавляем самописные методы для добавления и удаления сотрудников — делалось на основе сниппетов XCode для версии на Objective-C
В итоге наш класс стал выглядеть следующим образом:
import Foundation
import CoreData
@objc(CSDepartment)
class CSDepartment : NSManagedObject{
@NSManaged var title: NSString
@NSManaged var internalID: NSNumber
@NSManaged var phone: NSString?
@NSManaged var employees: NSSet
override func awakeFromInsert() {
self.title = "New department"
self.internalID = 0
}
func description() -> NSString{
let employeesDescription = self.employees.allObjects.map({employee in employee.description()})
return "Department: title=(self.title), id=[(self.internalID)], phone=(self.phone) and employees = (employeesDescription)"
}
//Working with Employees
func addEmployeesObject(employee: CSEmployee?){
let set:NSSet = NSSet(object: employee)
self.addEmployees(set)
}
func removeEmployeesObject(employee: CSEmployee?){
let set:NSSet = NSSet(object: employee)
self.removeEmployees(set)
}
func addEmployees(employees: NSSet?){
self.willChangeValueForKey("employees", withSetMutation: NSKeyValueSetMutationKind.UnionSetMutation, usingObjects: employees)
self.primitiveValueForKey("employees").unionSet(employees)
self.didChangeValueForKey("employees", withSetMutation: NSKeyValueSetMutationKind.UnionSetMutation, usingObjects: employees)
}
func removeEmployees(employees: NSSet?){
self.willChangeValueForKey("employess", withSetMutation: NSKeyValueSetMutationKind.MinusSetMutation, usingObjects: employees)
self.primitiveValueForKey("employees").minusSet(employees)
self.didChangeValueForKey("employees", withSetMutation: NSKeyValueSetMutationKind.MinusSetMutation, usingObjects: employees)
}
}
Итоговый код проверки работы и оставшиеся проблемы
В конечном счете были внесены такие коррективы:
В класс CSDataManager внесены два метода для получения полного списка департаментов и сотрудников
Изменен код примера — посмотрели фичи добавления, удаления объектов и каскадного удаления
Итак итоговый код AppDelegate:
import UIKit
import CoreData
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: NSDictionary?) -> Bool {
//Get manager
let manager = CSDataManager.sharedInstance()
//Testing insert new objects
let newDepartment : CSDepartment = NSEntityDescription.insertNewObjectForEntityForName("CSDepartment", inManagedObjectContext: manager.managedContext) as CSDepartment
let newEmployee: CSEmployee = NSEntityDescription.insertNewObjectForEntityForName("CSEmployee", inManagedObjectContext: manager.managedContext) as CSEmployee
let newEmployee2: CSEmployee = NSEntityDescription.insertNewObjectForEntityForName("CSEmployee", inManagedObjectContext: manager.managedContext) as CSEmployee
newEmployee.department = newDepartment
newDepartment.addEmployeesObject(newEmployee2)
manager.saveContext()
//Get and print all departments
println("Have add oen department and two employees")
println("Departments: (manager.departments())")
println("Employees: (manager.employees())")
//Testing remove child object
newDepartment.removeEmployeesObject(newEmployee2)
manager.saveContext()
println("Have delete one employee")
println("Departments: (manager.departments())")
//Testing cascade remove
manager.managedContext.deleteObject(newDepartment)
manager.saveContext()
println("nHave delete department")
println("Departments: (manager.departments())")
println("Employees: (manager.employees())")
//Uncomment to remove all records
// let departments = manager.departments()
// for i in 0..departments.count{
// let dep = departments[i] as CSDepartment
// manager.managedContext.deleteObject(dep)
// }
// let employees = manager.employees()
// for i in 0..employees.count{
// let emp = employees[i] as CSEmployee
// manager.managedContext.deleteObject(emp)
// }
// manager.saveContext()
// println("nHave delete all data")
// println("Departments: (manager.departments())")
// println("Employees: (manager.employees())")
return true
}
}
Найдена существенная бага — каскадное удаление объектов происходит на ура, а вот при удалении сотрудников из департамента с помощью методов removeEmployeesObject и removeEmployees
на дочерних объектах не происходит сброса указателей на департамент и соответственно объекты по-прежнему валидно крадутся системой в хранилище.
Заключение
В общем работать можно, но пока не без боли в душе. Напильник всегда придется держать под рукой. Буду рад комментариям, поправкам и свободной дискуссии в поисках истинного пути самурая.
Автор: KoNEV