100 Days of SwiftUI Day 31

100 Days of SwiftUI – Day 31

It’s day 31 of the 100 Days of SwiftUI! Yesterday, we implemented our Word Scramble app and looked at some advanced SwiftUI features. Today, we’re reviewing what we’ve learned with this project and complete a few challenges. Let’s take a look at the challenges and how I solved them!

Word Scramble SwiftUI Challenge #1

Our first challenge is to disallow answers that are shorter than three letters or are just our starting word. For example, if our starting word is “coconut”, then we can’t enter “on” or “coconut” as answers.

I solved this challenge by modifying the addNewWord function. I’ve added two more guard statements. One to check if the answer is at least 3 characters long and one to check if our answer does not match our starting word (rootWord).

 guard answer.count >= 3 else {
            wordError(title: "Word too short.", message: "Please enter words that are at least 3 characters long!")
            return } // Checks if our answer is at least 3 characters long
        
        guard answer != rootWord else {
            wordError(title: "That's our starting word.", message: "That would be too easy, don't you think?") // Checks if our answer isn't the same as our starting word (rootWord)
            return
        }

That does it for the first challenge!

Word Scramble SwiftUI Challenge #2

Our second challenge is to add a toolbar button that calls startGame(), so users can restart with a new word whenever they want to.

I solved this by adding a .toolbar to our list and then adding a Button to that .toolbar that’s mapped to action: startGame.

.toolbar{
    Button("New Game", action: startGame)
}

Then, I modified the startGame function. Besides giving us a new word, it also has to reset our usedWords array.

usedWords = [String]() // Resets the usedWords array

Challenge number 2, done!

Word Scramble SwiftUI Challenge #3

Our third and final challenge is to put a text view somewhere so you can track and show the player’s score for a given root word.

I started by adding a State variable to our code for our score.

@State private var playerScore = 0 // Used to track the score

There are tons of ways to provide the user with a score, but I decided to go for the following approach: for each correct answer, a user gets 1 point + points equal to the length of the answer. For example: “goal” would net you 5 points. 1 point for the answer itself and 4 extra points because “goal” consists of 4 characters. This rewards users for giving longer, more difficult answers.

I added a simple calculateScore function to implement this.

func calculateScore() {
    playerScore+=(newWord.count + 1) // If a word is correct, the player earns 1 point + points equal to the length of the given word
    }

Next, I called this new function in our addNewWord function.

func addNewWord() {
// lots of code here

calculateScore()
}

All that remained to complete the third challenge was adding a textfield that displayed our score.

Section("Score") {
    Text("\(playerScore)")
}

To provide a good overview, here’s the app in full, challenges included:

import SwiftUI

struct ContentView: View {
    
    @State private var usedWords = [String]() // array that contains the words a user has entered
    @State private var rootWord = "" // The word we're spelling from
    @State private var newWord = "" // Used to clear the text field by putting in an empty String
    @State private var errorTitle = "" // Used as title for our alert in case of an error
    @State private var errorMessage = "" // Used as message for our alert in case of an error
    @State private var showingError = false // Used to control whether our error alert is shown
    @State private var playerScore = 0 // Used to track the score
    
    var body: some View {
        NavigationView {
            List {
                Section {
                    TextField("Enter your own word", text: $newWord)
                        .autocapitalization(.none) // removes auto capitialization of words for aesthetic purposes
                }
                
                Section {
                    ForEach(usedWords, id: \.self) { word in
                        HStack {
                            Image(systemName: "\(word.count).circle") // adds a number displaying how many letters our word contains in a circle (symbol)
                            Text(word) // loops through our usedWords array and displays it in the section.
                        }
                    }
                }
                
                Section("Score") {
                    Text("\(playerScore)")
                }
            }
            
            .navigationTitle(rootWord) // Title shown at the top of our app
            .onSubmit(addNewWord) // onSubmit means, when the user presses the enter key. In this case, when the press enter, run the addNewWord function so that the word that's entered get's added to the array.
            .onAppear(perform: startGame) // When the app starts, run function startGame
            .alert(errorTitle, isPresented: $showingError) {
                Button("OK", role: .cancel) {}
            } message: {
                Text(errorMessage)
            }
            .toolbar{
                Button("New Game", action: startGame)
            }
        }
    }
    
