📜 ⬆️ ⬇️

Swift + CoreData + A bit of a file

image

I combed my hands here to find out what kind of animal this Swift is and what it is actually eaten with. As expected, there were a lot of problems and pitfalls, but either I don’t know how to cook this Swift at all. The biggest problem awaited me when I tried to make friends with this Swift with CoreData - the thing basically refused to work. Abundant googling did not lead to at least any good results - the information was either extremely fragile or smacked with crutches. Therefore, on the first evening of tormenting, I capitulated and decided to use the most stupid solution when working with CoreData in the old-fashioned way - to keep all the code in the good old Objective-C and to access it from Swift (for example, in interfaces). However, perfectionism in the soul did not give rest and it was necessary to implement a pure monolingual solution, which I actually could do, although I must admit that it was not without crutches too. Who is interested in the process please under the cat. Also along the way I propose to collect bugs and not the most convenient things in my opinion that came along with the new language. Perhaps something I did crookedly - I will be grateful for comments and amendments, as well as a discussion of best practices.

To whom time is precious


Without reading the article, you can immediately download the example from here https://github.com/KoNEW/CoreDataTest.git and smoke everything yourself.

Synthetic example


What are we going to pick

Hereinafter, we will use a synthetic project to analyze all the problems and examples - we will make an application for viewing and managing the classic entities “Department” and “Employee”.
')
At the same time, the department will be characterized by such fields as:



And the employee, respectively:



Data management manager

Actually, the first step is to open Xcode and create a trivial project using CoreData and exposed Swift language. The only edit we will make at this stage is to cut out all the work with CoreData from the application delegate and transfer it to a separate class, which will work as a singleton for us. I just got used to doing this when I used to work on code, and here I’ll repeat it - at the same time you can look at how to make a singleton on Swift. For all of our classes, we will use the prefix here and hereafter CS (CoreData + Swift).

Jamb â„–1
I don’t know this bug in Xcode 6 Beta or a feature, but the prefixes for the classes of their own, in order not to write them every time, now you need to set them manually. This can be done in the File Inspector tab if you select a project file.


So, what we do:


According to the results, the AppDelegate.swift file is as follows:

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 } } 

And the CSDataManager.swft file is as follows:
 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! } } 


The code that generates XCode automatically was taken as a basis - that is, it can be considered to some extent a reference. Of the interesting things in terms of language learning, in this file I would single out for myself:



Jamb â„–2
Work with properties - I really do not like. The example is based on what Xcode itself offers by default — accordingly, I conclude that this is the best existing solution. Specifically, I do not like it - the need to directly declare an internal variable for storage (it used to work under the hood). At the same time, the variables themselves, despite the leading underscore in front, remain visible from the outside - and in fact we get that two properties are visible in the file inspector for each of our tasks. Total totally I do not like:
  • The need to explicitly duplicate properties to solve such problems.
  • The inability to create just internal variables
  • The impossibility of creating internal properties - we would decide earlier that the definition of a property is inside the implementation file, and not a blank file


image


Jamb â„–3
The pattern of the singleton is realized through a rigid crutch using the internal structure. In theory, this should be solved in a simple way through the use of class variables (class var) that are stated in the language specification - but de facto the compiler is not yet supported. Sadness, sadness - waiting for corrections. Also, in the current version of the language, it is still impossible (compared to Objective-C) to denote the class initializer as a private method, as a result, it is still impossible to make a pure idiot-resistant singleton.


Jamb number 4 or feature, I do not know
It is also worth paying attention to how the call to the NSNotificationCenter works. There is one simple point. Apple writes that all system libraries (UIKit, Foundation, CoreData, etc.) are already successfully fully friendly with Swift. However, in reality it turns out to be not quite so, or so, but not quite Namely, NSNotificationCenter runs under the hood on pure Objective-C, most likely for compatibility with all your other code. For this reason, its application has a number of nuances and limitations:

Moment one

In order for our code to work normally with Objective-C calls, we need to make it compatible — here, in general, according to the instructions. Add the magic attributes @objc() to the class name and the methods we need - for example, this is the part:

 @objc(CSDataManager) class CSDataManager:NSObject { ... @objc(appDidEnterBackground) func appDidEnterBackground(){ ... 


Moment two

It would be logical to tie a call from the notification center to the saveContext method itself - but since it returns Tuple with us, we cannot do this, such constructions are not defined in Objective-C. Because of this, we use a crutch with a simple void method call. In principle, everything is zen here - no, it is not. But in the head such things when designing your product, it is worth bearing in mind.


Create a data model

Here everything is trivial using standard Xcode tools to create our data model - as a result we get something like that.

image

Actually the problem


And what is the actual problem. It is simple - in Xcode 6-Beta, code generation is broken for the heirs classes from NSManagedObject. More precisely, the code will be generated on Objective-C, and not on Swift, but this is somehow not comfy.
So, briefly, what solutions are there again:



Follow the instructions


We first consider the work with only one entity of the “Department”; we will return to relations a little later. So, following the instructions from Apple, letter by letter, we come to this file to describe the CSDepartment class:

 import Foundation import CoreData import UIKit class CSDepartment : NSManagedObject{ @NSManaged var title: NSString @NSManaged var internalID: NSNumber @NSManaged var phone: NSString? } 


And we’ll check all the work with this code, which I left in my AppDelegate for simplicity (then, by the way, it will change to a more correct version for us).

 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 } 


Run, watch logs and grieve. Important points:



We use the file

After wandering around the network, I found a series of gestures that cause the code to become working. So, what to do:

Step 1. Compatible with Objective-C.

Somewhere under the hood, we still have clean Objective-C calls, so we need to make our new class compatible with Objective-C calls. We do this in the usual way due to the directive @objc()

Step 2. We polish the model file.

This is not an obvious step - we need to select the file of our model again and hand-to-hand register in the model configuration what class to use to display the entity.

image

Step 3. Cosmetic.

After the two previous steps, everything should work, but I still added the awakeFromInsert method, which now also works. And also added a description method, so that a more beautiful and understandable data string is displayed in the log.

As a result, the code of our class began to look like this:

 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)" } } 


We run our tests again - everything works, you can rejoice.

We work with relationships


So, with trivial entities figured out. By analogy, you can make a description of the entity CSEmployee . There is only one thing left to do - to make our system work correctly with entities - to be able to add and delete links. The relationship between the department and the staff is one-to-many. Here, the new language and Xcode behaved in two ways.
To implement the communication from the employee to the department, everything turned out to be trivial - we simply add one more to the list of its properties, which indicates the department itself. In total, the employee's class here began to look like this (I also added random generation of the name and surname from global arrays):

 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" } } 


But to implement the support mechanism on the side of the department, the file had to be taken in a stronger hand - because again, because of the broken code generation, magic methods for adding child entities were not created. Total do the following thing:



As a result, our class began to look like this:

 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) } } 


Final Job Verification Code and Remaining Problems


In the end, such adjustments were made:


So, the final code 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 } } 


Found a significant bug - cascading removal of objects occurs with a bang, but when you remove employees from the department using the removeEmployeesObject removeEmployees on the child objects, you do not reset the department pointers and, accordingly, the objects are still validly stolen by the system in the storage.
UPDATE: This is not a bug - Objective-C code behaves the same way, apparently I’ve confused something. Apparently these problems should be solved by an architectural approach.

Conclusion


In general, you can work, but so far not without pain in the shower. File always have to keep on hand. I would be glad to comment, amend and free discussion in search of the true path of the samurai.

Source: https://habr.com/ru/post/225727/


All Articles