Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to use Core Data Relationship in ForEach SwiftUI

Im facing issues with displaying my Core Data inside of SwiftUI because of relationships being Sets. What is the best way to go about displaying a many relationship set inside of a SwiftUI ForEach loop?

For instance, I have two entities in core date: Entry & Position. Entry can contain many positions, and each position can only belong to one entry.

I have the Entry as a @binding var on the view that is suppose to display the entry's Positions. I would ideally like to access the positions directly from this entry variable, but because positions are a Set, I get the following error:

error 'ForEach' requires that 'Set' conform to 'RandomAccessCollection'

@Binding private var entry: Entry?

ForEach(entry?.positions) { position in
    Text("position here")
}

Solution One:

Or, I could do a fetch request for all positions, then filter out all the ones that do not belong to the entity, I do not like this idea as I would be fetching potentially thousands of Positions to only get a few. (or am i thinking of this wrong? Im new to core data, coming from realm because of swiftui)

Although this could work if I could do a fetch ONLY on the @binding entry var, and fetch all its positions as sorted fetched results, but I'm not sure there is a way to do this.

Maybe like this, or would this be a performance issue if there was potentially thousands of entry's each with 10-20+ positions? and could objectID be used this way, and would it still be unique if the entry was moved into another journal?:

@Binding private var entry: Entry?

@FetchRequest(
    entity: Position.entity(),
    sortDescriptors: [],
    predicate: NSPredicate(formate: "entry.objectID == %@", self.entry.objectID)
) var positions: FetchedResults<Position>

Solution Two:

I thought of adding an attribute to positions like 'date', this way positions could be compared and sorted? But not sure how this could be updated with SwiftUI, as it would be done only once in the init().

let list = entry.wrappedValue?.positions?.sorted()

Core Data Models:

public class Entry: NSManagedObject, Identifiable {

    // MARK: - ATTRIBUTES
    @NSManaged public var date: Date

    // MARK: - RELATIONSHIPS
    @NSManaged public var journal: Journal?
    @NSManaged public var positions: Set<Position>?
}

public class Position: NSManagedObject, Identifiable {

    // MARK: - RELATIONSHIPS
    @NSManaged public var entry: Entry?
}

How would you go about solving this problem? Keep in mind on the view where the positions are being listed, that this view can add, delete, and modify positions, so the solution should allow SwiftUI to reload the view and update the positions when changes are made.

like image 276
Steve Da Monsta Avatar asked Nov 06 '19 23:11

Steve Da Monsta


Video Answer


1 Answers

@JoakimDanielson comment works like a charm, but requires a few tweaks.

Wrapping the set in the array initializer works like this, but requires optional sets to be unwrapped. I was surprised to find force unwrapping does not cause a crash even if the set is nil? Maybe someone could explain this?

ForEach(Array(entry.positions!)) { position in
   Text("Position")
}

The next issue was that the array would be randomized everytime the set of positions changed due to sets being unordered. So by conforming Position to the Comparable Protocol solved this. I decided it made the most sense to sort positions by date, so I updated the model like so:

public class Position: NSManagedObject, Identifiable {

    // MARK: - ATTRIBUTES
    @NSManaged public var date: Date

    // MARK: - RELATIONSHIPS
    @NSManaged public var entry: Entry?
}

extension Position: Comparable {

    static func < (lhs: Position, rhs: Position) -> Bool {
        lhs.date < rhs.date
    }
}

Then the ForEach could be sorted and looks like this:

ForEach(Array(entry.positions!).sorted()) { position in
   Text("\(position.date)")
}

Some other solutions I found but are not ideal for reasons mentioned in original post, but they do work, is to either use a fetch request customized inside the view init like so:

@FetchRequest var positions: FetchedResults<Position>

init(entry: Entry) {

    var predicate: NSPredicate?

    // Do some kind of control flow for customizing the predicate here.
    predicate = NSPredicate(formate: "entry == %@", entry)

    self._positions = FetchRequest(
        entity: Position.entity(),
        sortDescriptors: [],
        predicate: predicate
    )
}

or create an "middle man" View Model bound to @ObservedObject that converts core data managed objects to useable view data. Which to me makes the least sense because it will basically contain a bunch of redundant information already found inside the core data managed object, and I like the idea of the core data managed object also being the single source of truth.

like image 157
Steve Da Monsta Avatar answered Sep 22 '22 16:09

Steve Da Monsta