I’ve an app with the next mannequin:
@Mannequin class TaskList {
@Attribute(.distinctive)
var identify: String
// Relationships
var parentList: TaskList?
@Relationship(deleteRule: .cascade, inverse: TaskList.parentList)
var taskLists: [TaskList]?
init(identify: String, parentTaskList: TaskList? = nil) {
self.identify = identify
self.parentList = parentTaskList
self.taskLists = []
}
}
If I run the next take a look at, I get the anticipated outcomes – Father or mother
has it is taskLists
array up to date to incorporate the Youngster
listing created. I do not explicitly add the kid to the guardian array – the parentList
relationship property on the kid causes SwiftData to robotically carry out the append into the guardian array:
@Take a look at("TaskList with youngsters with unbiased saves are within the database")
func test_savingRootTaskIndependentOfChildren_SavesAllTaskLists() async throws {
let modelContext = TestHelperUtility.createModelContext(useInMemory: false)
let parentList = TaskList(identify: "Father or mother")
modelContext.insert(parentList)
strive modelContext.save()
let childList = TaskList(identify: "Youngster")
childList.parentList = parentList
modelContext.insert(childList)
strive modelContext.save()
let fetchedResults = strive modelContext.fetch(FetchDescriptor())
let fetchedParent = fetchedResults.first(the place: { $0.identify == "Father or mother"})
let fetchedChild = fetchedResults.first(the place: { $0.identify == "Youngster" })
#anticipate(fetchedResults.rely == 2)
#anticipate(fetchedParent?.taskLists.rely == 1)
#anticipate(fetchedChild?.parentList?.identify == "Father or mother")
#anticipate(fetchedChild?.parentList?.taskLists.rely == 1)
}
I’ve a subsequent take a look at that deletes the kid and exhibits the guardian array being up to date accordingly. With this context in thoughts, I am not seeing these relationship updates being noticed inside SwiftUI. That is an app that reproduces the difficulty. On this instance, I’m making an attempt to maneuver “Finance” from underneath the “Work” guardian and into the “House” listing.
To begin, the next code is a working instance the place the habits does what I anticipate – it strikes the listing from one guardian to a different with none situation. That is carried out utilizing the native OutlineGroup
in SwiftUI.
ContentView
struct ContentView: View {
@Question(type: TaskList.identify) var taskLists: [TaskList]
@State personal var selectedList: TaskList?
var physique: some View {
NavigationStack {
Checklist {
ForEach(taskLists.filter({$0.parentList == nil})) { listing in
OutlineGroup(listing, youngsters: .taskLists) { listing in
Textual content(listing.identify)
.onTapGesture {
selectedList = listing
}
}
}
}
.sheet(merchandise: $selectedList, onDismiss: {
selectedList = nil
}) { listing in
TaskListEditorScreen(existingList: listing)
}
}
}
}
TaskListEditorScreen
struct TaskListEditorScreen: View {
@Atmosphere(.dismiss) personal var dismiss
@Atmosphere(.modelContext) personal var modelContext
@State personal var viewModel: TaskListEditorViewModel
@Bindable var listing: TaskList
init(existingList: TaskList) {
listing = existingList
viewModel = TaskListEditorViewModel(taskList: existingList)
}
var physique: some View {
NavigationView {
TaskListFormView(viewModel: viewModel)
.toolbar {
ToolbarItem {
Button("Cancel") {
dismiss()
}
}
ToolbarItem {
Button("Save") {
listing.identify = viewModel.identify
listing.parentList = viewModel.parentTaskList
strive! modelContext.save()
dismiss()
}
}
}
}
}
}
TaskListFormView
struct TaskListFormView: View {
@Bindable var viewModel: TaskListEditorViewModel
var physique: some View {
VStack {
Type {
TextField("Identify", textual content: $viewModel.identify)
NavigationLink {
TaskListPickerScreen(viewModel: self.viewModel)
} label: {
Textual content(self.viewModel.parentTaskList?.identify ?? "Father or mother Checklist")
}
}
}
}
}
TaskListPickerScreen
struct TaskListPickerScreen: View {
@Atmosphere(.dismiss) personal var dismiss
@Question(filter: #Predicate { $0.parentList == nil }, type: TaskList.identify)
personal var taskLists: [TaskList]
@Bindable var viewModel: TaskListEditorViewModel
var physique: some View {
Checklist {
ForEach(taskLists) { listing in
OutlineGroup(listing, youngsters: .taskLists) { baby in
getRowForChild(baby)
}
}
}
.toolbar {
ToolbarItem {
Button("Clear Father or mother") {
viewModel.parentTaskList = nil
dismiss()
}
}
}
}
@ViewBuilder func getRowForChild(_ listing: TaskList) -> some View {
HStack {
Textual content(listing.identify)
}
.onTapGesture {
if listing.identify == viewModel.identify {
return
}
self.viewModel.parentTaskList = listing
dismiss()
}
}
}
TaskListEditorViewModel
@Observable class TaskListEditorViewModel {
var identify: String
var parentTaskList: TaskList?
init(taskList: TaskList) {
identify = taskList.identify
parentTaskList = taskList.parentList
}
}
You may setup the next container and seed it with take a look at information to confirm that the result’s the lists can transfer between mother and father and SwiftUI updates it accordingly.
#Preview {
ContentView()
.modelContext(DataContainer.preview.dataContainer.mainContext)
}
@MainActor class DataContainer {
let schemaModels = Schema([ TaskList.self ])
let dataConfiguration: ModelConfiguration
static let shared = DataContainer()
static let preview = DataContainer(memoryDB: true)
init(memoryDB: Bool = false) {
dataConfiguration = ModelConfiguration(isStoredInMemoryOnly: memoryDB)
}
lazy var dataContainer: ModelContainer = {
do {
let container = strive ModelContainer(for: schemaModels)
seedData(context: container.mainContext)
return container
} catch {
fatalError("(error.localizedDescription)")
}
}()
func seedData(context: ModelContext) {
let lists = strive! context.fetch(FetchDescriptor())
if lists.rely == 0 {
Activity { @MainActor in
SampleData.taskLists.filter({ $0.parentList == nil }).forEach {
context.insert($0)
}
strive! context.save()
}
}
}
}
struct SampleData {
static let taskLists: [TaskList] = {
let residence = TaskList(identify: "House")
let work = TaskList(identify: "Work")
let reworking = TaskList(identify: "Transforming", parentTaskList: residence)
let kidsBedroom = TaskList(identify: "Youngsters Room", parentTaskList: reworking)
let livingRoom = TaskList(identify: "Dwelling Room", parentTaskList: reworking)
let administration = TaskList(identify: "Administration", parentTaskList: work)
let finance = TaskList(identify: "Finance", parentTaskList: work)
return [home, work, remodeling, kidsBedroom, livingRoom, management, finance]
}()
}
Nevertheless, I must customise the structure and interplay of every row, together with how the indications are dealt with. To facilitate this, I changed using OutlineGroup
with my very own customized views. The next three views make up that part – permitting for guardian/baby nesting.
Word at face worth it might seem to be these do not do a lot else over OutlineGroup. In my actual app these are extra sophisticated. It’s streamlined for the reproducible instance.
struct TaskListRowContentView: View {
@Atmosphere(.modelContext) personal var modelContext
@Bindable var taskList: TaskList
@State var isShowingEditor: Bool = false
init(taskList: TaskList) {
self.taskList = taskList
}
var physique: some View {
HStack {
Textual content(taskList.identify)
}
.contextMenu {
Button("Edit") {
isShowingEditor = true
}
}
.sheet(isPresented: $isShowingEditor) {
TaskListEditorScreen(existingList: taskList)
}
}
}
struct TaskListRowParentView: View {
@Bindable var taskList: TaskList
@State personal var isExpanded: Bool = true
var youngsters: [TaskList] {
taskList.taskLists!.sorted(by: { $0.identify < $1.identify })
}
var physique: some View {
DisclosureGroup(isExpanded: $isExpanded) {
ForEach(youngsters) { baby in
if baby.taskLists!.isEmpty {
TaskListRowContentView(taskList: baby)
} else {
TaskListRowParentView(taskList: baby)
}
}
} label: {
TaskListRowContentView(taskList: self.taskList)
}
}
}
struct TaskListRowView: View {
@Bindable var taskList: TaskList
var physique: some View {
if taskList.taskLists!.isEmpty {
TaskListRowContentView(
taskList: taskList)
} else {
TaskListRowParentView(taskList: taskList)
}
}
}
With these views outlined, I replace my ContentView
to make use of them as a substitute of the OutlineGroup
.
Checklist {
ForEach(taskLists.filter({$0.parentList == nil})) { listing in
TaskListRowView(taskList: listing)
}
}
With this transformation in place, I begin to expertise my situation inside SwiftUI. I’ll transfer the Finance
listing out of the Work listing by way of the editor and into the House
guardian. Throughout debugging, I can confirm that the modelContext.save()
name I’m doing instantly causes each of the guardian lists to replace. The work.taskLists
is lowered by 1 and the residence.tasksLists
array as elevated by 1 as anticipated. I can kill the app and relaunch and I see the finance listing as a toddler of the House listing. Nevertheless, I do not see this replicate in real-time. I’ve to kill the app to see the adjustments.
If I alter my save code in order that it manually updates the guardian array – the difficulty goes away and it really works as anticipated.
ToolbarItem {
Button("Save") {
listing.identify = viewModel.identify
listing.parentList = viewModel.parentTaskList
// Manually add to "House"
if let newParent = viewModel.parentTaskList {
newParent.taskLists?.append(listing)
}
strive! modelContext.save()
dismiss()
}
}
This has me confused. After I debug this, I can see that viewModel.parentTaskList.taskLists.rely
equals 2 (Transforming, Finance). I can print the contents of the array and each the Transforming
and Finance
fashions are in there as anticipated. Nevertheless, the UI does not work until I explicitly name newParent.taskLists?.append(listing)
. The listing already exists within the array and but I have to do that to ensure that SwiftUI to replace it is binding.
Why does my specific append
name clear up for this? I do not perceive how the array was mutated by SwiftData and the observing Views didn’t get notified of the change. My unique method (not manually updating the arrays) works effective in each unit/integration take a look at I run however I can not get SwiftUI to watch the array adjustments.
If somebody might clarify why that is the case and whether or not or not I will be required to manually replace the arrays going ahead I’d admire it.
I’ve the total supply code obtainable as a Gist for simpler copy/paste – it matches the contents of the publish.
Edit
One different factor that causes confusion for me is that the Finance
listing already exists previous to my manually appending it to the guardian listing array. So, regardless of my appending it anyway SwiftData is sensible sufficient to not duplicate it. With that being the case, I do not know if it is simply changing what’s in there, or saying “nope” and never including it. If it is not including it, then what’s notifying the observing Views that the information modified?