Categories
Software

Furigana in SwiftUI (2)

This is part 2. The previous episode is here.

To quickly recap: We want to layout text that looks like this:

Hello aka Konnichi wa aka 今日は aka こんにちは
こんにち is furigana that tells readers how to pronounce 今日

When I began looking into implementation options, all roads seemed to be leading toward an AttributedString in a UILabel. There are a variety of descriptions of CoreText support for something called Ruby Text. There is also a mark down schemes for expressing Ruby text and sample code showing how to tickle the belly of CoreText to layout furigana text. I was not able to get any CoreText furigana working and there are likely two reasons for this:

  1. There are reports (not substantiated by me) that Apple removed Ruby Text support from Core Text. This seems like an odd thing, but definitely plausible
  2. I really wanted to do something that was SwiftUI-ish (SwiftUI-y?)

It’s quite possible that if I’d kept hammering away at CoreText I’d have got it to work. My official excuse is my heart wasn’t in it.

To implement this in SwiftUI my first approach included:

  1. a markdown scheme, eg “おはよおはよざいます。<<今日((こんいち))>>は。”
  2. code to convert markdown strings (like the one above) into an array of model objects
    struct TextModel {
    let mainText: String
    let furiGana: String
    }
  3. UI code that takes in an array of TextModels and displays them in collection of VStacks in an HStack
struct TextCollection: View {
    let textElements: [TextModel]
    init(markdown: String) {
        self.textElements = markdown.convertedToElements
    }
    var body: some View {
        HStack() {
            ForEach(textElements) { element in
                VStack() {
                    Text(element.furiGana)
                    Text(element.mainText)
                }
            }
        }
    }
}

The result looks like this:

Not yet perfect, but a great start!

In the next part, I’ll discuss my journey to figure out how to size the furigana as a function of the size of the main text. Stay tuned!

Categories
Software

Furigana in SwiftUI

I have been thinking about implementing Furigana in my Flash Cards App for a while now. Until recently it has definitely not been high priority. But I am in the process of adding some Japanese vocab where I’m using kanji. So far I’ve only been adding the really basic characters, but still… In order to have this app be accessible to all users, adding furigana support has recently become more important.

What the heck is Furigana?!

Written Japanese uses four different types of characters. They are:

  1. Kanji – These are pictograms where each character represents an idea and can have multiple pronunciations. For example, 月 is the character used to express month, and moon. Sometimes it will be pronounced getsu, other times tsuki etc. It can be combined with other characters in longer ‘words’. 月曜日 means Monday. I find it quite interesting that the Japanese word for Monday literally translates to Moon Day. There are thought to be over 50,000 unique Kanji. To be considered vaguely literate, the consensus suggests you need to know how to read and write 1,500 to 2,000 Kanji characters. Yikes!
  2. Hiragana – This is the most common Japanese alphabet. It is comprised of approximately 50 characters all representing specific sounds. Each hiragana character represents a single pronunciation. Also the sound of each character is also its name. For example KA (か) is always pronounced ‘ka.’ This might be a mind blowing concept to speakers of English (Looking at you W) Young kids start reading and writing hiragana. Over time they use more Kanji (see above) Where a beginning Japanese student might write: くうこうにいきます (kuukou ni ikimasu) an adult would more likely write: 空港に行きます. Both of these mean ‘I am going to the airport’ but the first text is all hiragana while the second is a combination of kanji and hiragana
  3. Katakana – This is an alphabet, similar to hiragana, but only used for words that have been introduced to Japanese: ピアノ (Piano), カラオケ (Karaoke, an interesting word in many ways), ホテル (Hotel)
  4. Romaji – Yet another alphabet that uses the English/western alphabet to express the same sounds available with Hiragana and Katakana. For example SA (romaji) is pronounced the same as さ (hiragana) is pronounced the same as サ (katakana) I think Romaji is primarily used for creating content aimed at non-Japanese speakers (eg maps). However it is also common to see ads aimed at Japanese speakers use romaji, cuz in some circles in Japan, Western/English stuff is seen as ‘cool.’

Great, but I thought this was about furigana…

