Jack Bodine
Jack Bodine

Reputation: 115

Child subviews not resetting when a new parent is created in SwiftUI

I made a post about this yesterday and apologize for it not being clear or descriptive enough. Today I've made some more progress on the problem but still haven't found a solution.

In my program I have a main view called GameView(), a view called KeyboardView() and a view called ButtonView().

A ButtonView() is a simple button that displays a letter and when pressed tells the keyboardView it belongs to what letter it represents. When it's pressed it is also toggled off so that it cannot be pressed again. Here is the code.

struct ButtonView: View {
    
    let impactFeedbackgenerator = UIImpactFeedbackGenerator(style: .medium)
    var letter:String
    var function:() -> Void
    @State var pressed = false
    
    var body: some View {
        ZStack{
            
            Button(action: {
                if !self.pressed {
                    self.pressed = true
                    
                    self.impactFeedbackgenerator.prepare()
                    self.impactFeedbackgenerator.impactOccurred()
                    
                    self.function()
                }
            }, label: {
                if pressed{
                    Text(letter)
                    .font(Font.custom("ComicNeue-Bold", size: 30))
                    .foregroundColor(.white)
                    .opacity(0.23)
                } else if !pressed{
                    Text(letter)
                    .font(Font.custom("ComicNeue-Bold", size: 30))
                    .foregroundColor(.white)
                }
            })
        }.padding(5)
    }
}

A keyboard view is a collection of ButtonViews(), one for each button on the keyboard. It tells the GameView what button has been pressed if a button is pressed.

struct KeyboardView: View {
    
    @Environment(\.parentFunction) var parentFunction
    
    var topRow = ["Q","W","E","R","T","Y","U","I","O","P"]
    var midRow = ["A","S","D","F","G","H","J","K","L"]
    var botRow = ["Z","X","C","V","B","N","M"]
    
    
    var body: some View {
         VStack{
    
            HStack(){
                ForEach(0..<topRow.count, id: \.self){i in
                    ButtonView(letter: self.topRow[i], function: {self.makeGuess(self.topRow[i])})
                }
            }
            
            HStack(){
                ForEach(0..<midRow.count, id: \.self){i in
                    ButtonView(letter: self.midRow[i], function: {self.makeGuess(self.midRow[i])})
                }
            }
            
            HStack(){
                ForEach(0..<botRow.count, id: \.self){i in
                    ButtonView(letter: self.botRow[i], function: {self.makeGuess(self.botRow[i])})
                }
            }
        }
    }
    
    func makeGuess(_ letter:String){
        print("Keyboard: Guessed \(letter)")
        self.parentFunction?(letter)
    }
}

Finally a GameView() is where the keyboard belongs. It displays the keyboard along with the rest of the supposed game.

struct GameView: View {
    
    @Environment(\.presentationMode) var presentation
    
    @State var guessedLetters = [String]()
    @State var myKeyboard:KeyboardView = KeyboardView()
    
    var body: some View {
        ZStack(){

            Image("Background")
                .resizable()
                .edgesIgnoringSafeArea(.all)
                .opacity(0.05)
            
            VStack{
                Button("New Game") {
                        
                    self.newGame()
                        
                }.font(Font.custom("ComicNeue-Bold", size: 20))
                    .foregroundColor(.white)
                    .padding()
                
                self.myKeyboard
                    .padding(.bottom, 20)

            }
        }.navigationBarTitle("")
            .navigationBarBackButtonHidden(true)
            .navigationBarHidden(true)
            .environment(\.parentFunction, parentFunction)
    }
    
    func makeGuess(_ letter:String){
        self.guessedLetters.append(letter)
    }
    
    func newGame(){
        print("Started a new game.")

        self.guessedLetters.removeAll()
        self.myKeyboard = KeyboardView()
    }
    
    
    func parentFunction(_ letter:String) {
        makeGuess(letter)
    }
}

struct ParentFunctionKey: EnvironmentKey {
    static let defaultValue: ((_ letter:String) -> Void)? = nil
}

extension EnvironmentValues {
    var parentFunction: ((_ letter:String) -> Void)? {
        get { self[ParentFunctionKey.self] }
        set { self[ParentFunctionKey.self] = newValue }
    }
}

The issue is that when I start a new game, the array is reset but not keyboardView(), the buttons that have been toggled off remain off, but since it's being replaced by a new keyboardView() shouldn't they go back to being toggled on?

Upvotes: 3

Views: 285

Answers (1)

New Dev
New Dev

Reputation: 49580

I'll repeat what I said in an answer to your previous question - under most normal use cases you shouldn't instantiate views as variables, so if you find yourself doing that, you might be on the wrong track.


Whenever there's any state change, SwiftUI recomputes the body and reconstructs the view tree, and matches the child view states to the new tree.

When it detects that something has changed, it realizes that the new child view is truly new, so it resets its state, fires .onAppear and so forth. But when there's no change that it can detect, then it just keeps the same state for all the descendent views.

That's what you're observing.

Specifically, in your situation nothing structurally has changed - i.e. it's still:

GameView
 --> KeyboardView
      --> ButtonView
          ButtonView
          ButtonView
          ...

so, it keeps the state of ButtonViews as is.

You can signal to SwiftUI that the view has actually changed and that it should be updated by using an .id modifier (documentation isn't great, but you can find more info in blogs), where you need to supply any Hashable variable to it that's different than the current one, in order to reset it.

I'll use a new Bool variable as an example:

struct GameView {
  @State var id: Bool = false // initial value doesn't matter

  var body: some View {
      VStack() {
         KeyboardView()
            .id(id)  // use the id here
         Button("new game") {
            self.id.toggle() // changes the id
         }
      }
  }
}

Every time the id changes, SwiftUI resets the state, so all the child views', like ButtonViews', states are reset.

Upvotes: 2

Related Questions