I am working on a Swift/SwiftUI app where at one point, I use a ForEach loop to display a certain amount of letters on the screen. I want to make it so that when the user types one of the letters displayed, that letter changes colors (and when they delete that letter, the color changes back). I have it working, for the most part, using the following code (WordDataModel is a Swift File, ShuffleView is the SwiftUI View File):
WordDataModel:
class WordDataModel: ObservableObject {
@Published var shuffleDisplay: Array<Character> = ["S", "T", "R", "E", "E", "T"]
@Published var typedLetters: [Character] = []
}
ShuffleView:
import SwiftUI
struct ShuffleView: View {
@EnvironmentObject var dm: WordDataModel
var body: some View {
HStack(spacing: 3) {
ForEach(0...dm.boardLength, id: \.self) { index in
let letter = dm.shuffleDisplay[index]
Text(String(letter))
.font(.system(size: 50))
.foregroundColor(dm.typedLetters.contains(letter) ? .green : .blue)
}
}
}
}
(There's other code in the data model but I think this is all the relevant code). This solution works with one issue, when I type the letter "e", both "e"s in the word "Street" change color. I want to change it so that when the user types "e" - only one of the "e"s in the displayed word changes color, when they type another one, the other "e" changes. (This would apply to all letters that have a duplicate in the displayed word).
Any help is greatly appreciated!
CodePudding user response:
First, count up how many of each letter was typed.
Then, iterate through the letters in shuffleDisplay
. For each letter, if its typed-count is greater than zero, make the letter green and decrement its typed-count.
You cannot do this work directly in ForEach
. SwiftUI makes no guarantees about how often or in what order ForEach
creates its content.
Instead, make a new type to hold both a letter and the color in which to display it:
fileprivate struct DisplayLetter {
var letter: Character
var color: Color
}
Then, add a method to WordDataModel
to return an array of DisplayLetter
by examining shuffleDisplay
and typedLetters
:
extension WordDataModel {
fileprivate func displayLetters() -> [DisplayLetter] {
var typeCounts: [Character: Int] = typedLetters.reduce(into: [:]) {
$0[$1, default: 0] = 1
}
var answer: [DisplayLetter] = []
for c in shuffleDisplay {
let count = typeCounts[c] ?? 0
typeCounts[c] = max(0, count - 1)
answer.append(.init(
letter: c,
color: count > 0 ? .green : .blue
))
}
return answer
}
}
Finally, change ShuffleView
to call this method and use the result in ForEach
:
struct ShuffleView: View {
@EnvironmentObject var dm: WordDataModel
var body: some View {
HStack(spacing: 3) {
let dLetters = dm.displayLetters()
ForEach(dLetters.indices, id: \.self) { i in
let dLetter = dLetters[i]
Text(String(dLetter.letter))
.font(.system(size: 50))
.foregroundColor(dLetter.color)
}
}
}
}