Reputation: 115
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
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 ButtonView
s 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 ButtonView
s', states are reset.
Upvotes: 2