Mutating an array of model objects that is a child of a model object

Hi all,

In my SwiftUI / SwiftData / Cloudkit app which is a series of lists, I have a model object called Project which contains an array of model objects called subprojects:

final class Project1
{
    var name: String = ""
    
    @Relationship(deleteRule: .cascade, inverse: \Subproject.project) var subprojects : [Subproject]?
    
    init(name: String)
    {
        self.name = name
        self.subprojects = []
    }
}

The user will select a project from a list, which will generate a list of subprojects in another list, and if they select a subproject, it will generate a list categories and if the user selects a category it will generate another list of child objects owned by category and on and on.

This is the pattern in my app, I'm constantly passing arrays of model objects that are the children of other model objects throughout the program, and I need the user to be able to add and remove things from them.

My initial approach was to pass these arrays as bindings so that I'd be able to mutate them. This worked for the most part but there were two problems: it was a lot of custom binding code and when I had to unwrap these bindings using init?(_ base: Binding<Value?>), my program would crash if one of these arrays became nil (it's some weird quirk of that init that I don't understand at al).

As I'm still learning the framework, I had not realized that the @model macro had automatically made my model objects observable, so I decided to remove the bindings and simply pass the arrays by reference, and while it seems these references will carry the most up to date version of the array, you cannot mutate them unless you have access to the parent and mutate it like such:

project.subcategories?.removeAll { $0 == subcategory }
project.subcategories?.append(subcategory)

This is weirding me out because you can't unwrap subcategories before you try to mutate the array, it has to be done like above. In my code, I like to unwrap all optionals at the moment that I need the values stored in them and if not, I like to post an error to the user. Isn't that the point of optionals? So I don't understand why it's like this and ultimately am wondering if I'm using the correct design pattern for what I'm trying to accomplish or if I'm missing something? Any input would be much appreciated!

Also, I do have a small MRE project if the explanation above wasn't clear enough, but I was unable to paste in here (too long), attach the zip or paste a link to Google Drive. Open to sharing it if anyone can tell me the best way to do so. Thanks!

An array is a value type in itself even if the elements are reference types. So if you do

var myArray = project.subcategories ?? []

then any change to myArray will not affect project.subcategories but if you make a change to an object in myArray (a Subproject object?) then that object in the project.subcategories array will also be updated.

So I guess the short answer is that if you want to add or remove objects to the project you need to work with project.subcategories

I can't really help you with the rest because I don't really understand what you are saying in the paragraph after the last code snippet.

@joadan What you're saying about an array being a value type even if it has reference types makes sense to me. What I'm a little confused about is that when I pass subcategories by reference in the sample project I have (i.e. passing it as a parameter to a struct where it's declared as var subcategories: [Subcategory], it still does update when I mutate the array using project.subcategories?.removeAll { $0 == subcategory }.

Regarding what I was saying in the paragraph after the second block of code, I just meant that you can't unwrap the array like normal:

if var subcategories = self.project.subcategories { subcategories.removeAll { $0 == subcategory } }

and instead have to do this:

if let subcategories = self.project.subcategories { self.project.subcategories?.removeAll { $0 == subcategory} }

Just feels off to me.

You don’t need to unwrap anything so just skip the if let

self.project.subcategories?.removeAll { $0 == subcategory}

calling a method on something that is nil in swift is perfectly fine

@joadan but for self.project.subcategories?.removeAll { $0 == subcategory} you won't know if it didn't removeAll because subcategory didn't exist in the array, or if subcategories was nil to begin with because it didn't load from CloudKit / SwiftData.

That’s a completely different issue and it’s not something you checked either in the code you have provided

Mutating an array of model objects that is a child of a model object
 
 
Q