Элементы в List без дочерних отображаются при использовании SwiftData со стрелкой
Пытаюсь сделать List c Tree структурой. Когда загружаю данные из SwiftData, все отображается корректно, но после первого сохранения элементы БЕЗ дочерних отображаются со стрелкой. Обнаружил, что items: [Category]!
до сохранения - nil, после сохранения - пустой массив
Перерыл весь интернет-решения не нашел. Прошу о помощи
import SwiftData
import SwiftUI
@Model
class Category {
@Attribute(.unique) var name: String
@Relationship(deleteRule: .nullify, inverse: \Category.parent)
var items: [Category]?
var parent: Category?
init(name: String, parent: Category? = nil) {
self.name = name
self.parent = parent
}
}
struct ContentView: View {
@Environment(\.modelContext) var context
@Query private var categories: [Category]
var body: some View {
VStack {
List(categories.filter { $0.parent == nil }, children: \.items) { category in
Text(category.name)
}
HStack {
Button("Load data") {
let one = Category(name: "Parent one")
let two = Category(name: "Parent two")
let three = Category(name: "Parent three")
_ = Category(name: "Child one", parent: one)
_ = Category(name: "Child two", parent: one)
_ = Category(name: "Child three", parent: two)
_ = Category(name: "Child four", parent: two)
_ = Category(name: "Child five", parent: three)
_ = Category(name: "Child six", parent: three)
context.insert(one)
context.insert(two)
context.insert(three)
}
Spacer()
Button("Add item") {
}
}.padding(.horizontal)
}
}
}
Ответы (1 шт):
Проблема, с которой ты столкнулся, связана с тем, как SwiftData обрабатывает отношения и как SwiftUI List отображает данные.
Почему появляется стрелка:
SwiftData и инициализация отношений: Когда ты создаешь объект Category без дочерних элементов, свойство items не инициализируется пустым массивом автоматически. Оно остается nil. Это важно, потому что List в SwiftUI по-разному обрабатывает nil и пустой массив ([]).
List и children:: Параметр children: в List ожидает key path к свойству, которое содержит коллекцию дочерних элементов. Когда items равно nil, List не знает, есть ли у элемента дочерние элементы или нет. Поэтому он на всякий случай отображает стрелку раскрытия.
Сохранение и пустой массив После сохранения, SwiftData, при создании "fault" объекта (объекта, данные которого еще не загружены из БД), автоматически инициализирует свойство items пустым массивом [] для уже существующих (сохранённых) объектов, даже если дочерних элементов нет. Это стандартное поведение ORM-систем (Object-Relational Mapping, к которым относится SwiftData).
List видит []: Теперь List видит, что items — это пустой массив, и знает, что дочерних элементов нет. Но key path .items всё равно сообщает List, что потенциально дочерние элементы могут быть.
Решения:
Есть несколько способов решить эту проблему. Я покажу тебе наиболее эффективные и идиоматичные для SwiftUI и SwiftData:
- Использование вычисляемого свойства (Computed Property) (Рекомендуемый):
Это самый лучший способ. Мы добавим вычисляемое свойство, которое будет возвращать пустой массив, если items равно nil.
import SwiftData
import SwiftUI
@Model
class Category {
@Attribute(.unique) var name: String
@Relationship(deleteRule: .nullify, inverse: \Category.parent)
var items: [Category]?
var parent: Category?
init(name: String, parent: Category? = nil) {
self.name = name
self.parent = parent
}
// Добавляем вычисляемое свойство
var children: [Category] {
items ?? []
}
}
struct ContentView: View {
@Environment(\.modelContext) var context
@Query private var categories: [Category]
var body: some View {
VStack {
// Используем новое вычисляемое свойство children
List(categories.filter { $0.parent == nil }, children: \.children) { category in
Text(category.name)
}
HStack {
Button("Load data") {
let one = Category(name: "Parent one")
let two = Category(name: "Parent two")
let three = Category(name: "Parent three")
_ = Category(name: "Child one", parent: one)
_ = Category(name: "Child two", parent: one)
_ = Category(name: "Child three", parent: two)
_ = Category(name: "Child four", parent: two)
_ = Category(name: "Child five", parent: three)
_ = Category(name: "Child six", parent: three)
context.insert(one)
context.insert(two)
context.insert(three)
// Попробуй закомментировать/раскомментировать try? context.save()
// и посмотри на поведение. Это поможет понять разницу между
// nil и [].
try? context.save()
}
Spacer()
Button("Add item") {
// Добавь функциональность добавления, если нужно
}
}.padding(.horizontal)
}
}
}
Объяснение:
var children: [Category] { items ?? [] }: Это вычисляемое свойство. Оно не хранит данные, а вычисляет значение каждый раз, когда к нему обращаются.
items ?? []: Это оператор "nil coalescing". Он проверяет, равно ли items nil. Если да, то возвращает пустой массив ([]). Если нет, то возвращает значение items.
List(..., children: .children): Теперь мы используем key path .children в List. List всегда будет получать либо пустой массив, либо массив дочерних элементов, и будет правильно отображать стрелку.
Инициализация items в init (Менее предпочтительный, но рабочий):
@Model class Category { @Attribute(.unique) var name: String @Relationship(deleteRule: .nullify, inverse: \Category.parent) var items: [Category]? = [] // Инициализируем пустым массивом var parent: Category?
init(name: String, parent: Category? = nil) { self.name = name self.parent = parent }
}
struct ContentView: View { @Environment(.modelContext) var context @Query private var categories: [Category]
var body: some View { VStack { List(categories.filter { $0.parent == nil }, children: \.items) { category in Text(category.name) } HStack { Button("Load data") { let one = Category(name: "Parent one") let two = Category(name: "Parent two") let three = Category(name: "Parent three") _ = Category(name: "Child one", parent: one) _ = Category(name: "Child two", parent: one) _ = Category(name: "Child three", parent: two) _ = Category(name: "Child four", parent: two) _ = Category(name: "Child five", parent: three) _ = Category(name: "Child six", parent: three) context.insert(one) context.insert(two) context.insert(three) // Попробуй закомментировать/раскомментировать try? context.save() // и посмотри на поведение. try? context.save() } Spacer() Button("Add item") { } }.padding(.horizontal) } }
}
Объяснение:
var items: [Category]? = []: Мы явно инициализируем свойство items пустым массивом при создании объекта Category.
Почему первый способ лучше:
Меньше изменений: Тебе не нужно менять логику init.
Более явное намерение: Вычисляемое свойство children ясно показывает, что ты хочешь получить дочерние элементы, и обрабатываешь случай, когда их нет.
Соответствие поведению SwiftData: Этот способ лучше отражает то, как SwiftData работает с отношениями "по умолчанию" (faulting).
Лучше для производительности Т.к. не создаётся лишних пустых массивов
- Использование OutlineGroup (Если нужна поддержка iOS 13/14):
Если тебе нужна поддержка более старых версий iOS (до iOS 15), List с children: может работать некорректно. В этом случае используй OutlineGroup:
import SwiftData
import SwiftUI
@Model
class Category {
@Attribute(.unique) var name: String
@Relationship(deleteRule: .nullify, inverse: \Category.parent)
var items: [Category]?
var parent: Category?
init(name: String, parent: Category? = nil) {
self.name = name
self.parent = parent
}
var children: [Category] {
items ?? []
}
}
struct ContentView: View {
@Environment(\.modelContext) var context
@Query private var categories: [Category]
var body: some View {
VStack {
// Используем OutlineGroup
List {
ForEach(categories.filter{$0.parent == nil}) { category in
OutlineGroup(category, children: \.children) { child in
Text(child.name)
}
}
}
HStack {
Button("Load data") {
let one = Category(name: "Parent one")
let two = Category(name: "Parent two")
let three = Category(name: "Parent three")
_ = Category(name: "Child one", parent: one)
_ = Category(name: "Child two", parent: one)
_ = Category(name: "Child three", parent: two)
_ = Category(name: "Child four", parent: two)
_ = Category(name: "Child five", parent: three)
_ = Category(name: "Child six", parent: three)
context.insert(one)
context.insert(two)
context.insert(three)
// Попробуй закомментировать/раскомментировать try? context.save()
// и посмотри на поведение.
try? context.save()
}
Spacer()
Button("Add item") {
// Добавь функциональность, если нужно
}
}.padding(.horizontal)
}
}
}
Объяснение:
OutlineGroup: Этот компонент специально предназначен для отображения иерархических данных. Он работает с children key path аналогично List.
Ключевые выводы:
Понимай разницу между nil и пустым массивом ([]) в контексте SwiftData и SwiftUI.
Используй вычисляемые свойства (computed properties), чтобы обеспечить правильную обработку отношений в List (или OutlineGroup).
Инициализация items = [] в init — это обходной путь, но менее предпочтительный.
Я рекомендую использовать первый способ (с вычисляемым свойством children). Он самый чистый, эффективный и соответствует лучшим практикам работы со SwiftData и SwiftUI.