WWDC22 SALE: Save 50% on all my Swift books and bundles! >>

How to deal with object hierarchies and relationships using structs?

Forums > Swift

@mh175  

Swift puts a lot of emphasis on value types, but it seems that it's impossible to create relationships to other objects without resorting to classes. Almost 0 tutorials I've seen online deal with this. Once you use classes and need to archive hierarchies you then need to use NSKeyedArchiver, which means you then need to subclass NSObject and adopt NSCoding (actually NSSecureCoding -- it's now required by Apple). You lose a lot of Swift goodies.

// Value semantics breaks down

struct Dog { }

struct Person {
    var favoriteDog: Dog
}

struct House {
    var dogs = [Dog]()
    var people = [Person]()
}

I've tried various solutions to keep value semantics all the way down by referencing a unique identifier similar to how a database works, but this involves a lot of bookkeeping. It also means you need to pass the entire data model everywhere so you have access to the repository (similar to Core Data's context).

// Store Identifiers

struct Dog {
    let id = UUID()
}

struct Person {
    var favoriteDogID: UUID
}

struct House {
    var dogs = [Dog]()
    var people = [Person]()

    func dog(id: UUID) -> Dog! {
        return self.dogs.first(where: { $0.id == id } )
    }

    mutating func updateDog(_ dog: Dog) {
        var dogs = self.dogs
        let index = self.dogs.firstIndex(where: { $0.id == dog.id } )!
        dogs[index] = dog
        self.dogs = dogs
    }
}

Has anyone found an elegant solution for this? It's a common scenario that goes unaddressed.

1      

You defined a peculiar example with a one-to-many relationship between dogs and people, i.e., each dog can have one or more people for whom that dog is their favorite, but a person cannot exist without a favorite dog. If that's your intention, simply move the people property from the House struct to the Dogs struct. Then you won’t need an id.

But I suspect your example is artificial and abstracted too far from the problem you intend to solve. If you have a real problem in mind, it would help to explain that. And if you need a many-to-many relationship, I expect you should define a relational database instead of trying to simulate one.

1      

hi,

bob makes a decent point: if you're really working with essentially a one-to-many relationship, where the one "owns" the many, put one struct inside the other, although the size of that struct might become prohibitive to copy around, perhaps. but this may not be exactly your situation; and frankly, storing an id in one struct in order to locate a related struct somewhere else is not unique to structs.

for example, Apple has posted some sample code on how to configure Core Data with both a "cloud" and a "local" store, yet still implement a relationship from an object in one of those stores to one in the other store. the key is that an object in one store maintains the id of a related object in the other store, and finding one from the other is implemented using a Core Data fetch (essentially, an optimized firstIndex function).

if you do wish to go the id/lookup route, e.g., to find the person that owns a given dog (with a given id), you could do a similar firstIndex lookup of the people array; but you could also implement a dictionary of type [UUID : Int] in your House struct to quickly locate the correct index within the people array for the dog having the given id. yes, the dictionary would require a little maintenance, but mostly just for adds and deletes, and it would not be much work even if you rebuild it when necessary:

func rebuildLookupDictionary() {
  lookupDictionary.removeAll()
  for index in people.indices {
    lookupDictionary[person[index].favoriteDogID] = index
  }
}

func person(owning dog: Dog) -> Person {
  let index = lookupDictionary[dog.id]!
  return people[index]
}

hope that helps,

DMG

1      

@mh175  

You guys are the best. 🙏 Thank you for taking the time to answer. OK so I'm not totally off track here. Phew. This makes me feel better.

It does seem like I am trying to recreate a relational database but Core Data would be far overkill for my purposes.

1      

Hacking with Swift is sponsored by Emerge

SPONSORED Why are Swift reference types bad for app startup time, and what’s the performance cost of protocol conformances? That’s just a couple of the topics you can learn about on the Emerge blog — written by the app performance experts behind Emerge’s advanced app optimization and monitoring tools, based on their experience of working at companies like Apple, Airbnb, Snap, and Spotify.

Find out more

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.