Week 3 of Learning SwiftUI

CoreML, UITextChecker, Managing assets, and more!

Posted by Joe Speakman on February 03, 2021 · 13 min read



Day 12 (Day 27)

Today we add the primary user interface, and add the CoreML model we created yesterday into the project. As we have learned in past projects with SwiftUI, there needs to be some @State properties added to store the corresponding controls’ information. We are adding three out of the six needed, one for the date: the amount of sleep the user wants to get and the amount of coffee they had that day.

@State private var wakeUp = Date()
@State private var sleepAmount = 7.0
@State private var coffeeAmount = 2

For my selfish benefit, I defaulted the values to my average for the day. Then we add in the date picker and the two-steppers while using the %g string specifier that no matter what the provided value, it will remove all nonessential zeros. We also added to the NavigationView the app title and a button to calculate the result with the Machine learning model and display the product with an activity alert. After adding the .mlmodel file to the project we set up it with our project, first by letting SwfitUI know we have used a CoreML model by:

let model = SleepCalculator()

After that we would then pass the date components into the model to get the output result. this is done in a do, catch block:

    func calculateBedtime() {
        let model = SleepCalculator()
        
        let components = Calendar.current.dateComponents([.hour, .minute], from: wakeUp)
        let hour = (components.hour ?? 0) * 60 * 60
        let minute = (components.minute ?? 0) * 60
        
        do {
            let prediction = try
                model.prediction(wake: Double(hour + minute), estimatedSleep: sleepAmount, coffee: Double(coffeeAmount))
            let sleepTime = wakeUp - prediction.actualSleep
            
            let formatter = DateFormatter()
            formatter.timeStyle = .short
            
            alertMessage = formatter.string(from: sleepTime)
            alertTitle = "Your ideal bedtime is..."
        } catch {
            alertTitle = "Error"
            alertMessage = "Sorry, there was a problem calculating your bedtime."
        }
        showingAlert = true
        
    }

This code also enables us to change the alertTitle, alertMessage, and showing alert properties that are yet to be defined. Let us work on that now, adding these to just below the first three:

@State private var alertTitle = ""
@State private var alertMessage = ""
@State private var showingAlert = false
Day 13 (Day 28)

Today is the wrap-up for project 4, and It has been fun to get my feet wet with CoreML and using some new interface items. The focus of today’s tasks is to clean up the interface on BetterSleep, and do the end-of-project Quiz. Currently, the layout is, as Paul put it, “Substandard.” So to resolve this, we are going to embed the current layout in a Form and change the current Text items to be Section header text. After completing this part of the exercise, Xcode has decided that I no longer need to be working on this project as it has now made my project not open with the current version of Xcode. Unfortunately, that does mean that I cannot complete the other two challenges without repeating all of the work from the past two days. With the sadness of a corrupted project, I took the end-of-project assessment and got an 81% on the test. I look forward to the start of the next project.

Day 14 (Day 29)

Paul shares that this will be the final “easy” project of the course, I look forward to the challenge. Today we are working on learning how these three items work: List, Bundle, and UITextChecker. Starting off with List, a list’s UIKit equivalent would be UITableView. It is incredibly simple to create a list with SwiftUI you could add dynamic data and static data all in the same list block, just like this:

List {
    Text("Static row number 1")
    Text("Static row number 2")

    ForEach(0..<9) {
        Text("Dynamic row number \($0)")
    }

    Text("Static row number 3")
    Text("Static row number 4")
}

This can power some incredibly complex and creative uses. The next thing in the lesson for today is loading resources from your app bundle. It sounds complex, but it is not. Loading files from the bundle using URLs. Using this code:

if let fileURL = Bundle.main.url(forResource: "superawesomefile", withExtension: "json") {
  // The file was found! 
}

Keep in mind that this does not apply to any file type that Xcode and swift will manage for you (Like images)… Now, what if, for some reason, the app can’t find the file in question? Or the app can’t read the file?

    func startGame() {
        if let startWordsURL = Bundle.main.url(forResource: "start", withExtension: "txt") {
            if let startWords = try?
                String(contentsOf: startWordsURL) {
                let allWords = startWords.components(separatedBy: "\n")
                rootWord = allWords.randomElement() ?? "silkworm"
                return
            }
        }
        
        fatalError("Could not load start.txt from bundle.")
    }

This code checks the bundle for the file in question, and if it can’t find it sets a basic root word and give the fatal error listed. As Paul shared, it could be the best experience for the end-user to have the app crash, then function poorly with missing functionality.

Day 15 (Day 30)

Today we will implement some important features within the app, such as adding the supplied project file and checking the user’s word. Additionally, we go over how to use UITextChecker. UITextChecker lives under UIKit, and is Objective-C based. So there is a little more leg work to get it working in SwiftUI. First, we create an instance of UITextChecker, that is responsible for parsing the user’s word. Then we need to create an NSRange, to read the entire length of the string. so the code will look a little something like this:

    func isReal(word: String) -> Bool {
        let checker = UITextChecker()
        let range = NSRange(location: 0, length: word.utf16.count)
        let misspelledRange = checker.rangeOfMisspelledWord(in: word, range: range, startingAt: 0, wrap: false, language: "en")
        
        return misspelledRange.location == NSNotFound
    }

We also add some error checking if a word already exists in the list and a valid word in the English dictionary.

Day 16 (Day 31)

Here is the final day of the project! I took the post-project wrap up quiz and scored a respectable 83%. This work makes sense to me, and there were some important topics touched on in this project, like fatalError. Listed are the Challenges for taking the learning further:

  • 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, compare the start word against their input word and return false if they are the same.
  • Add a left bar button item that calls startGame(), so users can restart with a new word whenever they want to.
  • 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 up to you, but something involving several words and their letter count would be reasonable.

Embarking on the first challenge, Paul recommends adding a check into the isReal() function. This can be accomplished by using this code:

if word.count >= 4 {
//code here
}

The second challenge is to add a bar button that calls the startGame() function. This is a rudimentary task that can be accomplished by adding this right after the .navigationBarTitle:

 .navigationBarItems(trailing:
                                    Button(action: startGame) {
                                        Text("Start game")
                                    }
            )

The final challenge is to add score under the list and update every time a user successfully adds a word to the list. This can be done multiple ways. You can add a @State object and then use Text to display the score. Additionally, to manage the score you can add to the startGame() code to reset the counter back to zero, then in the isReal() write logic to increase the counter by one.

Week three.

We have come so far in the past three weeks; time flies when you are having fun! I have developed a good routine as I try and get at least an hour of coding and writing in each morning before work. Then on my lunch, I will try and eat as fast as I can to then self-edit my writing from the morning or work out the challenges at the end of the project. I also try and get at least one Apple Fitness+ class in. By the end of my day, I am exhausted, but I’m doing meaningful work to help me become a better developer and feel better.