Categories
Software

Enums with associated values are very helpful!

For my flash cards app, I’m currently working on the issue of language selection. This app is going to provide users with a list of many words in many languages. Its goal is to give people a tool to use flash cards for learning and maintaining multiple languages.

Consider a person that doesn’t have this app. Let’s assume their native tongue is French, and they want to practice their English and Japanese. The obvious approach would be to spend one chunk of time working between French and English and another chunk of time working between French and Japanese.

But what if they could work between all three languages? Or maybe just between their non-native languages? (English and Japanese in this case) This is the app for that. Maybe they also want to get to level 1 of Norwegian? Just add it to the mix.

To give credit where it is due, I got this idea from a recent episode of The Tim Ferriss Show.

Tim: If you’re getting too much traffic due to this link, my apologies. I’d be happy to remove it, just let me know.

I confess, Tim Ferriss is definitely a celebrity crush of mine, but back to the topic at hand, enums…

This flash cards app is going to start with approximately ten languages, and I want to give users flexibility in choosing what languages/words get shown, and also what languages they will be expected to use for their answers.

I started by defining a list of askLanguages, and a list of answerLanguages. I envisioned two columns of buttons. The column on the left would let users pick the askLanguages and the column on the right would let users pick the answerLanguages.

But there are some invalid combinations.

If a user selects zero answerLanguages, that is obviously not going to work

But if a user selects all the languages for askLanguages, and selects one answerLanguages, that’s not cool either. If you pick Japanese as your only answerLanguage, it doesn’t make sense to also use Japanese as an askLanguage. But if you add Haida as a second answerLanguage, then Japanese is a valid askLanguage. (eg “What is the Haida word for サーモン?” Trick question, Haida has *many* words for salmon.)

If a user selects two askLanguages, their list of answerLanguages needs to include either both the askLanguages, or at least one other language not in the askLanguages list.

When a user has selected values that can’t actually be used, it could be complicated/messy/challenging for the app to convey why their choice won’t work and what they need to change to make it workable.

So instead of giving users this hyper-flexible way to choose, I assumed users’ selection preferences are going to fall into one of a few categories:

  • use all the languages, for both asking and answering
  • use a subset of languages, for both asking and answering
  • use one language for asking (native language perhaps) and all other languages for answering
  • use one language for answering and all other languages for asking
  • use one language for asking and one language for answering (old school!)

So I defined an enum that captures these specific cases:

enum LanguageChoice: Equatable, Codable {
    case all
    case oneToOne(Language, Language)
    case oneToAll(Language)
    case allToOne(Language)
    case symmetricSubset(Set<Language>)
}

And now all those obscure invalid corner cases go away. The UI to select a language choice starts with a picker with the five possibilities. For most of the picker choices there will a simple second step for users. Typically either:

  • pick one item from a list -or-
  • pick N items from a list

There are still some invalid cases (eg symmetricSubset requires users to pick at least one language) These cases (so far) are self-evident, and easy to convey to users in the picking UI. The logic to determine validity can be handled via a simple computed property on the enum.

    var isValid: Bool {
        switch self {
        case .oneToOne(let ask, let answer):
            return ask != answer
        case .symmetricSubset(let languages):
            return languages.count > 1
        default:
            return true
        }
    }

And thanks to the great team of people working on the Swift language, encoding and decoding enums with associated values is now built in. This means persisting a users preferred configuration is easy peasy.

If it turns out some users want to specify more complex language choices (eg “I want one askLanguage, and five answerLanguages“) these can be handled with new or modified enum cases. Even if it turns out there is a group of users who want the ‘totally wide open control’ option, it can be handled as a case in the enum. These users will need to navigate the invalid cases described above, but all the users that select less problematic options can remain blissfully unaware of this complexity.

Categories
Software

Swift Dictionaries and the Codable Protocol

(Not a book by JK Rowling, tho I’m sure many people would purchase and read Harry Potter and the Codable Protocol)

