Morphology in Swift
This post is brought to you by Emerge Tools, the best way to build on mobile.
Morphology is more exciting than what the definition on the tin might suggest. The study of the formation of words certainly doesn’t garner hot conference talk C.F.P.s, nor does any A.P.I. around the subject beget much praise.
But maybe it should?
After all, we’ve all seen the “morphology version” of a cardinal sin, where strings end up reading like this (from left to right):
- “She will receive 0 game”: Awful +.
- “She will receive 0 game(s)”: Awful -. Passable but lazy.
- “She will receive 0 games”: Correct!
And the problem becomes even more exacerbated once the underlying data model is mutated, and the user interface doesn’t keep up.
Why does this kinda thing happen? Well, in short, it’s a little bit of a pain in the [REDACTED] to get it right. What seems simple in terms of engineering can turn into boilerplate, drone-worthy code that’s trivial to get wrong. Take our video game shopping sample app from above, maybe it had code that looked similar to this:
if selectedGames.isEmpty {
checkoutString = "She will receive 0 games."
} else if selectedGames.count == 1 {
checkoutString = "She will receive 1 game."
} else {
checkoutString = "She will receive \(selectedGames.count) games."
}
And that’s not even considering localized strings. Regardless, it’s a tawdry implementation that too many apps end up shipping anyways. Even still, it’s a problem that has more nuance than that.
Why Morphology is Inherently Difficult
Think of all of the beautiful intricacies our languages have. For example, in English, adding the prefix “re-“ to a verb indicates repetition, as in “do” to “redo”. Or, take Spanish, where the suffix “-azo/-aza” added to a noun refers to a sudden, forceful action, as in “puerta” to “portazo”.
Those are fairly elementary examples, so perhaps you’re looking for a more…colorful illustration? If so, may I suggest to you…
antidisestablishmentarianism?
This word, one of the most pure examples of morphology in the English language, refers to a political stance which opposes the removal of a state-sponsored religion or church. During the 19th century, it was specifically used during debates concerning the disestablishment of the Church of England.
The word itself, though? It consists of several smaller morphemes which are combined to form a single word that has a very specific meaning:
- “Anti-“ is a prefix that means “against”
- “dis-“, another prefix, means “not” or “reverse”
- “establishment”, the root word, refers to a governing or controlling group
- “-arian”, a suffix, which means “pertaining to” or “advocating for”
- And finally, “-ism” is a suffix that means “belief in” or the “practice of.”
Thus, we arrive at antidisestablishmentarian.
All of these formations, the things they mean and all of the ways that words can change by virtue of them, are examples of morphology. And in programming, we encounter interesting cases of morphology all the time, whether we realize it or not.
In short, many languages use certain terminology to express all sorts of things about someone or something, and because of that - there are several ways in which they combine nouns, gender and their pluralizations to produce sentences uniquely tailored to a certain audience.
When those words are structured correctly, they are achieving a form of grammar agreement. When they don’t, well - see the picture above once more.
Back to Swift
So, now that we have a passing idea of what morphology is, and what it looks like when it’s represented incorrectly - how do make sure we’re getting it right in our own apps? Are we destined to write code that includes several localization entries, and if
statements determining pluralization cases?
Of course not. Fortunately, this problem has been solved by Foundation since iOS 15. In fact, the above issue can be solved like this:
Text("She will receive ^[\(count) games](inflect: true).")
And now, we correctly get all of the string values we’d expect:
This means you can delete a lot of “Use this string for this quantity/condition/etc” code, along with their localized string entries.
Using Inflection
You’ve likely noticed a few interesting things about the string above. Notably, it uses a special kind of interpolation that follows this format:
^[/*the string*/](inflect: true)
This opts the string into automatic grammar agreement, and so long as we’re using an AttributedString
type and localized strings1 within them, Foundation now takes care of the rest.
Further, Foundation can also efficiently handle terms of address. In our example, I was buying video games for my wife, i.e. “She will receive…”. But, what if I was buying them for a brother, or I had no idea who I was buying it for?
In recent versions of iOS, there are options to select your preferred term of address found under the Language & Region settings. In languages, such as Spanish, this option is used with the grammar agreement engine to create more personalized, grammatically correct strings:
Here, the system can more accurately represent the user throughout iOS in Spanish speaking locales. For example, the word “bienvenido” changes depending on the term of address someone is using. Each option would result in a different, more grammatically correct string for the word “bienvenido” and others like it. This better aligns with how Spanish speakers naturally communicate, as they may use diverse forms of a word based on the context of a conversation.
What does this mean for you and I? It means that Foundation allows us to not worry about any of this, and it will correctly use “bienvenida” for a female address, and “bienvenido” for a masculine tone.
Further, let’s say the user hasn’t selected any option for their term of address. Or, perhaps they have not granted apps access to it (in the screenshot above, that’s what the bottom toggle is for). In those cases, you can provide a generalized, all-purpose string to be used by including the inflectionAlternative
parameter.
In fact, this is similar to how Apple handles its “Welcome to Notes” copy:
Text("^[Used if there is a term of address](inflect: true, inflectionAlternative: 'Used if there isn\'t').")
Avoiding String Interpolation
You may be amazed that such a heavy lifting A.P.I. even exists in Foundation, but perhaps you’re a little scared off by the…stringyness of it. It’s true that it’s perhaps too easy to screw all of this up. For example, can you quickly spot why this doesn’t work?
Text("She will receive ^[\(count) games](inflected: true).")
No? I accidentally wrote inflected
instead of inflect
.
If you’ve been around the iOS block for a bit, perhaps this is all a little bit reminiscent of the visual format language of yore. Powerful and handy, no doubt - but it could also be a foolhardy endeavor. No matter, you can strongly type the process too by way of the Morphology
struct.
This way, you can perform manual grammar agreement at runtime. There are a couple of reasons as to why you’d go this route. For example - maybe you don’t have the data about someone until runtime to make a decision about which morphology rules should be used.
Every AttributedString
has a InflectionRuleAttribute
key for you to assign a Morphology
struct to. In fact, that’s what the grammar engine is doing under the hood with our string interpolation code in our examples up to this point. All the same, we could rewrite our video game example at the top of the post from this:
Text("She will receive ^[\(count) \(games)](inflect: true).")
…to this:
private func strongTyped() -> AttributedString {
var string = AttributedString(localized: "They will receive \(count)
games.")
var morphology = Morphology()
// In my case, I'm sending these to my wife. So we will set a feminine gender.
morphology.grammaticalGender = .feminine
let number: Morphology.GrammaticalNumber
switch count {
case 0:
number = .zero
case 1:
number = .singular
default:
number = .plural
}
morphology.number = number
string.inflect = InflectionRule(morphology: morphology)
let formattedResult = string.inflected()
return formattedResult
}
…and we get the same result.
You can do a lot of fine tuning with Morphology
, from using different pronouns on a per-language basis to refer to a third-person context, to checking whether or not the device’s selected language supports grammar agreement2 by passing in its BCP 47 language code:
if InflectionRule.canInflect(language: "en-IE") {
// Supports Ireland
}
Final Thoughts
Foundation is still, many years on, the unsung hero of iOS. It underpins so much of iOS programming, and without it - we’d spend every single day reinventing the wheel.
And with that, I hereby decree that henceforth, you are no longer but a simple programmer, but also officially a morphologist. You are no longer going to correct strings in your app, you are performing complex morphologic enhancements.
Until next time ✌️
-
I should note that it technically can be used without localized string entries. If the scheme’s language is defaulting to one that is supported by Foundation’s automatic grammar agreement feature, it’ll work out of the box. ↩
-
As far as I can tell from the documentation, Spanish and English are the two languages supported as of iOS 16. ↩