Search code examples
iosswiftswiftuirealmpicker

How to use SwiftUI Picker to update a Realm to-one relationship?


I have the following Realm schema where a Race is done on a Track:

final class Race: Object, ObjectKeyIdentifiable {
    @Persisted(primaryKey: true) var _id: ObjectId
    @Persisted var track: Track?
    @Persisted var duration: Int = 45
}

final class Track: Object, ObjectKeyIdentifiable {
    @Persisted(primaryKey: true) var _id: ObjectId
    @Persisted var name: String = "Imola"
    @Persisted var country: String = "🇮🇹"
    
    @Persisted(originProperty: "tracks") var group: LinkingObjects<TrackGroup>
}

final class TrackGroup: Object, ObjectKeyIdentifiable {
    @Persisted(primaryKey: true) var _id: ObjectId
    @Persisted var tracks = RealmSwift.List<Track>()
}

In my ContentView I have an Add Button that opens a sheet (AddRaceView). The new Race is already created when the sheet appears. Now, I want to use a Picker for the Track selection for our newly created Race.

The following code does not update the Track for the editable Race, and I do not understand why:

struct AddRaceView: View {
    
    @ObservedRealmObject var race: Race
    @ObservedRealmObject var trackGroup: TrackGroup
    
    var body: some View {
        Form {
            chooseTrackSection
            raceDurationSection
        }
    }
    
    @State private var trackPickerVisible = false
    
    var chooseTrackSection: some View {
        Section(header: Text("Track")) {
            Button {
                withAnimation(.easeIn) {
                    self.trackPickerVisible.toggle()
                }
            } label: {
                HStack {
                    Text(race.track?.name ?? "")
                    Spacer()
                    Image(systemName: "arrow.turn.right.down")
                }
            }
            if trackPickerVisible {
                // HERE: Selection is not processed.
                Picker(selection: $race.track, label: Text("Track")) {
                    ForEach(trackGroup.tracks) {
                        Text($0.name)
                    }
                }
                .pickerStyle(.wheel)
            }
        }
    }

Updating other values in Race (like duration) does work! When Track is a String for example, I can use the Picker to make a selection. The problem must be connected to the fact that I'm trying to change a Realm object/relationship.


Solution

  • There are three things that need to be taken into account:

    1. Picker needs to know where to find the values. The value can be specified manually by adding .tag(value) to the elements. While ForEach provides implicit tags for objects that conform to Identifiable, the type doesn't match in your case (needs to be Optional<Track> instead of Track).

    2. The Picker compares all tag values to the selection to find out which item is currently selected. The comparison fails if the objects are not from the same Realm instance. Unfortunately, there isn't currently any way to specify a Realm for ObservedResults or an ObservedRealmObject.

    3. Referencing objects from a frozen Realm doesn't work, so they (or their Realm) have to be thawed first.

    Code:

    // Fetch the Tracks from the Race's Realm by finding the TrackGroup by primaryKey
    if let tracks = race.realm?.thaw().object(ofType: TrackGroup.self, forPrimaryKey: trackGroup._id)?.tracks {
        Picker("Track", selection: $race.track) {
            ForEach(tracks) { track in
                Text(track.name)
                    .tag(Optional(track)) // Specify the value, making it optional to exactly match the expected type
            }
        }
    }