Какой самый эффективный способ отображения информации о отношениях Core Data в SwiftUI?
У меня есть сущность Core Data Topic, которая имеет отношение “один-ко-многим” к Question (questions с обратным отношением topic). Каждый Question содержит свойство previousAttempt.
Я работаю над представлением SwiftUI, которое отображает список доступных тем. Верхний слой - это TopicsView, который содержит @FetchRequest для получения всех тем. Затем следует следующий ForEach:
ForEach(topics, id: \.objectID) {topic in
TopicCellView(topic: topic)
}
Внутри TopicCellView я хочу отображать заголовок темы и количество вопросов, у которых previousAttempt больше 0 (то есть тех, которые были правильно отвечены хотя бы один раз).
Какой правильный/эффективный способ сделать это? По моему мнению, есть следующие варианты:
1) Использование @FetchRequest
struct TopicCellView: View {
@ObservedObject private var topic: Topic
@FetchRequest private var correctQuestions: FetchedResults<Question>
init(topic: Topic) {
self.topic = topic
_correctQuestions = FetchRequest<Question>(sortDescriptors: [], predicate: NSPredicate(format: "topic == %@ AND previousAttempt > 0", argumentArray: [topic]))
}
var body: some View {
Text(correctQuestions.count)
}
}
2) Подсчет правильных вопросов в init с использованием отношения
struct TopicCellView: View {
@ObservedObject private var topic: Topic
private let correctQuestionsCount: Int
init(topic: Topic) {
self.topic = topic
correctQuestionsCount = topic.questions.count { $0.previousAttempt > 0 }
}
var body: some View {
Text(correctQuestionsCount)
}
}
3) Подсчет правильных вопросов в init с использованием запроса выборки
struct TopicCellView: View {
@ObservedObject private var topic: Topic
private let correctQuestionsCount: Int
init(topic: Topic) {
self.topic = topic
correctQuestionsCount = // выполнить запрос выборки Core Data здесь
}
var body: some View {
Text(correctQuestionsCount)
}
}
4) Добавление correctQuestionsCount как свойства к topic
Это чисто концептуальный вариант без кода.
Причина этого вопроса в том, что я пытаюсь оптимизировать свои представления SwiftUI и вижу, что при использовании Core Data у меня часто бывают долгие обновления тела представления. Я пробовал решения 1-3 выше, и все они иногда вызывают долгие обновления тела представления. Возможно, это неизбежно, но я все равно хотел бы понять, какой самый эффективный способ получения и отображения такой информации.
Я предполагаю, что вариант №4 будет наиболее производительным, но он точно склонен к ошибкам, поэтому я хотел бы его избегать, если это совершенно не необходимо.
Самый эффективный способ отображения информации о связях CoreData в SwiftUI — использование производных атрибутов в модели Core Data (Вариант 4), в сочетании с правильными техниками предварительной выборки. Этот подход минимизирует запросы к базе данных и использование памяти, обеспечивая обновление в реальном времени при изменении исходных данных.
Содержание
- Основные проблемы производительности при подсчете связей
- Производные атрибуты: Наиболее эффективное решение
- Альтернативные подходы и их ограничения
- Стратегия реализации
- Дополнительные оптимизации производительности
- Когда выбирать каждый подход
Основные проблемы производительности
При работе со связями CoreData в SwiftUI основные проблемы производительности связаны с тем, как Core Data управляет объектными связями и как SwiftUI запускает обновления представлений.
Подход с @FetchRequest (Вариант 1) создает отдельные запросы выборки для каждого TopicCellView, что приводит к:
- Множественным запросам к базе данных для каждой темы
- Частым обновлениям представлений при изменении данных
- Накоплению памяти из-за поддержания отдельных контекстов выборки
Аналогично, подсчет связей в коде Swift (Вариант 2) заставляет Core Data загружать все связанные объекты в память перед их фильтрацией и подсчетом, что становится все менее эффективным по мере роста размера связей.
Как объясняется на SwiftLee, “вызов метода count для свойства связи позволит получить количество объектов в этой связи для одной записи”, но этот подход работает на уровне Swift, а не использует нативные возможности базы данных для подсчета.
Производные атрибуты: Наиболее эффективное решение
Производные атрибуты (Вариант 4) обеспечивают наиболее эффективное решение, выполняя подсчет на уровне базы данных. Этот подход:
- Использует подсчет на уровне SQL вместо загрузки и фильтрации объектов в памяти
- Сохраняет количество как часть модели данных для немедленного доступа
- Автоматически обновляется при добавлении, удалении или изменении связанных объектов
- Снижает накладные расходы на память, избегая ненужного формирования объектов
Согласно SwiftLee, “в этом случае мы заполняем articles.@count, что в основном означает, что мы хотим использовать связь articles с помощью агрегатной функции @count. Сохранение конфигурации модели сгенерирует обновленные классы сущностей, и вы сможете использовать новый атрибут.”
Шаги реализации:
-
Добавьте новый атрибут в сущность
Topic:- Имя:
correctQuestionsCount - Тип: Integer 16
- Отметьте “Optional” при необходимости
- Имя:
-
Настройте его как производный атрибут:
- Установите выражение для использования агрегатной функции count
- Укажите путь связи к вашей связи
questions - Добавьте фильтрацию для
previousAttempt > 0
-
Обновите ваш TopicCellView:
struct TopicCellView: View {
@ObservedObject private var topic: Topic
var body: some View {
Text("\(topic.correctQuestionsCount)")
}
}
Как отмечает fatbobman, “по сравнению с прямым вызовом свойства .count связи, использование производного атрибута для подсчета обычно более эффективно. Это потому, что производные атрибуты используют другой механизм подсчета — они вычисляют и сохраняют количество…”
Альтернативные подходы и их ограничения
Вариант 1: @FetchRequest
Хотя этот подход функционален, он создает множественные запросы к базе данных и может привести к проблемам производительности в сложных приложениях. Каждый TopicCellView поддерживает свой собственный запрос выборки, вызывая ненужную нагрузку на базу данных.
Вариант 2: Подсчет связей в Swift
Этот метод загружает все связанные объекты в память перед фильтрацией, что становится все менее эффективным по мере роста размера связей. Результаты поиска последовательно показывают, что операции на уровне базы данных превосходят обработку в памяти.
Вариант 3: Ручной запрос выборки в init
Аналогично Варианту 1, но без преимуществ обертки свойств, этот подход все равно требует запросов к базе данных для каждой темы и не обеспечивает автоматического обновления при изменении данных.
Стратегия реализации
Для оптимальной производительности с вашей связью “Тема-Вопрос”:
-
Добавьте производный атрибут в сущность Topic:
- Имя:
correctQuestionsCount - Тип: Integer 16
- Выражение:
questions.@count[previousAttempt > 0]
- Имя:
-
Оптимизируйте ваш основной TopicsView:
struct TopicsView: View {
@FetchRequest(
entity: Topic.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \Topic.name, ascending: true)]
) private var topics: FetchedResults<Topic>
var body: some View {
List {
ForEach(topics, id: \.objectID) { topic in
TopicCellView(topic: topic)
}
}
.onAppear {
// Настройте предварительную выборку для лучшей производительности
try? topics.managedObjectContext?.perform {
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Topic")
fetchRequest.relationshipKeyPathsForPrefetching = ["questions"]
// Примените к вашему существующему запросу
}
}
}
}
- Упростите TopicCellView:
struct TopicCellView: View {
@ObservedObject private var topic: Topic
var body: some View {
VStack(alignment: .leading) {
Text(topic.name)
Text("\(topic.correctQuestionsCount) правильных вопросов")
.font(.caption)
.foregroundColor(.secondary)
}
}
}
Дополнительные оптимизации производительности
Пакетная выборка
Реализуйте пакетную выборку для снижения использования памяти при загрузке больших наборов данных:
@FetchRequest(
entity: Topic.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \Topic.name, ascending: true)],
animation: .default
) private var topics: FetchedResults<Topic> {
didSet {
topics.fetchBatchSize = 20
}
}
Предварительная выборка связей
Как упоминалось в результатах поиска, предварительная выборка связей может значительно улучшить производительность:
FetchRequest(
entity: Question.entity(),
sortDescriptors: [],
predicate: NSPredicate(format: "topic == %@ AND previousAttempt > 0", argumentArray: [topic]),
animation: .default
) private var correctQuestions: FetchedResults<Question> {
didSet {
correctQuestions.fetchBatchSize = 10
}
}
Управление контекстами
Рассмотрите возможность использования разных контекстов управляемых объектов для фоновых операций, чтобы предотвратить блокировку пользовательского интерфейса при обновлении данных.
Когда выбирать каждый подход
| Подход | Лучше всего подходит | Особенности производительности |
|---|---|---|
| Производные атрибуты | Часто используемые подсчеты связей | ★★★★★ (Лучшая производительность) |
| @FetchRequest | Сложная фильтрация и сортировка | ★★☆☆☆ (Множественные запросы) |
| Подсчет в Swift | Маленькие связи, простая логика | ★★☆☆☆ (Памемоемкий) |
| Ручная выборка | Одноразовые операции | ★★☆☆☆ (Нет автоматического обновления) |
Для вашего конкретного случая использования подсчета вопросов с previousAttempt > 0, производные атрибуты обеспечивают оптимальное решение, потому что количество:
- Вычисляется на уровне базы данных
- Автоматически обновляется при изменении вопросов
- Доступно без дополнительных запросов
- Эффективно использует память
Как последовательно показывают исследования, операции на уровне базы данных превосходят обработку в памяти для подсчета связей в приложениях Core Data.