Last year we explored some NSFormatters and how to use them. We also explored some formatters introduced in iOS 13. Finally, a few weeks ago we learned about yet more formatters, and how to better use the ones we already had. In short, we have explored how powerful NSFormatter is. One thing we haven’t done yet though, is to write our own custom NSFormatter
subclass.
NSFormatter
NSFormatter
is an abstract class. All formatter classes inherit from it. In Swift, everything we need about it is open
, so we can create our own NSFormatters
with ease.
Overriding NSFormatters
The class comes with many methods you can override, but you must, at the very least, override the following:
func string(for obj: Any?) -> String
getObjectValue(_ obj: AutoreleasingUnsafeMutablePointer<AnyObject?>?, for string: String, errorDescription error: AutoreleasingUnsafeMutablePointer<NSString?>?) -> Bool
These look a bit messy, especially the second method. The second method will return us a formatted object. It can be a string, or anything else that makes sense in the context of your app.
Other methods you can override include:
attributedStringForObjectValue:withDefaultAttributes
: Suppose you have a string that is supposed to represent a big title and a smaller title underneath. You can use this formatter to format such strings.editingStringForObjectValue:
: You can override this when you are editing a string and the string your users see are different. By default, this will callstringForObjectValue:
.
There’s a few other ones, but they are either only useful in macOS or very complicated to implement (beyond this article). We won’t implement all of the methods, but be aware they exist so you can write a formatter that fits your needs.
EmojiFormatter
To show how to write our own formatters, we will create EmojiFormatter
. The formatter will take strings with old-school emoticons - such as :-), :-(, :-|, ;-(
- and it will replace them with an actual emoji - like π, βΉοΈ, π, π’
.
Because this formatter operates on strings and returns strings, we will define two things before moving on:
- The String representation will be the string that contains the Emoji. For example,
I'm happy to talk to you π
. - The original object will be the string with raw ASCII emoticons.
I'm happy to talk to you :-)
.
Implementing Emoji Formatter
To write the class, start by subclassing Formatter
and overriding the two mandatory methods:
class EmojiFormatter: Formatter {
override func string(for obj: Any?) -> String? {
}
override func getObjectValue(_ obj: AutoreleasingUnsafeMutablePointer<AnyObject?>?, for string: String, errorDescription error: AutoreleasingUnsafeMutablePointer<NSString?>?) -> Bool {
}
}
Next, we will add a property to make the formatting a bit easier through ASCII/emoji mapping.
let emojiMapping = [
":-)": "π",
":-|": "π",
":-(": "βΉοΈ",
";-(": "π’"
]
Next, implement two methods: One will replace ASCII with emoji, and the other will replace emoji with ASCII.
func replaceAsciiWithEmoji(in string: String) -> String {
var rawString = string
emojiMapping.forEach {
rawString = rawString.replacingOccurrences(of: $0, with: $1)
}
return rawString
}
func replaceEmojiWithAscii(in string: String) -> String {
var rawString = string
emojiMapping.forEach {
rawString = rawString.replacingOccurrences(of: $1, with: $0)
}
return rawString
}
Finally, we need to implement the overriden methods in order to use this formatter. They are pretty straightforward.
override func string(for obj: Any?) -> String? {
if let string = obj as? String {
return replaceAsciiWithEmoji(in: string)
}
return nil
}
override func getObjectValue(_ obj: AutoreleasingUnsafeMutablePointer<AnyObject?>?, for string: String, errorDescription error: AutoreleasingUnsafeMutablePointer<NSString?>?) -> Bool {
obj?.pointee = replaceEmojiWithAscii(in: string) as AnyObject
return true
}
The last method looks complicated. It’s important to remember that (NS)Formatter
is an Objective-C class, and thus it has kept most of its original interfaces when exposing it to Swift. The obj AutoreleasingUnsafeMutablePointer
contains a reference to the object that will be returned upon converting the string into something. In this case, it’s a string.
Formatter Usage
With that, our formatter is done. We can get an ASCII representation of an emoji string like so:
let emojiFormatter = EmojiFormatter()
print(emojiFormatter.string(for: "I'm happy to talk to you π how you doin'? π") ?? "") // I'm happy to talk to you :-) how you doin'? :-|
To get a “formatted” emoji string - that is, to convert its ASCII representations to actual emoji, we need to do a little bit of more low-level stuff and bridging:
var formattedEmojiStringContainer: AnyObject?
var fstring = "I'm happy to talk to you π how you doin'? π"
var errorDescription: NSString?
emojiFormatter.getObjectValue(&formattedEmojiStringContainer, for: fstring, errorDescription: &errorDescription)
print(formattedEmojiStringContainer!) // I'm happy to talk to you :-) how you doin'? :-|
Of course, this is less than ideal, and all formatters included in Foundation contain methods so developers don’t have to do that dirty work themselves. Before continuing, we are going to write a rawString(for emojiString: String)
methods that takes a String with Emoji and turns them into ASCII emojis.
public func rawString(for emojiString: String) -> String? {
var formattedEmojiStringContainer: AnyObject?
getObjectValue(&formattedEmojiStringContainer, for: emojiString, errorDescription: nil)
return formattedEmojiStringContainer as? String
}
And now we have a nice interface to formate ASCII emojis into actual emojis.
print(emojiFormatter.emojiString(for: "I'm happy to talk to you π how you doin'? π") ?? "") // I'm happy to talk to you :-) how you doin'? :-|
And that’s how you create a basic formatter! In a future article, we are going to create a more powerful formatter that will show us everything we can do with NSFormatter
.
A reference of the entire class we wrote in this article is below:
class EmojiFormatter: Formatter {
// MARK: - User facing methods
public func rawString(for emojiString: String) -> String? {
var formattedEmojiStringContainer: AnyObject?
getObjectValue(&formattedEmojiStringContainer, for: emojiString, errorDescription: nil)
return formattedEmojiStringContainer as? String
}
// MARK: - Emoji Mapping
let emojiMapping = [
":-)": "π",
":-|": "π",
":-(": "βΉοΈ",
";-(": "π’"
]
func replaceAsciiWithEmoji(in string: String) -> String {
var rawString = string
emojiMapping.forEach {
rawString = rawString.replacingOccurrences(of: $0, with: $1)
}
return rawString
}
func replaceEmojiWithAscii(in string: String) -> String {
var rawString = string
emojiMapping.forEach {
rawString = rawString.replacingOccurrences(of: $1, with: $0)
}
return rawString
}
// MARK: - Overriden methods
override func string(for obj: Any?) -> String? {
if let string = obj as? String {
return replaceAsciiWithEmoji(in: string)
}
return nil
}
override func getObjectValue(_ obj: AutoreleasingUnsafeMutablePointer<AnyObject?>?, for string: String, errorDescription error: AutoreleasingUnsafeMutablePointer<NSString?>?) -> Bool {
obj?.pointee = replaceEmojiWithAscii(in: string) as AnyObject
return true
}
}
Why NSFormatter? Why not create custom formatting classes?
Seeing this example, you may be thinking that it may be easier to just create an EmojiFormatter
that doesn’t inherit from (NS)Formatter
. After all, basic formatters are just going to need string replacing at most. So what’s the point?
One big advantage of using NSFormatter
is that there’s multiple places all over Foundation and the rest of the iOS, macOS, and other Apple Platforms APIs. Because they take an NSFormatter
, you can pass them any custom formatters.
Even SwiftUI has APIs that take formatters. As of WWDC2020, you can interpolate in SwiftUI strings that take a formatter.
Text("\("I'm happy to talk you :-)" as NSObject, formatter: EmojiFormatter())")
Other than that cast to NSObject
, this is very straightforward to use, and you can expect multiple places all over Apple’s technologies that take formatters and you can pass your own.
Conclusion
Formatters never cease to amaze us. Writing our own can be very easy, but it has everything you need to write more complex formatters. There’s various APIs that can take formatters throughout all the APIs in Apple’s platforms, and thus subclassing (NS)Formatter to get that default functionality can be very rewarding.