Search code examples
swiftuicore-datarelationshiptextfieldxcode14

in SwiftUI, I have 2 Entities (A & B) in my CoreData with a relationship (one to many) between them, how can I fetch all attributes of B in TextFields


Let's say I have 2 entities:

  1. GameSession :which has Attributes "date", "place", "numberofplayer" + a relationship called "players" with "Player"
  2. Player: which has Attributes "name","score_part1","score_part2","score_part3" + a relationship with "GameSession"

the relationship is "one to many": One session can have many players

Let's say now I have a list of GameSession and when I click on on one (with a NavigationLink) It sends me to a new view where I can see: All the names of the players of that session (in text) and also right next to the player name I would like to have 3 TextField in which I can enter (an update) "score_part1","score_part2","score_part3" for every players of that session

Basically I am able to display the name of all the players of a given session, But it seems impossible to have the "score_part1","score_part2","score_part3" in editable TextField...

I have an error saying "Cannot convert value of type 'String' to expected argument type 'Binding<String>'"

Basically in my first view I have something like that:

struct RamiListePartieUIView: View {@Environment(.managedObjectContext) var moc@FetchRequest(entity: GameSession.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \GameSession.date, ascending: false)]) var gamesessions: FetchedResults<GameSession>

var body: some View {
    
    VStack {
        List {
            ForEach(gamesessions, id: \.date) { session in
                NavigationLink (destination: DetailPartieSelecUIView(session: session)){
                    Text("\(session.wrappedPlace) - le \(session.wrappedDate, formatter: itemFormatter) ")
                }
                
            }
            .onDelete(perform: deleteSessions)
            .padding()
        }
    }
}
}

And in my second view I have something like that:

struct DetailPartieSelecUIView: View {
    @State var session:GameSession
    @Environment(\.managedObjectContext) var moc
    
    
    var body: some View {
        
        
        Section("Ma session du \(session.wrappedDate, formatter: itemFormatter)"){
            
            ForEach(session.playersArray, id: \.self) { player in
                
                HStack {
                    Text(player.wrappedName)       //  OK it works
                    TextField("score", text : player.wrappedScore_part1) //  it generates an error
                    TextField("score", text : player.wrappedScore_part2) //  it generates an error
                    TextField("score", text : player.wrappedScore_part3) //  it generates an error
                    
                }
            }
        }
    }
}

private let itemFormatter: DateFormatter = {
            let formatter = DateFormatter()
        //    formatter.dateStyle = .short
        //    formatter.timeStyle = .medium
            formatter.dateFormat = "YYYY/MM/dd" //"YY/MM/dd"
            return formatter
}()

also,
I have defined the "wrappedScore_part1","wrappedScore_part2","wrappedScore_part3" in the Player+CoreDataProperties.swift file

and "wrappedPlace", "wrappedData" as well as the "PlayersArray" in the GameSession+CoreDataProperties.swift file

it is done like that:

public var wrappedPlace: String {
    place ?? "Unknown"
}

// Convert NSSet into an array of "Player" object
public var playersArray: [Player] {
    let playersSet = players as? Set<Player> ?? []

    return playersSet.sorted {
        $0.wrappedName< $1.wrappedName
    }
}

I am new at coding with swiftUI so I am probably doing something wrong... If anyone can help me it would be much appreciated.

Thanks a lot

I have tried a lot of things. Like changing the type of my attribute to Int32 instead os String. As I am suppose to enter numbers in those fields, I thought it would be best to have Integer. But it didn't change anything. and ultimately I had the same kind of error message

I tried also to add the $ symbol, like that:

TextField("score", text : player.$wrappedScore_part1)

But then I had other error message popping up at the row of my "ForEach", saying "Cannot convert value of type '[Player]' to expected argument type 'Binding'" And also on the line just after the HStack, I had an error saying "Initializer 'init(_:)' requires that 'Binding' conform to 'StringProtocol'"

Thank you for your help! Best regards, JB


Solution

  • Your first problem of how to fetch the players in a session you need to supply a predicate to the @FetchRequest<Player>, e.g.

    @FetchRequest
    private var players: FetchedResults<Player>
    
        init(session: Session) {
            let predicate = NSPredicate(format: "session = %@", session)
            let sortDescriptors = [SortDescriptor(\Player.timestamp)] // need something to sort by.
            _players = FetchRequest(sortDescriptors: sortDescriptors, predicate: predicate)
        }
    

    That acts like a filter and will only return the players that have the session relation equalling that object. The reason you have to fetch like this is so any changes will be detected.

    The second problem about the bindings can be solved like this:

    struct PlayerView: View{
        @ObservedObject var player: Player {
    
        var body:some View {
            if let score = Binding($player.score) {
                TextField("Score", score)
            }else{
                Text("Player score missing")
            }
        }
    }
    

    This View takes the player object as an ObservedObject so body will be called when any of its properties change and allows you to get a binding to property. The Binding init takes an optional binding and returns a non-optional, allowing you to use it with a TextField.