HK Blog

Early last month, WWDC conference took place (virtual like last year) with a bunch of new announcements regarding the Apple development ecosystem. While everyone else (including myself) is excited about the new async / await feature or the brand new Xcode cloud, I think that there's a cool feature added to the Foundation framework that is underrated by a good portion of iOS developers: the new Attribute String API.

If at any point, you had to deal with some type of attributed string you will probably know that is not a trivial thing to do. Even the most basic layout, like an underlined label, needs a block of code to setup the UI in a proper way which is very difficult to read and understand. Even worse, if your app is multi-language and needs to localize the strings, you need to create some type of utility with a custom syntax in your localizable strings to fit your needs. Until now, there was a lack of standardization when it comes to handling localizable attributed strings.

iOS 15 and Swift 5.5 come with a huge improvement in this area and also introduce a new language feature called Automatic Grammar Agreement. Let's take a look.

Attributed String

Markdown syntax support

We can now create an attributed string with a string that has a markdown syntax.

let welcomeString = try? AttributedString(markdown: "**Welcome**")

If you're using SwiftUI, you can also use the markdown syntax directly in your Text views.

struct ContainerView: View {
	var body: some View {
		VStack {
			Text("**Welcome**")
		}
	}
}

One of the coolest things about this compatibility is that we can combine multiple formats in a clearer and more readable way.

struct ContainerView: View {
	var body: some View {
		VStack {
			Text("**Welcome** _John_ \n Please visit our [website](http://www.example.com)")
		}
	}
}

Localizable & interpolation support

If you need to support multi-languages, you can now initialize an attributed string by taking advantage of the markdown syntax.

// Localizable.string - Spanish
"Welcome, _please visit our [website](http://wwww.example.com)_" = 
    "Bienvenido, _visita nuestro [sitioweb](http://wwww.example.com)_";
let welcomeAttributedString = 
    AttributedString(localized: "Welcome, _please visit our [website](http://wwww.example.com)_")

This is pretty cool already. But we can also add string interpolation to our localizable strings!. For example, if we want to give our user a more personal welcome, we can now do something like this:

// Localizable.string - Spanish
"Welcome **%@**, _please visit our [website](http://wwww.example.com)_" = 
    "Bienvenido **%@**, _visita nuestro [sitioweb](http://wwww.example.com)_";
let welcomeAttributedString = 
    AttributedString(localized: "Welcome **\(userName)**, _please visit our [website](http://wwww.example.com)_"

Automatic Grammar Agreement

Imagine that we have a coffee order app. In the checkout screen, we want to show the user their order as shown below:

// English
1 small cappuccino - $2.99
2 medium espressos - $4.99

But, if our app also supports Spanish, we need to re-order the text to match the correct grammar syntax. Also, we have to manage the singularity/plurality of the adjectives that we are using.

// Spanish
1 capuchino chico - $2.99
2 expresos medianos - $4.99

In English, you say medium wether if you're referencing one or more than one item. However in Spanish, you say mediano if you're referencing one item and medianos if you reference to more than one. In addition, in Spanish, the nouns precede the adjective. So, if your app supports multi-language, you have to think about all of this. Even for a small example like the one we just discussed.

The new feature Automatic Grammar Agreement takes this responsibility and does all the transformations automatically. To apply it when loading a localizable string, we have to use the Markdown syntax extension for Automatic Grammar Agreement:

^[ .... ] (inflect: true). We start with ^ symbol following with square brackets. Inside the brackets, we put the string that we want to inflect. To finish, we indicate that we want that portion of the text to be inflected between parenthesis.

// Localizable.string 

"small" = "small";
"medium" = "medium";
"espresso" = "espresso";
"cappuccino" = "cappuccino";
// 1 small cappuccino - $2.99
"^[%lld %@ %@](inflect: true) - **$%@**" = "%1$lld %2$@ %3$@ - **$%%4$@**";

// Localizable.string - Spanish
"small" = "chico";
"medium" = "mediano";
"espresso" = "expresos";
"cappuccino" = "capuchino";
"^[%lld %@ %@](inflect: true) - **$%@**" = "%1$lld %3$@ %2$@ - **$%%4$@**";
let coffeeCount = 2
let coffeeSize = String(localized: "medium")
let coffeType = String(localized: "espresso")
let price = 4.99

let attributedSring = 
    AttributedString("^[\(coffeeCount) \(coffeeSize) \(coffeeType)](inflect: true) - **\(price)**")

// English will print:
2 medium espressos - $4.99
// Spanish will print:
2 expresos medianos - $4.99
let coffeeCount = 1
let coffeeSize = String(localized: "small")
let coffeType = String(localized: "cappuccino")
let price = 2.99

let attributedSring = 
    AttributedString("^[\(coffeeCount) \(coffeeSize) \(coffeeType)](inflect: true) - **\(price)**")

// English will print:
1 small cappuccino - $2.99
// Spanish will print:
1 capuchino chico - $2.99

This is a huge improvement, although we still need to translate the text ourselves, we don't have to change the order of the parameters in our code depending of the language that the user has configured.

Furthermore, iOS 15 will come with a new setting option: Term of Address with which we can specify our gender and how we want to be treated by the system. This also will be handled automatically by the grammar agreement feature.

TermOfAddress

In Spanish, the noun that we use could be different given the gender. For example, for Welcome, we should say Bienvenido if the user is a male or Bienvenida if the user is a female.

Before iOS 15, we probably needed to have something like this:

// Localizable.string - Spanish

"Welcome" = "Bienvenido/a";

With Automatic Grammar Agreement we can just use the inflect attribute in the syntax

// Localizable.string - Spanish

"^[Welcome](inflect: true)" = "Bienvenido";

// Will print: Bienvenido or Bienvenida depending on the user's configuration

Please be aware that all these features are in beta stage, so, you could find some instability and some changes in the future.

If you want to dive deep into this subject, you can watch the WWDC talk and read the official documentation.