Making Assessments in Swift Playgrounds

August 05, 2016

Available in the iOS 10, the Swift Playgrounds app allows you to author and consume playgrounds on your iPad. The app was previewed at WWDC and given it’s own dedicated session during the week. For the past few weeks, I’ve been working off the Starter project provided by Apple in order to experiment with creating a Playground book.

Because the app hasn’t been officially released and it’s still in beta, there isn’t much documentation available at the moment– this has made some parts of the process more challenging. In particular, I had held off on trying to make the assessment feature work for awhile, but ended up tackling this last week.

If you’re not familiar with assessments, here’s how they work. In the app, as the user inputs code to solve challenges and exercises, the code can be checked for correctness. When appropriate, hints are offered via popovers and the solution can be shown if the user chooses. When an exercise or challenge has been completed, the user is shown an success message, and a green checkmark appears in the table of contents next to the completed page.

Right now, assessments only work in the Playgrounds app and not through Xcode 8. This may change in the future but it’s something to be aware of.

There are two new properties available on the PlaygroundPage.current that make assessments work.

The first is an assessmentStatus property of type AssessmentResults, which is an enum. This property can be set to a passing or failing assessment. The Playgrounds app will show an appropriate hint or success message.

public enum AssessmentResults {
    case pass(message: String)
    case fail(hints: [String], solution: String?)
}

PlaygroundPage.current.assessmentStatus = .pass(message: "Great job!")

The second is a text property of type String, which is the content of the entire Playground. This property ends up being extremely important because it let’s you examine the entire contents of the playground. From this, you can parse the text and decide if the exercises have been completed or not.

let input = PlaygroundPage.current.text

For example, if the exercise was to create a UILabel and set the numberOfLines property, you could search the playground text of a series of tokens which, if found, would mean the exercise was completed. If one or more of the tokens weren’t found, you could return hints and an optional solution.

let input = PlaygroundPage.current.text

if !input.contains("UILabel") && !input.contains(".numberOfLines") {
    PlaygroundPage.current.assessmentStatus = .fail(hints: ["Create a `UILabel` and set `numberOfLines`"], solution: nil)
} else {
    PlaygroundPage.current.assessmentStatus = .pass(message: "Great job!")
}

In my case, I only wanted to examine the editable code blocks and not the entire Playground. By limiting the scope to certain regions, it would be easier to validate user input. I ended up writing a helper function based around NSScanner to extract editable code regions.

public func findUserCodeInputs(from input: String) -> [String] {
    var inputs: [String] = []
    let scanner = Scanner(string: input)
    
    while scanner.scanUpTo("//#-editable-code", into: nil) {
        var userInput: NSString? = ""
        scanner.scanUpTo("\n", into: nil)
        scanner.scanUpTo("//#-end-editable-code", into: &userInput)

        if userInput != nil {
            inputs.append(String(userInput!))
        }
    }
    
    return inputs
}

If you’re not familiar with the //#-editable-code and //#-end-editable-code tokens, they are used to demarcate editable areas of Swift code in a Playground book. The // follows normal comment syntax, and the #- is the start of Playground markup. See the WWDC session for more examples or read up on the documentation format.

With a way to pull out user input, I used another function to return an AssessmentResults from the full Playground text.

public func makeAssessment(of input: String) -> AssessmentResults {
    let codeInputs = findUserCodeInputs(from: input)

    // validate the input; return .fail if needed

    return .pass(message: "Great job!")
}

With the above definitions, it’s easy to then set the assessmentStatus of the Playground page. Typically, I am doing this at the end of my page and hiding it with a //#-hidden-code block.

let input = PlaygroundPage.current.text
PlaygroundPage.current.assessmentStatus = makeAssessment(of: input)

For more examples of how to work with assessments, check out the Learn to Code book available in the Playgrounds app on iOS 10. You can AirDrop the book over to your Mac and then dig into the .playgroundbook.