- PVSM.RU - https://www.pvsm.ru -

Зачесались у меня тут руки узнать, что это за зверь такой Swift и с чем его собственно едят. Как и ожидалось проблем и подводных камней пока оказалось много, ну либо я совсем не умею этот Swift готовить. Самая большая проблема ожидала меня при попытке подружить этот самый Swift с CoreData — штука принципиально отказывалась работать. Обильное гугление не приводило к хоть каким-либо хорошим результатам — информация была либо крайне обрывочной, либо попахивала костылями. Посему в первый вечер терзаний я капитулировал и решил использовать самое тупое решение в работе с CoreData по-старинке — хранить весь код в старом добром Objective-C и уже к нему обращаться из Swift (например в интерфейсах). Однако, перфекционизм в душе не давал покоя и требовалось реализовать чистое одноязычное решение, что я собственно и смог сделать, хотя признаться и не без костылей тоже. Кому интересен процесс прошу под кат. Также попутно предлагаю собирать баги и не самые на мой взгляд удобные вещи, которые пришли вместе с новым языком. Возможно, что-то я сделал криво — буду благодарен комментариям и поправкам, а также обсуждению лучших практик.
Можно без чтения статьи сразу просто качнуть пример отсюда https://github.com/KoNEW/CoreDataTest.git [1] и все раскурить самому.
Здесь и далее для разбора всех проблем и примеров будем использовать синтетический проект — будем делать приложение по просмотру и управлению классическими сущностями «Департамент» и «Сотрудник».
При этом департамент мы будем характеризовать такими полями как:
А сотрудника соответственно:
Собственно первым шагом открываем Xcode и создаем тривиальный проект с использованием CoreData и выставленным языком Swift. Единственная правка, которую мы сделаем на этом этапе — вырезаем всю работу с CoreData из делегата приложения и переносим ее в отдельный класс, который будет у нас работать в виде синглетона. Я просто привык так делать, когда раньше занимался кодом и здесь повторюсь — заодно можно глянуть как сделать синглетон на Swift. Префикс для всех наших классов будем использовать здесь и далее CS (CoreData+Swift).
Итак, что делаем:
По итогам файл 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 — то есть его можно считать в какой-то мере эталонным. Из интересного в плане обучения языку, в этом файле я бы для себя выделил:

Для того, чтобы наш код нормально работал с 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, но это как-то не комильфо вобщем.
Итак если кратко, какие тут есть решения еще раз:
Рассмотрим сначала работу только с одной сущностью «Департамента», к отношениям вернемся чуть позже. Итак, следуя инструкции от 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
}
Запускаем, смотрим логи и печалимся. Важные моменты:
[<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
не сработает, приложение просто вылетает.
Побродив по сети, я нашел ряд телодвижений, которые приводят к тому, чтобы код стал рабочим. Итак, что надо сделать:
Где-то под капотом у нас снова работает чистый Objective-C вызовы, так что нам надо сделать наш новый класс совместимым с Objective-C вызовами. Делаем это уже привычным образом за счет директивы @objc()
Это неочевидный шаг — нам надо снова выбрать файлик нашей модели и врукопашную прописать в конфигурации модели какой класс использовать для отображения сущности. 
После двух предыдущих шагов, все должно заработать, но я еще таки добавил метод 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"
}
}
А вот для реализации поддержки механизма на стороне департамента пришлось взять напильник в руку покрепче — так как опять таки ввиду сломанной кодогенерации волшебные методы для добавления дочерних сущностей не были созданы. Итого делаем следующую вещь:
В итоге наш класс стал выглядеть следующим образом:
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
Источник [4]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/ios/61889
Ссылки в тексте:
[1] https://github.com/KoNEW/CoreDataTest.git: https://github.com/KoNEW/CoreDataTest.git
[2] Swift and Objective-C in the Same Project: https://developer.apple.com/library/prerelease/ios/documentation/Swift/Conceptual/BuildingCocoaApps/MixandMatch.html#//apple_ref/doc/uid/TP40014216-CH10-XID_75
[3] Writing Swift Classes with Objective-C Behavior: https://developer.apple.com/library/prerelease/ios/documentation/Swift/Conceptual/BuildingCocoaApps/WritingSwiftClassesWithObjective-CBehavior.html#//apple_ref/doc/uid/TP40014216-CH5-XID_54
[4] Источник: http://habrahabr.ru/post/225727/
Нажмите здесь для печати.