UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

[Experiment] Word Scramble - Revisited

Forums > 100 Days of SwiftUI

Important Note: If you have not completed this project yet and its challenges, please do not read this post. This is just some silly fun I was having. Most of the things I am doing are a bit of overkill.

I know this project didn't really need everything I did for the experiment, but I thought it would be fun to do it, and discuss things here. I am still looking to expand it more by adding some Core Data, and tab that shows a list of previous "rootWords" with a total score for each one. Also considering the possibility of making it a game that you determine how many words you want to work with and get a total score for that.

Finally, I will split things in multiple comments for ease of use. (I will post this to github and share a link for anyone that wants to have fun with it).

To start with here's my ContentView:

struct ContentView: View {

    @State private var rootWord = ""
    @State private var inputWord = ""
    @State private var usedWords =  [String]()

    @State private var errorAlert = ErrorAlert.notValid
    @State private var showingWordError = false

    @State private var showingScoreDetails = false

    @State private var userScore = 0.0

    var body: some View {

        NavigationView {
            VStack {
                TextField("Enter Word", text: $inputWord, onCommit: addNewWord)
                    .textFieldStyle(RoundedBorderTextFieldStyle())
                    .padding()
                    .autocapitalization(.none)

                List(usedWords, id: \.self) { word in
                    Image(systemName: "\(word.count).circle")
                        .foregroundColor(.green)
                    Text(word)
                }

                Text("Currrent Word Score is: \(userScore, specifier: "%g")")
                    .font(.headline)
                    .foregroundColor(.purple)
                    .padding()
            }
            .navigationBarTitle(rootWord, displayMode: .inline)
            .navigationBarItems(leading:
                                    PlayButton(action: newGame),
                                trailing:
                                    InfoButton(action: { showingScoreDetails = true })
                                )
            .onAppear(perform: newGame)
            .alert(isPresented: $showingWordError) { errorAlert }
        }
        .alert(isPresented: $showingScoreDetails) { ScoreDetails.alert }
    }
      // functions go here... shown in next comment
}

3      

Nothing too special here really. The idea for now was to simplify the modifiers at the end of body... here are the functions:

    func newGame() {
        rootWord = GameBrain.allWords.randomElement() ?? "silkworm"
        usedWords = []
        userScore = 0.0
    }

    func updateScore(with word: String) {
        let lengthScore = Double(word.count - 2)
        let totalWordsUsedScore = Double(usedWords.count) * 1.5
        userScore += lengthScore + totalWordsUsedScore
    }

Again, nothing surprising, I will be showing GameBrain in another comment. The fun part for me was in this function:

    func addNewWord() {
        let answer = inputWord.lowercased().trimmingCharacters(in: .whitespacesAndNewlines)

        guard answer.count > 0 else { return }

        switch GameBrain.check(answer, with: rootWord, and: usedWords) {
        case .good:
            usedWords.insert(answer, at: 0)
            updateScore(with: answer)
            inputWord = ""
            return
        case .notValid:
            errorAlert = ErrorAlert.notValid
        case .notReal:
            errorAlert = ErrorAlert.notReal
        case .notOriginal:
            errorAlert = ErrorAlert.notOriginal
        case .notPossible:
            errorAlert = ErrorAlert.notPossible
        }
        showingWordError = true
        inputWord = ""
    }

I positioned the "good" case first so it becomes clear that we only continue if if the word is not good. Re-ordering would make it a mess to understand...

3      

I decided to remove the alerts into their own enum:

enum ErrorAlert {

    static let notValid = Alert(title: Text("Nope!"),
                                message: Text("You can do better than that!"),
                                dismissButton: .default(Text("Ok")))

    static let notReal = Alert(title: Text("Word not possible"),
                               message: Text("That isn't a real word!"),
                               dismissButton: .default(Text("Ok")))

    static let notOriginal = Alert(title: Text("Word used already!"),
                                   message: Text("Be more original"),
                                   dismissButton: .default(Text("Ok")))

    static let notPossible = Alert(title: Text("Word not recognized!"),
                                   message: Text("You can't just make them up you know!"),
                                   dismissButton: .default(Text("Ok")))
}

Like I said, this is overkill... but I like the idea of creating these things as global constants. I can change them in one easy place without having to try and remember where they are.