It is! Furigana is hiragana written above or beside kanji to let readers know how it should be pronounced. (Remember there are thought to be over 50,000 kanji characters and they frequently have multiple context-dependent pronunciations. As a rule of thumb, the more common the kanji the greater the number of different ways it can be pronounced.)

Here is a simple example of furigana

Hello aka Konnichi wa aka 今日は aka こんにちは
こんにち is furigana that tells readers how to pronounce 今日

One other point that bears mentioning is that Japanese can be written in rows (left to right, top to bottom) or in columns (top to bottom, right to left). I’m currently under the impression that SwiftUI only supports the row-based text layout. For the moment, I’m going to focus on row-based layout and ignore column-based layout.

If you want to learn more about what I’ve described here, you could do worse than going to Wikipedia.

The next post will jump into the SwiftUI implementation.

Categories
Software

A Situation Where It Makes Sense to Have Two Sources of Truth?

Don’t be fooled by the title. Writing code where a given value is stored in two different ways (in two different places) feels wrong to me too. But at the moment, it feels like solving my current problem with a single source of truth would result in more complex code.

Background; What am I trying to do?

In my ZoomBurst photo editing extension, currently the zooming and rotation effects use the centre of the image rect as the centre of the effect. For quite some time, I’ve been curious about what it would take to add the ability to allow users to specify a centre point for the effects. (My currently thinking would be to let users specify a single point to use as the centre for both zoom and rotation. But never say never…)

How am I Trying to Do it?

In broad strokes, two things need to happen to add this feature.

  1. add UI to allow users to specify the custom centre point
  2. CoreImage filters need to be updated to support effectCenter not being the same as imageCenter

For the UI, I’m adding an overlay marker to the output image. Users can change the centre by dragging this marker around. In swiftUI marker position is updated using the .offset() function in View.

I defined a @State variable to store the offset. It will be (0,0) at the top left corner and (imageSize.width, imageSize.height) at the bottom right.

    @State var currentOffset: CGSize = .zero

    func marker(inside size: CGSize) -> some View {
        return Circle()
            .stroke(.black, lineWidth: 10)
            .stroke(.white, lineWidth: 6)
            .frame(width: Self.markerDimension)
            .offset(currentOffset)
}

So far so good. However the CoreImage code cannot use this because imageSize will be different for preview output vs final image output. So the CombineOptions structure needs to store the centre value using a UnitVector size (ie height and width will be in the range [0..1])