    func addNewWord() {
        let answer = newWord.lowercased().trimmingCharacters(in: .whitespacesAndNewlines) // Lowercases answer, trims whitespaces and line breaks
        
        guard answer.count > 0 else { return } // Checks if the String isn't empty
        
        guard answer.count >= 3 else {
            wordError(title: "Word too short.", message: "Please enter words that are atleast 3 characters long!")
            return } // Checks if our answer is at least 3 characters long
        
        guard answer != rootWord else {
            wordError(title: "That's our starting word.", message: "That would be too easy, don't you think?") // Checks if our answer isn't the same as our starting word (rootWord)
            return
        }
        
        guard isOriginal(word: answer) else { // calls the isOriginal and wordError function
            wordError(title: "Word used already.", message: "Be more original!")
            return
        }
        
        guard isPossible(word: answer) else { // calls the isPossible and wordError function
            wordError(title: "Word not possible.", message: "You can't spell that word from \(rootWord)")
            return
        }
        
        guard isReal(word: answer) else { // calls the isReal and wordError function
            wordError(title: "Word not recognized.", message: "You can't just make up words, you know!")
            return
        }
        
        withAnimation { // adds an animation
            usedWords.insert((answer), at: 0) // inserts at the start of the array so that it is shown on top of the list in the app
        }
        
        calculateScore()
        newWord = ""
    }
    
    func startGame() {
        if let startWorldsUrl = Bundle.main.url(forResource: "start", withExtension: "txt") { // check if our txt file can be found in the app bundle
            if let startWords = try? String(contentsOf: startWorldsUrl) { // if it's found, do something ->
                let allWords = startWords.components(separatedBy: "\n") // create an array of all items that are on a new line
                rootWord = allWords.randomElement() ?? "silkworm" // Nill coalescing shouldn't happen here, but just is necessary just in case
                usedWords = [String]() // Resets the usedWords array
                playerScore = 0 // Resets score to 0
                return
            }
        }
        
        fatalError("Could not load start.txt from bundle.") // Should not happen, but if it does, it will show this error and crash the app
    }
    
    func calculateScore() {
        playerScore+=(newWord.count + 1) // If a word is correct, the player earns 1 point + points equal to the length of the given word
    }
    
    func isOriginal(word: String) -> Bool {
        !usedWords.contains(word) // checks whether the array already contains a word and if it doesn't, returns false
    }
    
    func isPossible(word: String) -> Bool {
        var tempWord = rootWord // make a copy of our rootWord in a new variable
        
        for letter in word { // loops over the tempWord and removes a letter if it's found, so that it cannot be used again
            if let pos = tempWord.firstIndex(of: letter) {
                tempWord.remove(at: pos)
            } else {
                return false
            }
        }
        
        return true
    }
    
    func isReal(word: String) -> Bool { // checks whether text (in this case a word) is valid English
        let checker = UITextChecker() // create an instance of a UITextChecker
        let range = NSRange(location: 0, length: word.utf16.count) // The range off our text to be checked. Location 0 means start at the start. The length is the full length of the word variable we'll pass in when our function is called
        let misspelledRange = checker.rangeOfMisspelledWord(in: word, range: range, startingAt: 0, wrap: false, language: "en")
        
        return misspelledRange.location == NSNotFound
    }
    
    func wordError(title: String, message: String) { // function that allows us to easily set up errors depending on the context
        errorTitle = title
        errorMessage = message
        showingError = true
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

And that’s it! All three challenges completed and another day finished. This was the last of our 5 introductory projects and things will ramp up in difficulty from here. High time to rest up and recharge, then. I’ll be back with day 32 tomorrow!

Darryl

Hi! My name is Darryl and this is my personal blog where I write about my journey as I learn programming! You'll also find articles about other things that interest me including games, tech and anime.

Post navigation

Leave a Comment

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

100 Days of SwiftUI – Day 52

100 Days of SwiftUI – Day 54

100 Days of SwiftUI – Day 38

100 Days of SwiftUI – Day 90