I’m working on a project where I need to write code to convert the following JSON into a structure:

{  "en" : { "stringUnit" : {
              "state" : "translated",
              "value" : "Cat" } },
    "fr" : { "stringUnit" : {
              "state" : "translated",
              "value" : "Chat" } },
    "ja" : { "stringUnit" : {
              "state" : "translated",
              "value" : "ねこ" } }
}

To accomplish this, I created the following model objects:

typealias LocalizationsDict = [String: Localization]

struct Localization: Codable, Equatable {
    let stringUnit: StringUnit
}

struct StringUnit: Codable, Equatable {
    let state: String
    let value: String
}

The code to decode a LocalizationsDict looked like this:

        let languages: LocalizationsDict = try JSONDecoder().decode(LocalizationsDict.self, from: data)

Everything was working wonderfully. But then I got greedy. (queue the jump scare music.) I wanted to see if the keys could be an enum, instead of a String. I felt this would make the code safer, and would mean languages could be types using autocomplete. (The jury may still be out on whether necessity or laziness is truly the mother of invention.)

Here was the code needed to create the Language enum:

enum Language: String, Codable {
    case en = "en"
    case fr = "fr"
    case ja = "ja"
}

Unfortunately…in order for a Swift dictionary to conform to Codable, its key type must be either String or Int. For a Swift dictionary with any other type of key, decode will assume the JSON will be represented as an array, where the first item is the first key, the second item is the first value, third item is the second key, etc.
Thank you Apple Dev forums, yet again. https://developer.apple.com/forums/thread/747665

This would mean the JSON would need to be stored like this:

[  "en", { "stringUnit" : {
              "state" : "translated",
              "value" : "Cat" } },
    "fr", { "stringUnit" : {
              "state" : "translated",
              "value" : "Chat" } },
    "ja", { "stringUnit" : {
              "state" : "translated",
              "value" : "ねこ" } }
}

Sadly, in this situation, the JSON was being generated by the metaphorical ‘somebody else’ and I didn’t have the option to change the format of my incoming JSON.

Instead I added a step when decoding this type of Dictionary. Here’s a code snippet:

typealias LocalizationsDict = [Language: Localization]
typealias LocalizationsDict2 = [String: Localization]

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let stringKeyedLocalizations = try container.decode(LocalizationsDict2.self, forKey: .localizations)
        var enumKeyedLocalizations: LocalizationsDict = [:]
        for (key, value) in stringKeyedLocalizations {
            if let enumKey = Language(rawValue: key) {
                enumKeyedLocalizations[enumKey] = value
            }
        }
        self.localizations = enumKeyedLocalizations
    }

