We’re helping him get set up in residence at Carleton res. Looks like all systems are go…

We’re helping him get set up in residence at Carleton res. Looks like all systems are go…

I recently used two cameras to take photos at a soccer game. One with a moderately zoom-y lens (80-200mm) the other with a more zoom-y lens (200-500mm).

When I was looking through the uploaded photos I noticed the time stamps of the two cameras didn’t line up. I’ve seen this before when I have some photos from my phone (which is very smart with time zones and daylight savings) and other photos from an SLR. (much less smart with time zones and daylight savings) Lightroom is very good about letting you select the photos from one camera and offsetting their capture times by a specific amount.
But this is where I made a strategic blunder! I adjusted camera 1 to match the clock of camera 2. Next I went to Lightroom to adjust the times of the camera1 photos. This would have been easy if I knew the delta between the clocks in the two cameras. Sadly I had just destroyed that evidence (by synching the camera clocks.)
So I tried estimating, which was close, but there were a few places where photos from both cameras now had the same capture time, and I was unable to judge from the content whether I needed to increase or decrease the compensation.
I was fairly confident that if I could present the capture times visually (as two data series in a graph) it would be obvious how much I’d need to shift one series to sync with the other series. If only I could extract all the time stamps from both sets of photos, and present them in a graph.
But how do I get all the time stamps of a set of photos in Lightroom? A bit of research taught me I would need to either buy or create a Lightroom plug-in. Being both stingy and keen to learn stuff like how to write a Lightroom plug in, I chose: create!
Martin Fowler has a very helpful page for people that want to create their first Lightroom plug-in. This page also has a line that rings true for me.
“As a programmer, I’m always looking for ways to spend several hours programming to save an hour’s work.”
Martin Fowler
I feel seen.
So I followed the basic arc of Martin’s Lightroom/Lua journey, and was able to extract the needed time stamp values.
For the curious among you, it looked something like this:

I was then able to clean it up and import it into Numbers. (And struggle to get Numbers to show a scatter plot where each series has its own X values. Not intuitive. Thank you YellowBox) And I created a graph like this:

I was definitely going to need to stretch it out, in order to be try out different offset values. Once I stretched it, I was able to shift the blue dots left so that the green dots all fell in gaps.

Of course, next time I’m in this situation I will remember to sync my cameras before I start. And as a fallback, if I do end up using un-synched cameras, I will be sure to fix the photo metadata before I sync the camera clocks.
And for any Lua fans out there, here’s my plug-in code.
local LrApplication = import 'LrApplication'
local LrLogger = import 'LrLogger'
local LrTasks = import 'LrTasks'
local myLogger = LrLogger( 'lightroomLogger' )
myLogger:enable( "logfile" )
local function log( message )
myLogger:trace( message )
end
local catalog = LrApplication.activeCatalog()
local function findCandidates()
return catalog:findPhotos {
searchDesc = {
criteria = "captureTime",
operation = "thisMonth",
value = 2024-08-04,
}
}
end
local function showDates()
LrTasks.startAsyncTask(function()
local photos = findCandidates()
for i, v in ipairs(photos) do
-- local prop = v:getRawMetadata("fileSize")
local createTime = v:getRawMetadata("dateTime")
local colourName = v:getRawMetadata("colorNameForLabel")
local createTimeOriginal = v:getRawMetadata("dateTimeOriginal")
local fileName = v:getRawMetadata("path")
local result = "orig, " .. createTimeOriginal .. ", " .. createTime .. ", " .. colourName .. ", " .. fileName
log(result)
end
end )
end
showDates()
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:
Viewself.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
Somehow, I’ve gone for years without being aware of this setting in the Test Targets used in Xcode projects. Apologies if this has been obvious to everyone on Earth other than me.
In my flash cards app, I needed logic to figure out which cards could be used for a given user’s ask/answer languages setting. For example, if a user wants to always use Korean as their ask language, and all other languages available as answer languages, then only cards that included Korean and at least one other language could be presented.
Also, bear in mind that for languages that have roman and non-roman script versions, they need to be treated as two separate languages, only one of which is relevant, depending on which the user has selected in Settings.
My first pass at the logic to pick the next card erred on the side of simple, rather than safe. It used the following process:
As I mention in the list, there is a bug in step 3 that can cause a crash later in the process. (Better would be to say at the get go, something like: ‘I don’t have any words that match your languages choices’
Also, the above has a highly buried dependency on the user’s setting/pref on whether they want to use the roman version or the non-roman version of languages that have their own special script/alphabets.
So (we’re getting VERY close to the topic of this post) I am currently in the process of improving this process by pulling the user setting dependency out (or at least make it injectable when testing.)
So I wrote a test, and attempted to run it. But when I ran my single test, with its shiny new injected dependency it never ran, because Xcode first insisted on running my main app, which of course was still crashing when attempting to use the old/broken logic for picking a card.
Thanks to a great answer from the ever helpful Quinn, I was able to discover this setting in Xcode.

Once I set it to None, I was able to run my stand alone test, and resume creating my improved card picking logic.
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.
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)
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.
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)
andpublic 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.
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:
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:
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!
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.
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:
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:
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.
(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:
JSON to a [String: Localization] dictionary[Language: Localization] dictionary[String: Localization] dictionary"en" to Language.en[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.