Building an anagram game with Swift UI

100 Days of SwiftUI – Day 30 – Learning Swift Day 216

Adding to a list of words

Inside ContentView.swift

  • create three @State private variable properties, an empty array of Strings and two empty Strings.
  • Inside the body property create a NavigationView, with a VStack inside it.
    • Give it a TextField with a placeholder text of "Enter your word" and a text: property of $newWord, which is a <Binding>String, and a List generated from the usedWord array, managed by the id: \.self and with Text($0) as return result. Finally, give the navigation view a .navigationBarTitle modifier with rootWord as argument.
      • Add the .textFieldStyle() modifier passing it a fresh instance of the RoundedBorderTextFieldStyle() struct along with adding .padding()around the Text Field.
  • Create a new method called addNewWord() that accepts no parameter and returns no value. Inside it, create a constant called answer that is the result of chain-calling the .lowercased() and .trimmingCharacters(in: .whitespacesAndNewlines) on newWord. Then guard check that the resulting String is longer than 0, otherwise return from the method. Leave some space for some future adds, then call .insert(answer, at: 0) on usedWords, before resetting newWord to an empty String.
  • Add the onCommit: parameter at the end of the TextField and pass addNewWord (without parentheses) as argument. Then add the .autocapitalization(.none) modifier to it.
  • Inside the List, add an Image above the Text, which implicitly creates a HStack, and pass the systemName: "\($0.count).circle)" argument as its only argument. This has the result of creating an image taken from SF Symbols which draws a circle with the number of the word’s letter-count.

Running code when our app launches

  • Drag the downloaded “start.txt” file into the Project Navigator. I put it just under the Info.plist file. My Xcode window behaved properly but, if yours does not, choose the “Create groups” and “Copy items if needed” options.
  • Create the startGame() method. Paul doesn’t specify where to put it, which should in theory mean inside the ContentView struct, but I will add an extension after the struct to keep the code clean. I wonder how this is preventing us from getting to the same place where we were with Massive View Controllers, which was the main issue with UIKit and MVC. This seems to be MVVM (?) but still, we wrote close to nothing and we are already at 100 lines of code… bah… Now, inside this method:
    • Conditionally bind (if let) the URL for the resource “start” with extension “txt” from our main app bundle to the constant startWordsURL.
    • If this succeeds, conditionally bind the return result of the String initialiser contentsOf: (with a try? before as this is a throwing initialiser) to the startWords constant, passing it the startWordsURL parameter.
    • if also this succeeds, store the .components of this String separated by the newline ("\n") character into a new array of strings called allWords. Then set our rootWord state property to be a random element from that array or the placeholder word “silkworm”!
    • at this point return. I wonder why this return keyword is necessary, but I think that it is because we don’t want to use multiple else statements after the conditional binding and we can just have a single return here, to make us exit the method.
    • After closing all braces apart from the top one, call a fatalError() passing in “Could not load start.txt from bundle” as its message parameter. This will make our app crash if a problem is encountered so far.
  • To actually call this function when the view appears we need to call this from within a modifier set to our Navigation View. Add the onAppear(perform:) modifier after setting the Navigation View title and pass it startGame, without parentheses.

Now, a bit of reflection because things can easily confuse: this should be the same as calling the method from viewDidAppear(), right? I have kind of never used that in my very short and small experience, as I was taught to put this kind of logic in viewDidLoad(). My guts says this made it happen before but, still, using all these modifiers is kind of meh to me, I wonder how long the “new effect” of SwiftUI will last and what will be of it when people start realising that yes, it’s nice, it’s shiny… but some ways of doing things in it are really awkward and require a lot more thinking than in UIKit.

Validating words with UITextChecker

Inside the extension created for the Gameplay methods write the following methods:

  • isOriginal(word: String) -> Bool, which accepts a string parameter and returns a Boolean. Inside, just write !usedWords.contains(word). As you can see we are not using the return keyword because this has been lifted from us in Swift 5.1 if we have only one line of executable code. That line reads: “The used words array does not contain the passed word. True or false?”.
  • isPossible(word: String) -> Bool. This method creates a copy of our root word (so that modifying it won’t damage the original), then loops over each letter in the passed word, trying to conditionally bind the first index such letter in the copy of our word if found to a position constant, removing it from the temporary word string as a consequence. If this doesn’t match/succeeds at any point, we just return false. If we ended the loop successfully, the code will continue its execution and return true.
  • isReal(word: String) -> Bool. Create an instance of UITextChecker(), an instance of NSRange with 0 as location and the count of the letters of our word parameter in UTF16 encoding before storing the result of the call of the rangeOfMisspelledWord(in:range:startingAt:wrap:language:) method on the checker passing it our word, range, 0, false and “en” as parameters. Finally, return the result of checking whether this misspelled range’s location is equal to the magic number NSNotFound or not. If not found is equal to true then we succeeded! Phew! 😅

Add now three @State properties to control the showing of alerts (this abuse of properties is something I really don’t like of this new programming way — and no, I’m not being sentimentalist…). Two of them will be empty strings (errorTitle and errorMessage) and one will be a Boolean set to false (showingError).

Create a new method called wordError() which will accept two string parameters (message and title) and which will set errorTitle to its title parameter, do the same for errorMessage and message and set showingError to true.