The details of what’s happening are as follows:

  1. Decode the JSON to a [String: Localization] dictionary
  2. Create an empty [Language: Localization] dictionary
  3. Iterate through the [String: Localization] dictionary
  4. For each entry, verify it’s possible to create a valid enum value from the language string. (eg map "en" to Language.en
  5. For each entry store the value in the [Language: Localization] dictionary, using the enum key generated in the previous step.

This works, but I thought this seemed like an interesting shortcoming in Swift.

Categories
Software

A SwiftUI Picker Using an Swift Enum Part 2

In Part 1, we created a basic SwiftUI Picker that was bound to an enum variable that included n possible values. When users pick a value from the picker, the app’s data model is aware of this change and the UI updates to reflect the user’s selection.

In this post we are going to extend this basic functionality.

Display Text for Sort Types

Life would be better if we could customize the display text for each of the different sort types. To do this we will add a function to our enum.

    func displayText() -> String {
        switch self {
        case .name:
            return NSLocalizedString("Name", comment: "display text for sort type: name")
        case .height:
            return NSLocalizedString("Height", comment: "display text for sort type: height")
        case .averageScore:
            return NSLocalizedString("Average Score", comment: "display text for sort type: averageScore")
        }
    }

In order to use this new function, replace rawValue calls with displayText() calls.

struct ContentView: View {
    @ObservedObject var settings = Settings.shared
    var body: some View {
        VStack {
            Picker(selection: $settings.sortType, label: Text("Sort Type")) {
                ForEach(SortType.allCases, id: \.self) { sortType in
                    Text("\(sortType.displayText())")
                }
            }
            Text("sort type is: \(settings.sortType.displayText())")
        }
    }
}

Persist Preferred Sort Type

In this section we will add code to remember a user’s previously selected sort type. So if the user closes the app and relaunches it, their preferred sort type will still be selected. To do this, we will write the sortType to UserDefaults, and then read this value when the app launches. These changes will be made in the Settings class.

class Settings: ObservableObject {
    static let shared = Settings()
    @Published var sortType: SortType {
        didSet {
            UserDefaults.standard.setValue(sortType.rawValue, forKey: "sortType")
        }
    }
    init() {
        sortType = SortType(rawValue: UserDefaults.standard.string(forKey: "sortType") ?? "name") ?? .name
    }
}

I’m mildly pained by the need to include both a fallback value for the string read from UserDefaults and also a fallback value SortType(rawValue: ) return value. I guess this is just my way to demonstrate that I don’t like forced unwraps !

Add a New Sort Type

So what happens when end requirements change and now our data can also be sorted by…. let’s say Shoe Size? What needs to change in our example? In fact, very little needs to change. Basically just add the new enum case, and add a corresponding case to the displayText function

enum SortType: String, CaseIterable {
    case name
    case height
    case averageScore
    case shoeSize
    
    func displayText() -> String {
        switch self {
        case .name:
            return NSLocalizedString("Name", comment: "display text for sort type: name")
        case .height:
            return NSLocalizedString("Height", comment: "display text for sort type: height")
        case .averageScore:
            return NSLocalizedString("Average Score", comment: "display text for sort type: averageScore")
        case .shoeSize:
            return NSLocalizedString("Shoe Size", comment: "display text for sort type: shoeSize")
        }
    }
}
Categories
Software Uncategorized

A SwiftUI Picker Using an Swift Enum

These two items (the SwiftUI Picker and a Swift enum) work really well together. Some might say they go together as well as Peanut Butter and Banana.

Requirement: Your app needs a way for a user to choose how to sort their list items. Today list items can be sorted by Name, Height and Average Score. Some time in the future, the list of sort types is expected to grow.

Eventually we are going to need some UI for this, but let’s start be defining an enum to define the sort types. Our enum needs to conform to CaseIterable because we will need to call the allCases class method. I don’t think String is required, but is helpful in the initial stage before we polish the UI.

enum SortType: String, CaseIterable {
    case name
    case height
    case averageScore
}

And also a Settings model object to store our source of truth (ie sort type) We will access the Settings singleton via the shared static variable. Settings needs to conform to ObservableObject because the Picker will bind to the sortType property.

class Settings: ObservableObject {
    static let shared = Settings()
    @Published var sortType: SortType
    init() {
        sortType = .name
    }
}

For the UI, we will use the following Picker init

    public init(selection: Binding<SelectionValue>, label: Label, @ViewBuilder content: () -> Content)

The UI content view will start with something like this:

struct ContentView: View {
    @ObservedObject var settings = Settings.shared
    var body: some View {
        VStack {
            Picker(selection: $settings.sortType, label: Text("Sort Type")) {
                ForEach(SortType.allCases, id: \.self) { sortType in
                    Text("\(sortType.rawValue)")
                }
            }
            Text("sort type is: \(settings.sortType.rawValue)")
        }
    }
}

If we run this code, we’ll see a picker above a text label. When we pick a different value in the picker, the text label updates accordingly. Woot!

In Part 2, we will:

  1. Improve the UI by adding display names for the sort types
  2. Use UserDefaults to persist and recall the selected sort type
  3. Add another sort type