Программирование
CoreData с использованием дженериков и шаблона проектирования Одиночка
Вам нужно хранить информацию локально на устройстве? Если ответ положительный, а данные слишком велики для UserDefault, вам, вероятно, придется использовать CoreData.
Вам нужно хранить информацию локально на устройстве? Если ответ положительный, а данные слишком велики для UserDefault, вам, вероятно, придется использовать CoreData.
Как же тогда наиболее эффективно использовать Core Data? Давайте посмотрим, что Xcode предлагает нам делать при создании нового проекта, использующего CoreData.
Если установить этот флажок, будет создан целый проект, готовый к использованию, с примерами использования CoreData с уже реализованными различными полезными функциями.
Самое главное, он сгенерирует файл Data Model и структуру PersistenceController. Давайте более подробно рассмотрим эту структуру.
import CoreData | |
struct PersistenceController { | |
static let shared = PersistenceController() | |
static var preview: PersistenceController = { | |
let result = PersistenceController(inMemory: true) | |
let viewContext = result.container.viewContext | |
for _ in 0..<10 { | |
let newItem = Item(context: viewContext) | |
newItem.timestamp = Date() | |
} | |
do { | |
try viewContext.save() | |
} catch { | |
// Replace this implementation with code to handle the error appropriately. | |
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. | |
let nsError = error as NSError | |
fatalError("Unresolved error \(nsError), \(nsError.userInfo)") | |
} | |
return result | |
}() | |
let container: NSPersistentContainer | |
init(inMemory: Bool = false) { | |
container = NSPersistentContainer(name: "CoreDataTutorial") | |
if inMemory { | |
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null") | |
} | |
container.loadPersistentStores(completionHandler: { (storeDescription, error) in | |
if let error = error as NSError? { | |
// Replace this implementation with code to handle the error appropriately. | |
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. | |
/* | |
Typical reasons for an error here include: | |
* The parent directory does not exist, cannot be created, or disallows writing. | |
* The persistent store is not accessible, due to permissions or data protection when the device is locked. | |
* The device is out of space. | |
* The store could not be migrated to the current model version. | |
Check the error message to determine what the actual problem was. | |
*/ | |
fatalError("Unresolved error \(error), \(error.userInfo)") | |
} | |
}) | |
container.viewContext.automaticallyMergesChangesFromParent = true | |
} | |
} |
Как можно заметить, PersistenceController извлекает информацию о контейнере и его контексте, а затем использует @EnvironmentObject для передачи полученного контекста каждому View.
import SwiftUI | |
@main | |
struct CoreDataTutorialApp: App { | |
let persistenceController = PersistenceController.shared | |
var body: some Scene { | |
WindowGroup { | |
ContentView() | |
.environment(\.managedObjectContext, persistenceController.container.viewContext) | |
} | |
} | |
} |
Такой подход не использует все преимущества шаблона проектирования Singleton и делает код беспорядочным.
Лучший способ использовать Core Data
Лучшим подходом было бы использование PersistenceController для обработки различных операций, возможно, с использованием разных расширений структуры для более аккуратного хранения разных файлов.
Изначально PersistenceController будет простым и без каких-либо функций, кроме приватного инициализатора.
class PersinstanceController{ | |
static let shared = PersinstanceController() | |
let container = NSPersistentContainer(name: "ContainerName") | |
private init(){ | |
container.loadPersistentStores{ description, error in | |
if let error = error { | |
print("Core Data failed to load: \(error.localizedDescription)") | |
//Handle the error correctly | |
} | |
} | |
} | |
} |
Теперь мы можем создать функции, которые реализуют дженерики, а именно «выборку» (fetch) и «удаление» (delete).
Функция «выборка» проста, она принимает в качестве параметра имя сущности и тип, по ним извлекает все значения, хранящиеся в таблице.
func fetch<T:NSFetchRequestResult>(entityName:String) throws -> [T] { | |
let request = NSFetchRequest<T>(entityName: entityName) | |
let entities = try container.viewContext.fetch(request) | |
return entities | |
} | |
let foodArray:[FoodEntity] = try PersistenceController.shared.fetch(entityName: "FoodEntity") |
Будьте осторожны: при таком использовании дженериков вам всегда нужно объявлять тип вашей переменной, иначе функция не поймет, из какой таблицы вам нужно получить данные.
let foodArray:[FoodEntity] = try PersistenceController.shared.fetch(entityName: "FoodEntity") // Works because you pointed out the type | |
let foodArray = try PersistenceController.shared.fetch(entityName: "FoodEntity") // Doesn't work since it doesn't know the type you need to use |
Функция «удаление» еще проще и требует в качестве параметра элемент, который вы хотите удалить.
func delete<T:NSFetchRequestResult>(data:T) throws{ | |
container.viewContext.delete(data as! NSManagedObject) | |
try saveContext() | |
} | |
try PersistenceController.shared.delete(data: food) |
Функция saveContext используется для сохранения изменений, сделанных при удалении/добавлении/редактировании объекта.
func saveContext() throws{ | |
try container.viewContext.save() | |
} |
К сожалению, полностью интегрировать механизм дженериков в создание нового элемента невозможно. Простая функция заключается в следующем:
func createNewFood(product:Product) throws{ | |
let newFood = FoodEntity(context: container.viewContext) | |
product.copyInEntity(food: newFood) | |
try saveContext() | |
} | |
try PersistenceController.shared.createNewFood(product: Product(name: "TestFood", expiryDate: Date(), isOpened: false, category: .meat)) |
Теперь вы, вероятно, спрашиваете себя: «Что такое Product и функции copyInEntity?»
Product — это структура, которая инкапсулирует все части информации, полученные из CoreData, и обрабатывает их внутри представления.
Она объявляется так:
import Foundation | |
struct Product: Identifiable, Hashable { | |
var id = UUID() | |
var name: String | |
var dateCreated = Date() | |
var expiryDate: Date | |
var isOpened: Bool | |
var category: Category | |
init(name: String, dateCreated: Date = Date(), expiryDate: Date, isOpened: Bool, category: Category) { | |
self.name = name | |
self.dateCreated = dateCreated | |
self.expiryDate = expiryDate | |
self.isOpened = isOpened | |
self.category = category | |
} | |
init(food:FoodEntity) { | |
self.name = food.name ?? "" | |
self.dateCreated = food.dateCreated ?? Date() | |
self.expiryDate = food.expiryDate ?? Date() | |
self.isOpened = food.isOpened | |
self.category = Category.getCategory(category: food.category ?? "") | |
self.id = food.id ?? UUID() | |
} | |
func copyInEntity(food:FoodEntity){ | |
food.name = self.name | |
food.category = self.category.name | |
food.isOpened = self.isOpened | |
food.expiryDate = self.expiryDate | |
food.dateCreated = self.dateCreated | |
food.id = self.id | |
} | |
} | |
enum Category: CaseIterable { | |
case meat | |
case fish | |
case vegetables | |
case fruit | |
case others | |
var name: String { | |
switch self { | |
case .meat: | |
return "Meat" | |
case .fish: | |
return "Fish" | |
case .vegetables: | |
return "Vegetables" | |
case .fruit: | |
return "Fruit" | |
case .others: | |
return "Others" | |
} | |
} | |
static func getCategory(category:String)->Category{ | |
if category == "Meat"{ | |
return .meat | |
}else if category == "Fish"{ | |
return .fish | |
}else if category == "Vegetables"{ | |
return .vegetables | |
}else if category == "Fruit"{ | |
return.fruit | |
}else if category == "Others"{ | |
return .others | |
} | |
return .others | |
} | |
} | |
Внутри структуры определены две функции, которые помогают обмениваться данными между объектом Swift и объектом CoreData:
- кастомный init принимает в качестве параметра объект CoreData и присваивает все значения Swift;
- copyInEntity делает прямо противоположное, присваивая объекту CoreData все значения Swift.
Эти две функции полезны для очистки кода и более быстрой разработки.
Если вы хотите проверить исходный проект, вот ссылка на GitHub: https://github.com/SidusProxy/CoreDataTutorial
-
Новости3 недели назад
Видео и подкасты о мобильной разработке 2025.14
-
Видео и подкасты для разработчиков4 недели назад
Исследуем мир фото и видео редакторов
-
Новости4 недели назад
Видео и подкасты о мобильной разработке 2025.13
-
Разработка2 недели назад
Конец продуктовой разработки в том виде, в котором мы ее знаем