After the .onAppear modifier add a .alert(isPresented) modifier bound to the $showingError state property. Inside the closure, create an Alert passing Text(errorTitle) as first parameter, Text(errorMessage) as second and .default(Text("OK"))) as third. What an awkward way of showing alerts… I miss my UIAlertController… 😞

Now, as last thing, add three guard statements in the addNewWord() method, calling the three checking methods as conditions and, inside the else part, calling wordError with sensible messages and returning. I now see the reason for all those properties but, still, it looks a bit awkward all this (not that I could make it any better, let’s be clear!).

Good, another app done, Day 30 completed.

So, we are now at somewhere between 110 and 130 lines of code for this, according to how many comments and code organisation you implemented. The UIKit version is similar in length but it is much clearer to me in intent, even if the syntax may look more awkward. Also, right now, the app is not progressing, we are stuck with that word forever, which is not nice. Is this for our challenges? Bah …, the original project (Project 5 of the UIKit version) was going on and on alone…

Am I skeptic? Possibly… Just I am not all “ooooh” and “aaaah” on this new framework.

100 Days of SwiftUI – Day 31 — Solving the challenges

Review

  1. List produces a scrolling table of rows. This is one of the most commonly used SwiftUI views.
  2. If a guard check fails we must always exit the current scope. That scope is usually a method, but it could also be a loop or a condition.
  3. Placing two views in a List row will create an implicit HStack. This effectively gives us behaviour that was the default on iOS: something on the let, then something on the right.
  4. We can create List directly from an array. We can also create one directly from a range.
  5. If a method has the same signature as a closure parameter, we can use the method instead. Swift blurs the lines between functions, methods, closures, and operators.
  6. We can use onAppear() to run a closure when a view is shown. This lets us run one-off code in an easy way.
  7. It’s possible to mix static and dynamic rows in a List. We can have static rows then dynamic, then more static, then more dynamic, and so on.
  8. We can use a ForEach view inside a List. This lets us mix dynamic rows alongside static rows.
  9. When creating views in a loop, SwiftUI needs to know how to identify each view uniquely. This lets it understand which views were added or removed.
  10. We can load files into a string if we know their URL. String(contentsOf:) is a throwing function, so we should use it carefully.
  11. All iOS apps have a main bundle. It stored our program, Info.plist, assets and more.
  12. Text fields have no border by default. We can add one using textFieldStyle().

Additional things reviewed:

  • We can control the way iOS capitalises text using the autocapitalization() modifier.
  • fatalError() will always cause a crash.
  • To read when return is pressed for a text view we should pass a valuator the onCommit parameter.
  • NSNotFound is a special value that has a range of meanings.

Challenges

Challenge 1: disallow answers that are shorter than three letters or are just our start word. For the three-letter check, the easiest thing to do is put a check into isReal() that returns false if the word length is under three letters. For the second part, just compare the start word against their input word and return false if they are the same.

For the first part I decided to go my own route as I didn’t want to mess up with the isReal method. After all, the word may be real but have only 2 letters. Rare but possible. I then added a guard check just after the check for the word having at least 1 letter.

For the second part I simply added a guard check that answer != rootWord and return an alert if false.

Challenge 2: add a left bar button item that calls startGame(), so users can restart with a new word whenever they want to.

This was solved by adding the following modifier to the Navigation View:

.navigationBarItems(leading: Button(action: startGame) {
                Text("New game")
            })

Don’t forget to reset the usedWords array to an empty one inside that method if you want the table view to become empty again.

Challenge 3: Put a text view below the List so you can track and show the player’s score for a given root word. How you calculate score is down to you, but something involving number of words and their letter count would be reasonable.

To solve this, which required much more time than I would have liked to, I added a section to the VStack, like this:

Section(header: Text("Score").bold()) {
    Text("Your score for \(rootWord) is \(score)")
}

After banging my head like an idiot against the wall because I am not really understanding when I should use the Binding $ sign or not and, possibly getting the fact that inside a Text that cannot be used, I created a computed property, like this:

var score: Int {
    guard !usedWords.isEmpty else { return 0 }
    
    var tempScore = 1.0
    
    for word in usedWords {
        for _ in word {
            tempScore *= 1.5
        }
    }
    
    tempScore *= Double(usedWords.count * 2)
    
    return Int(tempScore)
}

I used a Double because I wanted to give more points for each letters but twice looked too much. This checks for the array being empty to give me 0 at the beginning. It works, it is a big meh, but, fine, I accept it. I hope one day my brain will make the jump.

Anyway, that’s it for today! Need to get some lunch and go to work! See you tomorrow!


Thank you for reading!

If you like what I’m doing here please consider liking this article and sharing it with some of your peers. If you are feeling like being really awesome, please consider making a small donation to support my studies and my writing (please appreciate that I am not using advertisement on my articles).

If you are interested in my music engraving and my publications don’t forget visit my Facebook page and the pages where I publish my scores (Gumroad, SheetMusicPlus e ScoreExchange).

You can also support me by buying Paul Hudson’s books from this Affiliate Link.

Anyways, thank you so much for reading!

Till the next one!

Published by Michele Galvagno

Professional Musical Scores Designer and Engraver Graduated Classical Musician (cello) and Teacher Tech Enthusiast and Apprentice iOS / macOS Developer Grafico di Partiture Musicali Professionista Musicista classico diplomato (violoncello) ed insegnante Appassionato di tecnologia ed apprendista Sviluppatore iOS / macOS

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: