Home > Software engineering >  SwiftUI: Changing text color when specific letter is typed but avoiding duplicate color changes
SwiftUI: Changing text color when specific letter is typed but avoiding duplicate color changes

Time:01-21

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)
            }
        }
    }
}
  • Related