I create a new Swift file for this, called ErrorAlerts. Now I know where to change them 😉

4      

Before going on to the brains, I wanted to add the buttons I used for the navigationBarItems. These are in the same file called NavButtons

struct InfoButton: View {

    let action: () -> Void

    var body: some View {
        Button(action: action) {
            Icon.info
            Text("Scoring")
        }
    }
}

struct PlayButton: View {

    let action: () -> Void

    var body: some View {
        Button(action: action) {
            Icon.play
            Text("Next")
        }
    }
}

I usually place all SF Symbols I will be using in one file called Icons. If I have more than one type of icon, I place them in different enums. For example, if I have a tab view, in that file I will create an enum called TabIcon and another Icon that is more generic.

enum Icon {
    static let play = Image(systemName: "play.fill")
    static let info = Image(systemName: "info.circle")
}

Note here I used the same name as the symbol because it makes sense. If they were for a TabIcon for example, I would call one home (not house like the symbol) and so on...

4      

One last piece and on to the brains... I promise. This is truly overkill right here... but again... this whole thing was just for fun:

enum ScoreDetails {
    static let title = Text("Score Details")

    static let message = Text("""
                Each Word Gives 1.5 pts
                ________________

                Length Score = (# of letters - 2) pts
                """
    )

    static let alert = Alert(title: ScoreDetails.title, message: ScoreDetails.message, dismissButton: .default(Text("OK")))
}

This is the alert that shows the player how they are scored. Something I learned, was the amount of indentation actually matters. Xcode will complain until you indent all lines (including empty ones) by enough levels.

3      

This is in the same file as the brain, but placed separately, in case I need it later:

enum WordCheck {
    case notValid, notReal, notOriginal, notPossible, good
}

It works well for the brain:

enum GameBrain {

    static let allWords = fetchWords()

    static func fetchWords() -> [String] {
        if let wordsURL = Bundle.main.url(forResource: "start", withExtension: "txt") {
            if let fileContent = try? String(contentsOf: wordsURL) {
                let allWords = fileContent.components(separatedBy: "\n")
                return allWords
            } else {
                fatalError("Could not convert to string")
            }
        } else {
            fatalError("Could not find file URL")
        }
        return []
    }

    static func check(_ answer: String, with rootWord: String, and usedWords: [String]) -> WordCheck {
        // Is it valid?
        guard answer.count > 2 else { return .notValid }
        guard answer != rootWord else { return .notValid }

        let checker = UITextChecker()
        let range = NSRange(location: 0, length: answer.utf16.count)
        let misspelledRange = checker.rangeOfMisspelledWord(in: answer, range: range, startingAt: 0, wrap: false, language: "en")

        // is it real?
        guard misspelledRange.location == NSNotFound else { return .notReal }

        // is it original?
        guard !usedWords.contains(answer) else { return .notOriginal }

        // is it Possible?
        var tempWord = rootWord
        for letter in answer {
            if let pos = tempWord.firstIndex(of: letter) {
                tempWord.remove(at: pos)
            } else {
                return .notPossible
            }
        }
        // all clear
        return .good
    }
}

3      

Here's the git repo: https://github.com/MarcusKay/HWS_Fun

Now, the thing that annoyed me the most to be honest, is having 4 separate functions for checking a word, then calling that inside addNewWord and each time setting an alert there. All that code in a place I didn't want it to be...

But you might have noticed that this isn't much of an experimentation... is it? Well, the real experiment for me was in setting things up for future expansion. In other words, I wanted to see if I could do things in such a way that it wouldn't be messy when I change things, or add features such as persistence, saving high score... showing performance metrics... adding levels maybe...

Anyway... I didn't want to go through tutorials, but was in the mood to code.. so I did this... whatever your thoughts I'd love to hear them...

Cheers.

4      

This is cool, and thanks for posting --- because I'll be looking at it when I get to the point of conentraing on design patterns! thanks!

3      

Hacking with Swift is sponsored by RevenueCat

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Learn more here

Sponsor Hacking with Swift and reach the world's largest Swift community!

Archived topic

This topic has been closed due to inactivity, so you can't reply. Please create a new topic if you need to.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.