While it’s technically possible to rely on the unitVector size value in CombineOptions, this would create the need for quite a bit of translating between [0..1] and [0..imageSize] First there is the value passed to the offset function. But there is also a surprising amount of logic to prevent users from dragging the marker off the side of the preview image.

 DragGesture()
     .onChanged { gesture in
         let proposedOffset = gesture.translation + baseOffset
         currentOffset = proposedOffset.validSize(using: markerRect)



    func validSize(using rect: CGRect) -> CGSize {
        if rect.contains(self.asPoint) {
            return self
        }
        var result: CGSize = self
        if self.width < 0 {
            result.width = 0
        } else if self.width > rect.width {
            result.width = rect.width
        }
        if self.height < 0 {
            result.height = 0
        } else if self.height > rect.height {
            result.height = rect.height
        }
        return result
    }

Should users be able to drag the marker so that it half off the preview image? (ie the centre is right on the image edge?) Or will they be ok only dragging the marker to the point where it is still entirely overtop of the preview image. All of this code requires adding and subtracting half the width of the marker view in a surprising number of places. The code to do this is much easier to understand if it all takes place in the [0..imageSize] domain, rather than the [0..1] domain.

I’m not saying the more complex thing couldn’t be done. But I was imagining somebody coming into this code 6 months or 6 years from now. I feared that the brain power to understand the code jumping between the two domains would beg the question… ‘why is this jumping back and forth between [0..imageSize] and [0..1] so much?’

In order to minimize the chance of the two values getting out of sync I created a single place in the code where one value gets changed, and it updates the [0..1] value. Come to think of it, this would be a good excuse to add a Psst comment explaining this conundrum to future me or anyone else that happens to have the good fortune to be reading this code in the future. Maybe even link to this post.

Another thought I’ve just had (and don’t think I have enough functioning neurons this late at night to properly tackle) is how would something like this fly in an environment where others are reviewing my code. I feel like there would be a (justifiable) tendency among reviewers to be skeptical of my decision to use two sources of truth. I also suspect my attempts to defend it would be more qualitative than quantitative. And I’m not entirely sure how it would be resolved. I’d like to think it would be more than just a battle of wills because I’m not a fan of battles of wills.

Categories
Software

SwiftUI TextField Focus

When a new view gets presented on the screen (for example a password entry view) most of the time it makes sense to start with the focus in the text field. The keyboard should be visible and the user should be able to just start typing. Sure it’s a minor inconvenience to force users to first tap in the field, and then start typing, but heck we’re not savages are we?

UIKit’s UITextfield always had support for programmatically setting focus via UIResponder. [myTextField makeFirstResponder]

I get the sense this functionality has been gradually evolving in SwiftUI. I feel like the current implementation is more complex than the UIResponder model from yesteryear. I don’t want to sound like I’m grumbling here, and I feel like the complexity is needed to stay true to the SwiftUI model (composability, single source of truth, etc.) Having said that, I did need to iterate way more than anticipated.

The first piece is the @FocusState descriptor used to describe a variable in a View struct. There appear to be two ways to use FocusState.

  1. a Binding<Bool> type when there is a single View that either has or does not have focus
  2. a Binding<enum?> type where there are multiple views, and maybe one of them has focus, or maybe none of them have focus.

I should point out that the notion of focus here is broader than just text entry views. Any type of View and have focus, but the expected behaviour for focus in non-textEntry views is beyond the scope of this post.

Here is an example of how to use a Binding<Bool> FocusState

struct AddEntryView: View {
    
    @Binding var unsavedData: String
    @FocusState var addEntryHasFocus: Bool

    var body: some View {
        TextEditor(text: $unsavedData)
            .focused($addEntryHasFocus)
            .defaultFocus($addEntryHasFocus, true)
    }
}

Here is an example of how to use a Binding<enum?> FocusState

struct PasswordView: View {
    enum PasswordField {
        case secure
        case regular
    }
    @FocusState private var passwordField: PasswordField?
    @State var passwordText1: String = ""
    @State var passwordText2: String = ""
    let passwordExists: Bool

    var body: some View {
        VStack() {
            SecureField("Enter password", text: $passwordText1)
                .focused($passwordField, equals: .secure)
            TextField("Create a password", text: $passwordText2)
                .focused($passwordField, equals: .regular)
        }
        .defaultFocus($passwordField, .secure)
    }
}

Unfortunately, my use case is sort of in between these two implementation options. I only want one field, but sometimes I want it to be a TextField, and sometimes I want it to be a SecureField.

struct PasswordView: View {
    @FocusState private var passwordField: ????
    @State var passwordText: String = ""
    let passwordExists: Bool

    var body: some View {
        if passwordExists {
            SecureField("Enter password", text: $passwordText)
                .focused(????)
        } else {
            TextField("Create a password", text: $passwordText)
                .focused(????)
        }
    }
}

Since there will only ever be one field displayed, I naively thought I could just use the same Binding<Bool> var for both SecureField and TextField. Sadly those values just get ignored. So I need to use the enum approach, even there will only ever be a single field.

And just to add a bit of insult to injury, I wasn’t able to get .defaultFocus working. According to an Apple Engineer in the dev forums, I may have uncovered a bug. woo! All that to say, my next attempt looked something like this.

struct PasswordView: View {
    enum PasswordField {
        case secure
        case regular
    }
    @FocusState private var passwordField: PasswordField?
    @State var passwordText: String = ""
    let passwordExists: Bool

    var body: some View {
	VStack() {
            if passwordExists {
                SecureField("Enter password", text: $passwordText)
                    .focused($passwordField, equals: .secure)
            } else {
                TextField("Create a password", text: $passwordText)
                    .focused($passwordField, equals: .regular)
            }
        }
        .onAppear {
            passwordField = passwordExists ? .secure : .regular
        }
    }
}

BUT, I encountered a life cycle issue. .onAppear gets called before the document gets loaded. In my case, the document contents determine whether to show the TextField or the SecureField. So I needed to replace .onAppear with onChange. So here’s an approximation of my final code.

struct PasswordView: View {
    enum PasswordField {
        case secure
        case regular
    }
    @FocusState private var passwordField: PasswordField?
    @State var passwordText: String = ""
    @Binding var document: TurtleDocument

    var body: some View {
	VStack() {
            if document.passwordExists {
                SecureField("Enter password", text: $passwordText)
                    .focused($passwordField, equals: .secure)
            } else {
                TextField("Create a password", text: $passwordText)
                    .focused($passwordField, equals: .regular)
            }
        }
        .onChange(of: document.encryptedData) {
            passwordField = document.passwordExists ? .secure : .regular

        }
    }
}
Categories
Software

Investigating SwiftUI Document-based Apps

My Turtle app is a document-based app (iOS and MacOS) was written a long time ago. The iOS version relied heavily on UIDocumentBrowserViewController for managing the documents. After a couple of years of neglect the document management was starting to have problems. The iOS version wasn’t able to create new documents. Also closing and saving a document seemed to fail about half of the time.

Eventually these problems made the app unusable enough that I started weighing my options on how best to get things working again. I first considered doing something incremental, focussing on specific issues with the iOS build and sticking with the UIKit based implementation.

But then I saw a WWDC video demonstrating how to get started building a SwiftUI Document based app. I felt some trepidation that there would be a lot of refactoring from UIDocument to SwiftUI’s Document. The UIDocument subclass included metadata that in hindsight was could/should have been UI state. The original implementation also includes complexity related to supporting one iOS target and one macOS target. Last but not least, Turtle supports two different document formats. (Legacy and Threaded) I’m positive I’ve done a bad job of structuring the code to support these two document formats. Moving to a SwiftUI DocumentGroup would require rebuilding this.

So despite all these reasons to stick with the messy UIKit implementation, I jumped into a SwiftUI implementation. While I still have some functionality gaps, on the whole this conversion has been satisfying and even fun.

Future posts will detail some of the issues I’ve encountered along the way.

Categories
Software

How to listen for changes in an @AppStorage value

For my flash cards app, I’ve added a few user settings that I’ve discussed in other posts. For example, users can choose whether they want to use the same ask language for all cards (e.g. Always first show words in Japanese). They can also choose whether they want to use かな or Romaji for Japanese. They can also choose a subset of the cards they want to use (eg numbers, months/daysOfTheWeek etc.)

Any time one of these settings gets changed by the user, the app’s LanguageManager singleton needs to update the list of available words that match the settings. (for example, if a given word does not have a French translation and the user specifies fr as their only answer language, that card can’t be shown to the user.)

Strictly speaking, available cards could be recalculated on demand. (ie each time a user switches to a new card) But that would be suboptimal, given the available cards only changes when one of the settings changes. But in the spirit of saving cycles, I feel like finding a way to calculate available cards only when the value changes is a worthy cause.

For a starting point, I have the following code where the UI is setting the UserDefault value.

struct CardPickerView: View {
    
    @AppStorage("cardPile") var cardPile: CardPile = .allRandom
    var body: some View {
        VStack {
            Picker("Card Pile", selection: cardPile) {
                ForEach(CardPile.allCases,
                        id: \.self) { 
                    Text($0.title)
                        .tag($0.rawValue)
                }
            }
        }
    }
}

And the following code where LanguageManager updates availableCards

class LanguagesManager {
    static let shared = LanguagesManager()

    // The UserDefaults values that will impact which cards/words can be used
    @AppStorage("scriptPickers") var scriptPickers:  ScriptPickers = ScriptPickers.defaultDictionary
    @AppStorage("languageChoice") var languageChoice: LanguageChoice = .all
    @AppStorage("cardPile") var cardPile: CardPile = .allRandom

    // All the words
    let words: Words

    // The words that match the criteria
    var availableWords: Words

    func updateAvailableWords() {
        var result = words.filtered(by: cardPile.wordList)
        let askLanguages = languageChoice.askLanguages
        let answerLanguages = languageChoice.answerLanguages
        result = result.matching(askIdentifiers: askLanguages, answerIdentifiers: answerLanguages)
        availableWords = result
    }
}

This code mostly works as expected, however when a user updates scriptPickers or languageChoice or cardPile, availableWords doesn’t get updated. <frownyFace!!> My first thought was to use didSet (like this)

    @AppStorage("cardPile") var cardPile: CardPile = .allRandom {
        didSet { updateAvailableWords() }
    }

Sadly didSet on @AppStorage only works in very specific situations for me. Based on what I saw, it works if:

  1. your code is in a View
  2. you explicitly update the value (eg. self.cardPile = .all)

Your mileage may vary. Bottom line: didSet in LanguagesManager was not getting called for me.

It would have been possible to add calls to updateAvailableWords() in the UI code any time one of the related settings changed. But relying on the UI code to keep LanguagesManager fresh. Just. Felt. Wrong.

I also tried to use combine to subscribe to the @AppStorage values, but was not able to get that working. <anotherFrownyFace>

The solution I ended up using was to create a UserDefaults extension that would create a Binding<> for a specific UserDefaults key value. Anytime this value changes, it will fire a notfication.

extension UserDefaults {
    func cardPileBinding(for defaultsKey: String) -> Binding<CardPile> {
        return Binding {
            let rawValue = self.object(forKey: defaultsKey) as? Int ?? 0
            return CardPile(rawValue: rawValue) ?? .allRandom
        } set: { newValue in
            self.setValue(newValue.rawValue, forKey: defaultsKey)
            NotificationCenter.default.post(name: .userDefaultsChanged, object: defaultsKey)
        }
    }
}

extension Notification.Name {
    static let userDefaultsChanged = Notification.Name(rawValue: "user.defaults.changed"
}

Next, I used these bindings in the UI code where I’d previously been using @AppStorage.

struct CardPickerView: View {
    let cardPileBinding: Binding<CardPile>
    init(userDefaults: UserDefaults = .standard) {
        self.cardPileBinding = userDefaults.cardPileBinding(for: "cardPile")
    }
    var body: some View {
        VStack {
            Picker("Card Pile", selection: cardPileBinding) {
                ForEach(CardPile.allCases,
                        id: \.self) { 
                    Text($0.title)
                        .tag($0.rawValue)
                }
            }
        }
    }
}

Then I updated LanguagesManager to listen for this notification.

class LanguagesManager {
    private var subscriptions = Set<AnyCancellable>()
    init() {
        // a bunch of unrelated init stuff....

        NotificationCenter.default.publisher(for: .userDefaultsChanged)
            .sink(receiveValue: { notification in
                self.updateAvailableWords()
            })
            .store(in: &subscriptions)
    }

And this meets my needs, insofar as the UI code doesn’t need to explicitly fiddle with LanguagesManager state any time a user changes their prefs. I don’t like that the UI code needs to use this special binding to do the updating. I also don’t like that the UserDefaults extension needs to define multiple functions to return the different types of Bindings. If you all know of a way to get around this (generics?) I’m all ears!

I also don’t like that the getters and setters in the Binding values need to translate to and from rawValue. @AppSupport magically handled the translations between rawValue and encodedValue.

And one last problem with this approach, for at least one of my settings UI inplementations, calling the setter in the binding didn’t trigger an update to the UI. (it’s on my todo list to investigate this.) I got around this problem by adding a ‘dummy’ @AppStorage var in the View. Adding this dummy value seems to force the view to redraw when the UserDefault value changes.

    @AppStorage("cardPile") var cardPileDummy: CardPile = .allRandom
Categories
Software

Flipping #*&$%@! Cards with SwiftUI

Despite the salty title this was a very low stress investigation. I wanted to find a way to use animation to flip my flash cards. There are many great sources to do something like this:

struct ContentView: View {
    @State var angle: CGFloat = 0    
    var body: some View {
        VStack {
            Text("contentText: frontText")
                .padding(40)
                .background(.green)
                .rotation3DEffect(.degrees(angle), axis: (x: 0.0, y: 1.0, z: 0.0))
                .animation(.default, value: angle)
            Button("Flip") {
                angle += 180
            }
        }
    }
}

Tapping the button in the above app will flip the card, and show an empty green rectangle. So how would I go about showing different (dynamic) content on the ‘back’ of the card? The idea I’m currently going with is to create a front content view and a back contentView. The back contentView begins pre-rotated, so that when the parent view gets rotated it ends up being the correct orientation,

        VStack {
            ZStack {
                FlippableContent(contentText: frontText)
                FlippableContent(contentText: rearText)
                    .rotation3DEffect(
                        .degrees(180), axis: axis)
            }
            .rotation3DEffect(.degrees(angle), axis: axis)
            .animation(.default, value: angle)
            Button("Flip") {
                angle += 180
            }
        }
...
struct FlippableContent: View {
    let contentText: String
    var body: some View {
        VStack {
            Text(contentText)
        }
        .frame(width: 300, height: 250)
        .background(.green)
    }
}

This is close, but not quite right. It always only ever shows the FlippableContent view showing rearText (sometimes forwards, sometimes reverse). I experimented with different angles on the FlippableContents, however I’m not entirely clear on exactly how multiple 3d rotation effects get combined. Not my circus, not my monkeys I guess. Tho I’m definitely a bit curious…

To fix my problem, I’ve created logic that creates different opacity values for the front and back content of the card.

extension CGFloat {
    var rearOpacity: CGFloat {
        return (self / 180).truncatingRemainder(dividingBy: 2)
    }
    var frontOpacity: CGFloat {
        return 1 - rearOpacity
    }
}

And these new functions get used in opacity modifiers in the ZStack.

            ZStack {
                FlippableContent(contentText: frontText)
                    .opacity(angle.frontOpacity)
                FlippableContent(contentText: rearText)
                    .rotation3DEffect(
                        .degrees(180), axis: axis)
                    .opacity(angle.rearOpacity)
            }

Now the view gets initialized with angle at zero, which shows the front text. When the user flips the card, angle gets increased by 180 degrees. This animates through the rotation, and hides the front face, and shows the rear face. On the next flip, the rear face gets hidden and the front face gets shown.

Categories
Software

AppStorage part 2

Turns out this topic needs more than just one post. In part 1, I described how I was able to use AppStorage for an enum with associated values.

Here I will discuss a challenge I encountered when attempting to build the View to enable users to change their value for the languageChoice enum.

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

Step 1, create a version of LanguageChoice with no associated values:

    enum SimpleLanguageChoice: String, CaseIterable {
        case all
        case oneToOne
        case oneToAll
        case allToOne
        case symmetricSubset
    }

Step 2, create a state variable using the new enum type and bind it to a picker:

    @State var languageChoice: SimpleLanguageChoice = .all
    var body: some View {
        VStack {
            Picker("Languages Choice", selection: $languageChoice) {
                ForEach(SimpleLanguageChoice.allCases, id: \.self) {
                    Text($0.rawValue)
                        .tag($0)
                }
            }
        }
    }

Step 3, add UI below the picker to display the appropriate controls to go with the value of languageChoice.

    @ViewBuilder var bottomStuff: some View {
        switch languageChoice {
        case .all:
            EmptyView()
        case .oneToAll:
            Picker("Ask in", selection: $language) {
                ForEach(Language.allCases, id: \.self) {
                    Text($0.rawValue)
                        .tag($0)
                }
            }
     // et cetera

Step 4, add an AppStorage var to get the persisted LanguageChoice value and use it to configure the Picker UI.

    @AppStorage("languageChoice") var persistedLanguageChoice: LanguageChoice = .all
    @State var languageChoice: SimpleLanguageChoice

init() {
     languageChoice = SimpleLanguageChoice.simpleChoice(for: persistedLanguageChoice)
}

Insert record scratch sound here! This code created the following compiler error:
'self' used before all stored properties are initialized

I wanted to use the persisted languageChoice to set up the UI to allow users to choose a new languageChoice. But doing this required reading persisted languageChoice, which was not allowed. Alas. So to get things to work I needed to set a default value for languageChoice in the declaration and then update the value in init. Just do this, right?

    @State var languageChoice: SimpleLanguageChoice = .all
    init() {
        languageChoice = SimpleLanguageChoice.simpleChoice(for: persistedLanguageChoice)

Nope. If you set an initial value for a State variable in the declaration, updating it in init gets more complicated. You need to do this:

    init() {
        _languageChoice = State(initialValue: SimpleLanguageChoice.simpleChoice(for: persistedLanguageChoice)

Categories
Software

SwiftUI AppStorage wrinkles

The SwiftUI property wrapper AppStorage is a great way to cut down on boiler plate code needed to manage/access UserDefaults. At the risk of sounding like I’m looking a gift horse in the mouth, I feel compelled to describe a couple of challenges I encountered while using AppStorage in my current project, the language flash cards app.

AppStorage for enums with associated values

The type I wanted to persist using AppStorage was a fairly gnarly enum with a variety of associated values. It is the value that persists the users preferences for languages to use for asking and answering. See this blog post for more details on how this enum came to be.

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

Out of the box, AppStorage only works with the most basic of types. (Int, String, Bool, etc.) There are several great resources on how to extend AppStorage support to basic structs. If you google “AppStorage RawRepresentable” you’ll be away to the races in no time. The tl;dr version is: implement
public init?(rawValue: String)
and
public var rawValue: String

My first inclination was to use JSONDecoder() in init and JSONEncoder() in rawValue. Unfortunately JSONEncoder() was calling rawValue, which was calling JSONEncoder() etc.

So instead I defined a custom scheme to encode (and decode) the LanguageChoice enum. This required a fair bit of custom code (see below) and allowed LanguageChoice to work with @AppStorage. But I wasn’t out of the woods yet. There was still more work to be done, which I’ll describe in a future post.

Here is how I chose to conform to RawRepresentable.

    public init?(rawValue: String) {
        let components = rawValue.components(separatedBy: ":")
        guard let first = components.first else {
            return nil
        }
        switch first {
        case "all":
            self = .all
        case "oneToOne":
            guard components.indices.contains(2),
                  let lang1 = Language.init(rawValue: components[1]),
                  let lang2 = Language.init(rawValue: components[2]) else {
                return nil
            }
            self = .oneToOne(lang1, lang2)
        case "oneToAll":
            guard components.indices.contains(1),
                  let lang = Language.init(rawValue: components[1]) else {
                return nil
            }
            self = .oneToAll(lang)
        case "allToOne":
            guard components.indices.contains(1),
                  let lang = Language.init(rawValue: components[1]) else {
                return nil
            }
            self = .allToOne(lang)
        case "symmetricSubset":
            guard components.indices.contains(1) else {
                return nil
            }
            let languageComponents = components[1].components(separatedBy: ",")
            let languages: Set<Language> = Set(languageComponents.compactMap { Language(rawValue: $0) } )
            self = .symmetricSubset(languages)
        default:
            return nil
        }
    }

    public var rawValue: String {
        let data = try? JSONEncoder().encode(self)
        print("data: \(data)")
        let result: String
        switch self {
        case .all:
            result = "all"
        case .oneToOne(let language, let language2):
            result = "oneToOne:\(language.rawValue):\(language2.rawValue)"
        case .oneToAll(let language):
            result = "oneToAll:\(language.rawValue)"
        case .allToOne(let language):
            result = "allToOne:\(language.rawValue)"
        case .symmetricSubset(let set):
            let setRawValue = set.sorted().reduce("") {
                $0 + $1.rawValue + ","
            }
            
            result = "symmetricSubset:\(setRawValue)"
        }
        return result
    }

Unit tests for this code have been left as an exercise for the student.

Categories
Software

Binding to a Dictionary

SwiftUI is mostly awesome, but sometimes in the corners things get a bit messy. In my recent work on the multi-lingual flash cards app, I encountered one such corner.

I am planning to include languages that use writing systems other than the good ol’ Western Alphabet. These include:

  • ਜਪਾਨੀ (Japanese)
  • 旁遮普语 (Punjabi)
  • китайський (Chinese)
  • ウクライナ語 (Ukrainian)

Just for fun, the above list is: Japanese (in Punjabi), Punjabi (in Chinese), Chinese (in Ukrainian), and Ukrainian (in Japanese)

When using the app, I want to give users the choice to see these words in their native script or in the ‘Roman’ alphabet. But I didn’t immediately how to implement this ‘feature.’

I saw two challenges:

  1. How to store the differently scripted versions of the same language
  2. How to map user preferences to the list of languages to use when quizzing users.

Problem #1: Storing the Languages

I ended up creating multiple localizations for each languages. For Japanese I used ‘ja’ and ‘ja-JP.’ The ‘ja’ localization stores the kana (and possibly kanji) version of the flash card content. The ‘ja-JP’ localization stores the romaji version of the content.

To be honest, I don’t LOVE this implementation option, but I really didn’t see anything better. Try not to judge me too harshly!

Problem #2: Mapping the user preferences to the languages list

Thanks to my solution to problem #1, the internal list of available languages will now look something like: en, fr, ja, ja-JP. But we never want to show users this list. Instead we will want to show them either ja or ja-JP. Depending on a user’s preferences, their language list will either be: en, fr, ja OR en, fr, ja-JP.

For each language with local script or western/roman alphabet options, the user will set a bool preference value. The bool preference values will be used to create a set of excluded languages. Here is the logic for the case where Japanese is the only multi-script language.

    var scriptExcludedLanguages: [Language] {
        let ja: Language = useNativeScript ? .ja_roman : .ja_nonRoman
        return [ja]
    }

The app can then remove the scriptExcludedLanguages to generate the list of languages available to the current user with the following code:

    var allLanguages: Set<Language> {
        let result = Language.allLanguages.subtracting(scriptExcludedLanguages)
        return result
    }

In the course of implementing this feature, I uncovered one other piece that ended up being non-obvious.

Problem #3 Binding the UI and UserDefaults for the Dictionary of Bools

First I defined the type:
typealias ScriptPickers = [String: Bool]
And then in the picker view added the following AppStorage property:
    @AppStorage("scriptPickers") var scriptPickers:  ScriptPickers = ScriptPickers.defaultDictionary
This lead to the following cryptic compiler error:
No exact matches in call to initializer 
It turned out the fix for this was to make ScriptPickers conform to RawRepresentable. Here’s what that looks like:

extension ScriptPickers: RawRepresentable where Key == String, Value == Bool {
    public init?(rawValue: String) {
        guard let data = rawValue.data(using: .utf8),  
            let result = try? JSONDecoder().decode(ScriptPickers.self, from: data)
        else {
            return nil
        }
        self = result
    }

    public var rawValue: String {
        guard let data = try? JSONEncoder().encode(self),   
              let result = String(data: data, encoding: .utf8) 
        else {
            return "{}"  // empty Dictionary respresented as String
        }
        return result
    }

}
// hat tip: actw https://stackoverflow.com/questions/65382590/how-to-use-appstorage-for-a-dictionary-of-strings-string-string 

But there was still the need to map the UI toggles to the ScriptPickers dictionary. Each language toggle needs a binding to the corresponding entry in dictionary. Here is the basic structure for doing that.

struct ContentView: View {
    
    @AppStorage("scriptPickers") var scriptPickers:  ScriptPickers = ScriptPickers.defaultDictionary

    var body: some View {
        VStack {
            ForEach(scriptPickers.keys.sorted(), id: \.self) { key in
                Toggle(key, isOn: binding(for: key))
            }
        }
        .padding()
    }
    private func binding(for key: String) -> Binding<Bool> {
        return .init(
            get: { self.scriptPickers[key, default: false] },
            set: { self.scriptPickers[key] = $0 })
    }

}

I am no noticing that some languages don’t just have roman and non-roman options. Punjabi for example can be expressed in Gurmukhi, Shamukhi, and the roman alphabet. Japanese can be expressed in Kanji (pictograms), Kana (non-Roman alphabets), and Romaji (using the Roman alphabet.)
So in the future, it feels like this code may need to be extended. Also, it feels like the languages should be expressed as an enum, rather than as strings.