Core Data

19 minute read

Core Data – фреймворк для работы с базой дынных в приложениях. С его помощью можно хранить и управлять данными. Я не часто его использовал, и у меня никак не было времени, чтобы разобраться с ним. Но на этих выходных время пришло.

Чтобы разобраться в принципах работы с Core Data, я хочу написать небольшое туду приложение. Звучит банально, но в этом приложении список дел можно будет сохранять как изображение и делать его заставкой на экране.

Новый проект

Начну с создания нового проекта. Желательно, отметить галочку как указано на картинке. В этом случае в файлах AppDelegate.swift и SceneDelegate.swift` сгенерируется дополнительный код и добавится специальный файл Memo.xcdatamodeld. Memo – это название моего проекта.

В файле SceneDelegate.swift в функции scene появилась новая строчка

// Get the managed object context from the shared persistent container.
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext

Эта функция возвращает контекст управляемого объекта(managed object context). Контекст управляемого объекта похож на блокнот, который хранится в оперативной памяти. Это место, где объекты создаются, выбираются, обновляются, удаляются и сохраняются обратно в постоянное хранилище на устройстве.

Ниже еше одна интересная строчка кода:

// Add `@Environment(\.managedObjectContext)` in the views that will need the context.
let contentView = ContentView().environment(\.managedObjectContext, context)

Тут создается ContentView, который потом передается в конструктор контроллера. Но что это за environment?

Перед тем, как запустится, наше корневое представление ContentView – в окружении установится новый параметр managedObjectContext с контекстом, который мы только что создали. Окружение – это место где хранятся различные системны параметры, например локаль календаря, цветовая схема и т.д. Все эти параметры хранятся по разным ключам. И теперь наш контекст тоже хранится там по ключу managedObjectContext.

Теперь каждое представление может использовать “блокнот” для работы с объектами.

Определимся со структурой данных которыми буду манипулировать в приложении. Для этого в файл Memo.xcdatamodeld добавлю новую сущность Todo. Для этого надо кликнуть на большой жирный плюс Add Entity

Теперь к новой сущности добавляем нужные аттрибуты. Для простого туду достаточно добавить text, done и поля id с типом UUID(это поле будет использоваться как уникальный идентификатор объекта).

Генерировать код в ручную не обязательно. Можно оставить значение Class Definition в поле Codegen и успокоится. Но я выберу Manual/None – это позволит сгенерировать файлы в папке проекта и посмотреть что там будет.

Теперь генерирую файлы.

Получается пара новый файлов в проекте: Memo+CoreDataClass.swift и Memo+CoreDataProperties.swift. В Memo+CoreDataClass.swift описан сам объект. А в Memo+CoreDataClass.swift описываются его свойства.

Замечу, что еще один плюс генерации классов в ручную – возможность вносить разные дополнения, которые недоступны при работе только через интерфейс для .xcdatamodeld. Например, можно сделать нужные поля как enum. Или добавить реализацию нужного интерфейса.

Интерфейс

Нужно убедиться, что блок предпросмотра имеет доступ к параметру окружения managedObjectContext как и все приложение.

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        
        let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
        
        return ContentView().environment(\.managedObjectContext, context)
    }
}

Теперь к интерфейсу самого приложения. Делать его буду стильно, модно и молодежно с помощью SwidtUI. Нужен заголовок, список с задачами, текстовое поле и кнопка для добавления новых задач. В ContentView добавил такой код:

import SwiftUI
import CoreData

struct ContentView: View {
    @State var todo = ""
    
    var body: some View {
        NavigationView {
            VStack {
                // форма для создания нового таска
                HStack {
                    TextField("Task",text: $todo).padding(.all, 20)
                    Button(action: {
                       
                    }){
                        Text("Add Task")
                    }.padding(.all, 20)
                }
                // список всех задач
                List {
                    HStack {
                        Text("Hello, World!")
                        Spacer() в
                        Button(action: {
                           
                        }){
                            Text("Complete")
                            .foregroundColor(.blue)
                        }
                    }
                    HStack {
                        Text("Hello, World!")
                        Spacer()
                        Button(action: {
                           
                        }){
                            Text("Complete")
                            .foregroundColor(.blue)
                        }
                    }
                }
            }.navigationBarTitle(Text("Tasks")
                .font(.largeTitle))
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Теперь подружу интерфейс с Core Data.

Сохраняем данные

Пока у меня ничего не сохранятся и не выводится. Чтобы сохранить новую задачу, нужно иметь доступ к контексту управляемого объекта. И тут становится понятным, зачем мы добавляли контекст в окружение. К нему можно получить доступ, используя свойство @Environment внутри нашего ContentView.

@Environment(\.managedObjectContext) var managedObjectContext

@State var todo = ""

И теперь можем написать код, который создает новый объект Task. Этот код нужно поместить в экшене кнопки "Add Task"

Button(action: {
                       
    guard self.todo != "" else {return}
    
    let todo = Todo(context: self.managedObjectContext)
    todo.text = self.todo
    todo.done = false
    todo.id = UUID()
    
}){
    Text("Add Task")
}.padding(.all, 20)

И теперь с помощью managedObjectContext сохраняем данные на устройстве.

Читаем данные

Чтобы прочитать сохраненные данные из хранилища, воспользуюсь свойством @FetchRequest, которое добавлю в ContentView. С помощью этого свойства очень легко манипулировать сохраненными данными.

@FetchRequest(entity: Todo.entity(), sortDescriptors: []) var todos: FetchedResults<Todo>

Это, конечно, круто что можно одной строчкой объявить переменную и сразу указать что она заполнится данными их хранилища. Но как пом не, это не очень очевидно и читать такие объявления в чужом коде – самая настоящая боль. И это еще не используется параметр predicate в свойстве @FetchRequest который может сделать все объявление нечитаемым.

Теперь отредактирую отображение списка задач, добавлю цикл по todos с помощью ForEach

ForEach(todos) { todo in
    HStack {
        Text(todo.text!)
        Spacer()
        Button(action: {
            
        }){
            Text("Complete")
            .foregroundColor(.blue)
        }
    }
}

Этот код у меня не захотел сразу работать.

Чтобы избавиться от этих ошибок – нужно добавить интерфейс Identifiable для класса Todo в файле Todo+CoreDataProperties.swift

extension Todo: Identifiable {
    // ...
}

У нас уже есть аттрибут id, поэтому после добавления интерфейса все заработает как нужно.

Запускаем эмулятор и добавляем новые задачи.

Редактирование и удаление

Отлично! Можно добавлять новые задачи. Осталось добавить возможность помечать их как выполненными и удалять. Для этого изменю блок с кнопкой – добавлю условие и сделаю две кнопки.

HStack {
    Text(todo.text!)
    Spacer()
    if todo.done {
        Button(action: {
            
        }){
            Text("Remove")
            .foregroundColor(.blue)
        }
    } else {
        Button(action: {
            
        }){
            Text("Complete")
            .foregroundColor(.blue)
        }
    }
}

Теперь в несколько строчек реализуем выполнение задачи

Button(action: {
    todo.done = true
    
    do {
        try self.managedObjectContext.save()
    } catch {
        print(error.localizedDescription)
    }
    
}){
    Text("Complete")
    .foregroundColor(.blue)
}

И удаление

Button(action: {
    self.managedObjectContext.delete(todo)
}){
    Text("Remove")
    .foregroundColor(.blue)
}

Заключение

В приложениях на SwiftUI стало еще проще использовать Core Data. Всю основную магию за вас делает свойство @FetchRequest. Но если вы новичек в Swift и iOS, то вам стоит подробнее изучить как работает выборка объектов из хранилища и что такое предикаты.

Код из статьи доступен на GitHub.

Ссылки по теме: