1

I'm building a game where players (locally) need to describe a number words to their teammates, who in turn guess as many words as possible within a given time limit. This means I have to let them create teams and add players to those teams.

I've set up Core Data and created a SwiftUI view for creating teams and adding players to those teams: this works. I'm now building a view to edit the team properties, but I'm struggling with data validation using the .disabled modifier. I want to make sure that the 'Save' button gets disabled when less than 2 people are in a team and when any of the fields (team name or any of player names) are empty.

What happens is that when editing the team name (a direct property of the Team entity), my .disabled modifier gets triggered as planned, and thus I can check whether it's empty or not. However, when editing any of the player names, the function passed to the .disabled modifier does not trigger, so I can't check if the player name is empty or not.

Below is my code:

struct EditTeamsView: View {
    @Environment(\.presentationMode) var presentationMode
    @Environment(\.managedObjectContext) var managedObjectContext
    
    @ObservedObject var existingTeam: Team
    
    var body: some View {
        ZStack {
            Color.init(UIColor.systemGroupedBackground)
                .edgesIgnoringSafeArea(.all)
            
            VStack {
                Form {
                    // Team name textfield
                    Section(footer: Text("A team needs a minimum of 2 players, and can have a maximum of 5 players.")) {
                        TextField("Team name...", text: $existingTeam.name ?? "")
                    }
                    
                    Section {
                        ForEach(existingTeam.playerArray, id: \.self) { player in
                            TextField("Player name...", text: Binding(
                                        get: {
                                            player.wrappedName
                                        }, set: { newValue in
                                            player.name = newValue
                                        }))
                        }
                        .onDelete(perform: deletePlayers)
                    }
                }
                
                StockFullWidthButton(action: {
                    saveChanges()
                    self.presentationMode.wrappedValue.dismiss()
                }, label: "Save Changes")
                .disabled(!isInputValid())
            }
        }
        .navigationBarTitle("Edit Team", displayMode: .inline)
    }
    
    func deletePlayers(at offsets: IndexSet) {
        for index in offsets {
            let playerToRemove = existingTeam.playerArray[index]
            existingTeam.removeFromPlayers(playerToRemove)
        }
    }
    
    func isInputValid() -> Bool {
        print("Input is checked")
        
        guard !existingTeam.name!.isEmpty else { return false }
        
        guard existingTeam.playerArray.count >= 2 else { return false }
        
        for player in existingTeam.playerArray {
            if player.name!.isEmpty {
                return false
            }
        }
        
        return true
    }
    
    func saveChanges() {
        if managedObjectContext.hasChanges {
            try? self.managedObjectContext.save()
        }
    }
}

For the team name TextField binding I used this operator overload (taken from another post on SO). This works nicely for the team name textfield, but doesn't work for the player names.

func ??<T>(lhs: Binding<Optional<T>>, rhs: T) -> Binding<T> {
    Binding(
        get: { lhs.wrappedValue ?? rhs },
        set: { lhs.wrappedValue = $0 }
    )
}

That's why I used a custom binding for the player names textfields: Binding(get: { player.wrappedName }, set: { newValue in player.name = newValue })

Any idea on how to fix this? Thanks in advance!

dvb98
  • 13
  • 3

1 Answers1

0

If you want to update view on some condition you need to have explicit state for that and manage that state manually. In provided code you rely that view will be validated on refresh, but refresh does not happen because no dependency changed.

Here is possible solution - to have explicit state for bad input and trigger it explicitly when required condition changed

@ObservedObject var existingTeam: Team

@State private var isBadInput = false     // << this !!
var body: some View {
    ZStack {
        Color.init(UIColor.systemGroupedBackground)
            .edgesIgnoringSafeArea(.all)
        
        VStack {
            Form {
                // Team name textfield
                Section(footer: Text("A team needs a minimum of 2 players, and can have a maximum of 5 players.")) {
                    TextField("Team name...", text: $existingTeam.name ?? "")
                       .onReceive(existingTeam.$name) { _ in
                           self.isBadInput = !isInputValid()   // << updated !!
                       }
                }
                
                Section {
                    ForEach(existingTeam.playerArray, id: \.self) { player in
                        TextField("Player name...", text: Binding(
                                    get: {
                                        player.wrappedName
                                    }, set: { newValue in
                                        player.name = newValue
                                    }))
                          .onReceive(player.objectWillChange) { _ in
                               self.isBadInput = !isInputValid()   // << updated !!  
                          }
                    }
                    .onDelete(perform: deletePlayers)
                }
            }
            
            StockFullWidthButton(action: {
                saveChanges()
                self.presentationMode.wrappedValue.dismiss()
            }, label: "Save Changes")
            .disabled(isBadInput)         // << used !!
        }
    }
    .navigationBarTitle("Edit Team", displayMode: .inline)
}
Asperi
  • 228,894
  • 20
  • 464
  • 690
  • Thanks a lot for your help! However, I get the following errors: Cannot convert value of type '() -> ()' to expected argument type '(_.Output) -> Void' & Value of type 'Player' has no member '$name' – dvb98 Aug 31 '20 at 06:38
  • See updated, code is typed right here, because original one is not testable, so some adapting might be required. – Asperi Aug 31 '20 at 06:45