
Сьогодні ми розглянемо фреймворк під жахливою назвою CoreData від Apple. Я його дуже не люблю. Це рішення Apple для роботи з SQLite (реляційною базою даних). CoreData може зберігати об'єкти Swift в SQLite, а також виконувати зворотну операцію.
Вступ
Я особисто віддаю перевагу Realm, оскільки вам не потрібно робити багато рухів, щоб змусити його працювати: просто встановіть і починайте створювати все, що хочете, плюс на офіційному сайті є чудова документація для всіх функцій. Але коли ви переглядаєте вакансії інших компаній (я особисто ніколи цього не роблю. Інші компанії... вони існують? Справді?!:)). Часто ви бачите, що CoreData є обов'язковою, і якщо у вас немає досвіду з цим фреймворком, то ви легко можете «зникнути в повітрі» на початку співбесіди, і ніхто не дізнається про всі ті красиві кнопки, які ви можете створити :).
Отже, чому мені не подобається цей фреймворк? Тому що він важкий для розуміння для початківців, і легко заплутатися і зробити помилки, а чим більше коду ви написали, тим більше шансів, що десь ви могли помилитися.
Починаючи з iOS 10, Apple зробила багато для полегшення роботи з CoreData. Наприклад, вони представили об'єкт NSPersistentContainer
, який бере на себе завдання створення NSManagedObjectModel
, NSPersistentStoreCoordinator
та NSManagedObjectContext
. Тепер, щоб почати, вам просто потрібно ініціалізувати об'єкт persistentContainer = NSPersistentContainer(name: "MyModel")
. Спочатку ця стаття була задумана як огляд сторонніх рішень для CoreData, але оскільки Apple змінила цей фреймворк, багато з тих рішень були або в процесі переробки, або, ще гірше, — написані на Objective-C. Отже, у нас буде три приклади. У першому ми пройдемо через NSFetchedResultsController
та NSPersistentContainer
; у другому ми почнемо писати зручний клас, а в останньому прикладі ми покращимо клас, додавши генерики.
.
Приклад 1
Проект доступний для вивчення і називається MagicalRecord_project
(спочатку я хотів використовувати цей фреймворк). Давайте переглянемо файл Model.xcdatamodeld
:
Для атрибута avatar
(типу Binary Data
) була активована опція Allows External Storage
.
Перейдемо до контролера:
fileprivate let persistentContainer: NSPersistentContainer = NSPersistentContainer(name:"Model") fileprivate lazy var fetchedResultsController: NSFetchedResultsController<person> ={ let fetchRequest = NSFetchRequest<person>() fetchRequest.entity = Person.entity() fetchRequest.sortDescriptors =[NSSortDescriptor(key:"name", ascending:true)] let frc = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: self.persistentContainer.viewContext, sectionNameKeyPath:"name", cacheName:nil) frc.delegate = self return frc }()</person></person>
Тут ми створюємо об'єкти persistentContainer
та fetchedResultsController
, які відповідатимуть за заповнення
func createPersonObject(name: String, about: String, image: UIImage){ let person =NSEntityDescription.insertNewObject(forEntityName:"Person", into: persistentContainer.viewContext) as! Person person.name = name person.about = about person.avatar = UIImageJPEGRepresentation(image, 1.0) as NSData? }
Подивіться, як це виглядає у viewDidLoad
:
override func viewDidLoad(){ super.viewDidLoad() tableView.delegate = self tableView.dataSource = self //створити дані createPersonObject(name:"Ім'я A", about:"деяка інформація...", image: UIImage(named:"01")!) createPersonObject(name:"Ім'я B", about:"деяка інформація...", image: UIImage(named:"02")!) perform(#selector(insertAfterDelay), with: nil, afterDelay: 2.0) //отримати дані в контролеріdo{ try fetchedResultsController.performFetch()} catch { print(error)}}
Цей рядок perform(#selector(insertAfterDelay), with: nil, afterDelay: 2.0)
додає третій об'єкт до контексту, а NSFetchedResultsController
оновлює tableView
(про це ми поговоримо пізніше).
fetchedResultsController.performFetch()
передає дані з CoreData до контролера.
Тепер запустіть цей додаток.
На початку ми бачимо два доданих об'єкти, а через дві секунди — три. Давайте створимо extension
для ViewController
:
Оскільки у нас є fetchedResultsController
замість масиву dataSource
, ми використовуємо його для отримання всіх необхідних даних. Змінну fileprivate lazy var fetchedResultsController
ми оголосили як делегат NSFetchedResultsController
. Тепер ми створимо необхідні методи:
extension ViewController: NSFetchedResultsControllerDelegate { func controllerWillChangeContent(_ controller: NSFetchedResultsController<nsfetchrequestresult>){ tableView.beginUpdates()} func controllerDidChangeContent(_ controller: NSFetchedResultsController<nsfetchrequestresult>){ tableView.endUpdates()} func controller(_ controller: NSFetchedResultsController<nsfetchrequestresult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?){ switch type {case .insert: tableView.insertRows(at:[newIndexPath!], with: .automatic)case .delete: tableView.deleteRows(at:[indexPath!], with: .automatic)case .update: tableView.reloadRows(at:[indexPath!], with: .automatic)case .move: tableView.moveRow(at: indexPath!, to: newIndexPath!)}} func controller(_ controller: NSFetchedResultsController<nsfetchrequestresult>, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType){ switch type {case .insert: tableView.insertSections(IndexSet(integer: sectionIndex), with: .automatic)case .delete: tableView.deleteSections(IndexSet(integer: sectionIndex), with: .automatic)case .move, .update: tableView.reloadSections(IndexSet(integer: sectionIndex), with: .automatic)}}} </nsfetchrequestresult></nsfetchrequestresult></nsfetchrequestresult></nsfetchrequestresult>
Тепер, коли щось змінюється (додається, оновлюється або видаляється якийсь елемент), NSFetchedResultsController
знає про це і може внести необхідні корективи в UITableView
. Подібний механізм реалізовано в Realm:
Це не зовсім відповідає нашому прикладу, але, як ви можете побачити, механізми оновлення UITableView
подібні.
Приклад 2
У цьому прикладі ми створимо StoreManager
. Відкрийте проект StoreManager_project
. StoreManager
був створений за допомогою патерну Singleton
(один екземпляр на все життя програми):
fileprivate override init(){}static let sharedInstance = StoreManager() override func copy()-> Any { fatalError("Вам не дозволено використовувати метод copy в singleton!")} fileprivate lazy var fetchRequest: NSFetchRequest<car> ={ let request = NSFetchRequest<car>() request.entity = Car.entity() request.sortDescriptors =[NSSortDescriptor(key:"name", ascending:true)]return request }() fileprivate lazy var persistentContainer: NSPersistentContainer ={ let container = NSPersistentContainer(name:"Model") container.loadPersistentStores(completionHandler:{(storeDescription, error)inif let error = error as NSError? { fatalError("Невирішена помилка \(error), \(error.userInfo)")}})return container }() </car></car>
NSPersistentContainer
має дві властивості, а саме, властивість і метод: viewContext
та newBackgroundContext
. Перша пов'язана з main queue
, друга — з privateQueueConcurrencyType
. Коли ми щось записуємо в newBackgroundContext
, він надсилає сповіщення для об'єкта viewContext
, щоб об'єднати вміст. Тому, здається, нам більше не потрібно підписуватися на це сповіщення. З документації до viewContext
:
Цей контекст налаштований на те, щоб бути генераційним і автоматично споживати сповіщення про збереження з інших контекстів.
Завдяки newBackgroundContext
:
…встановлено на автоматичне споживання трансляцій NSManagedObjectContextDidSave.
Я додав кілька методів для модифікації в третьому прикладі:
По-перше, ми очищаємо сховище, а потім додаємо моделі автомобілів. Потім є метод для додавання третього автомобіля через дві секунди (ніколи не використовуйте performSelector: afterDelay:
— це погана практика). А також два методи showMainStorage()
, які показують, як змінилося сховище. Наприклад, ви можете розкоментувати наступний фрагмент коду:
NotificationCenter.default.addObserver(self, selector:#selector(storageHasChanged(note:)), name: NSNotification.Name.NSManagedObjectContextObjectsDidChange, object: nil) deinit { NotificationCenter.default.removeObserver(self)} //MARK: Дії func storageHasChanged(note:NSNotification){ let inserted = note.userInfo?["inserted"] let deleted = note.userInfo?["deleted"] let updated = note.userInfo?["updated"] if let insertedSet = inserted as? Set<car>, insertedSet.count > 0{ print("ДОДАНО") print(insertedSet.map { $0.name })}if let deletedSet = deleted as? Set<car>, deletedSet.count > 0{ print("ВИДАЛЕНО") print(deletedSet.map { $0.name })}if let updatedSet = updated as? Set<car>, updatedSet.count > 0{ print("ОНОВЛЕНО") print(updatedSet.map { $0.name })}} </car></car></car>
Ви можете експериментувати з повідомленням про зміни в базі даних. Мій результат:
[Optional("Zapor")] INSERTED [Optional("Fiat")] INSERTED [Optional("F1")] DELETED [Optional("Zapor"), Optional("F1"), Optional("Fiat"), Optional("BMW"), Optional("Fiat"), Optional("BMW"), Optional("Alfa Romeo"), Optional("Alfa Romeo"), Optional("BMW"), Optional("Fiat"), Optional("BMW"), Optional("F1"), Optional("F1"), Optional("Alfa Romeo"), Optional("F1"), Optional("Fiat"), Optional("Alfa Romeo"), Optional("Zapor"), Optional("Zapor"), Optional("Zapor")] CARS: Optional(["BMW", "F1", "Fiat", "Zapor"]) INSERTED [Optional("Alfa Romeo")] CARS: Optional(["Alfa Romeo", "BMW", "F1", "Fiat", “Zapor"])
Приклад 3
У цьому прикладі я хотів продемонструвати, як працювати з JSON. Для невеликого уроку я запросив JSON у вигляді рядка, а потім розпарсив його:
let json ="{\"cars\":[{\"name\":\"ZaZ\"},{\"name\":\"Lada\"},{\"name\":\"Lexus\"}]}" let dict = try! JSONSerialization.jsonObject(with: json.data(using: .utf8)!, options: JSONSerialization.ReadingOptions.allowFragments) as![String:Any]
А в файлі StoreManager
:
//щоб переконатися, що у нас є лише одна копія lazy var backgroundContext:NSManagedObjectContext={return self.persistentContainer.newBackgroundContext()}() fileprivate lazy var persistentContainer: NSPersistentContainer ={ let container = NSPersistentContainer(name:"Model") container.loadPersistentStores(completionHandler:{(storeDescription, error)inif let error = error as NSError? { fatalError("Невирішена помилка \(error), \(error.userInfo)")}})return container }() func saveContext(type: ContextType){ let context = type == .main ? persistentContainer.viewContext : backgroundContext if context.hasChanges {do{ try context.save()} catch { let nserror = error as NSError fatalError("Невирішена помилка \(nserror), \(nserror.userInfo)")}}} func getMainContextObjects<t: NSManagedObject>(entity: T.Type)-> [NSManagedObject]? {return persistentContainer.viewContext.fetchAll(entity: entity, fetchConfiguration:nil)} func clearStorage<t: NSManagedObject>(type: ContextType, entity: T.Type){ let context = type == .main ? persistentContainer.viewContext : backgroundContext let objects = context.fetchAll(entity: entity, fetchConfiguration:nil)if let objects = objects {for item in objects { context.delete(item)}}} func showStorage<t: NSManagedObject>(type: ContextType, entity: T.Type){ let context = type == .main ? persistentContainer.viewContext : backgroundContext let objects = context.fetchAll(entity: entity, fetchConfiguration:nil) print("OBJECTS: \(objects)") print("COUNT: \(objects?.count)")} </t:></t:></t:>
Тут я зберігаю backgroundContext
як змінну, щоб завжди отримувати лише один backgroundContext
, який потім можу зберегти за допомогою методу saveContext(type:)
. Інші методи зрозумілі. Оскільки у нас є enum
...
enum ContextType {case main case background }
...ми завжди знаємо контекст, з яким працюємо. Повернемося до ViewController
. Зверніть увагу, як я створюю об'єкти:
for item in dict["cars"] as![Any]{ let car = StoreManager.sharedInstance.backgroundContext.insert(entity: Car.self) car.name =(item as? [String:Any])?["name"] as! String? }
Але в методі insert(entity:)
немає контексту! Справа в тому, що я написав розширення для роботи з узагальненими даними.
//MARK: Розширення CoreData extension NSManagedObject{ class var entityName : String { let components = NSStringFromClass(self)return components } class func fetchRequestObj()-> NSFetchRequest<nsfetchrequestresult> { let request = NSFetchRequest<nsfetchrequestresult>(entityName: self.entityName)return request } class func fetchRequestWithKey(key: String, ascending: Bool =true)-> NSFetchRequest<nsfetchrequestresult> { let request = fetchRequestObj() request.sortDescriptors =[NSSortDescriptor(key: key, ascending: ascending)]return request }} extension NSManagedObjectContext{ func insert<t: nsmanagedobject>(entity: T.Type)-> T { let entityName = entity.entityName returnNSEntityDescription.insertNewObject(forEntityName: entityName, into: self) as! T } func fetchAll<t: nsmanagedobject>(entity: T.Type, fetchConfiguration:((NSFetchRequest<nsmanagedobject>)-> Void)?)-> [NSManagedObject]? { let dataFetchRequest = NSFetchRequest<nsmanagedobject>(entityName: entity.entityName) fetchConfiguration?(dataFetchRequest) var result =[NSManagedObject]()do{ result = try self.fetch(dataFetchRequest)} catch { print("Не вдалося отримати дані, критична помилка: \(error)")}return result }} </nsmanagedobject></nsmanagedobject></t:></t:></nsfetchrequestresult></nsfetchrequestresult></nsfetchrequestresult>
Таким чином, ви можете додати сутність у контекст, використовуючи її тип і self (Car.self)
. Також було додано метод fetchAll
для роботи з узагальненнями. Ми використовуємо його в методах StoreManager
. Тепер наш менеджер готовий до роботи. Сподіваюсь, що протестую і покращу його, якщо буде потрібно.