Why data is important

Data is what makes apps viable. It is what drives the user interactions and informs them of what’s going on. While some apps use data in the form of text, there are other apps that use very fine grained measurements and need to present these in ways that are appropriate for the user.

Respecting the users locale

When dealing with data relating to measurements we want to be respectful of the users locale and language settings. If we have a value that has been recorded as meters per second and the users default measurement for speed is miles per hour, we want to respect that when we display the value to the user. Similarly if a temperature measurement is in degrees Kelvin, we want to format that in the users default value such as Celsius or Fahrenheit.

Creating a custom Formatter

A lot of formatters already exist and are provided by as part of the respective platforms and should be preferred over a custom implementation of Formatter. There is also sample code from Apple which demonstrates the use of formatters in an app.

Though there are times when we want to take a string such as “ABCD1234EFGH5678” and turn it into something like “ABCD-1234-EFGH-5678” which is then displayed to the user. To do this, we want to create a custom formatter. This sounds complex, but it is as a straight forward and the good thing is that the implementation can be unit tested.

Define the subclass

To get started, define a new value type such as the following.

class SuperAwesomeCustomFormatter {

}

Create the implementation

After defining the class, we can build out the implementation. The amazing thing about how this protocol is defined is that it doesn’t case what the value being passed in is. It is the responsibility of the formatter to determine if it knows how to provide an appropriate String as a result.

extension SuperAwesomeCustomFormatter: Formatter {

  override func string(for obj: Any?) -> String? {
    /* 
    super secret custom implementation goes here okay, 
    just make sure you return an optional String value.
    The actual implementation is left as an exercise
    for the reader to look at.
    */
    nil
  }

}

The Formatter class provides the definition string(for:) as well as attributedString(for:withDefaultAttributes:) and editingString(for:). This makes the Formatter subclass super powerful and adaptable to the use case. Do take note of the default behaviour discussion in the apple documentation if you are overriding these values.

Write some unit tests

As we are dealing with formatting strings we want to make sure that it does what is expected. So for this we write some unit tests. These tests can be as simple or as complex as you want.

class SuperAwesomeCustomFormatterTests: XCTestCase {

  func testFormatsString() {
    let testSubject = SuperAwesomeCustomFormatter()
    let expected = "1234-ABCD-4567-EFGH"
    let result = testSubject.string(for: "1234ABCD4567EFGH")
		
    XCTAssertEqual(expected, result)
  }

}

Showing formatted values

A lot of SwiftUI types work with a formatter such as Text and TextField. This is demonstrated by a super simple view like the following which presents a TextField and uses an appropriate custom formatter for the value.

struct ContentView: View {

  @State var superAwesomeString: String = ""

  var body: some View {
    VStack {
      TextField(
        "Super Awesome Value",
        value: $superAwesomeString,
        formatter: .superAwesomeFormatter
      )
    }
    .padding()
  }

}

extension Formatter {

  static var superAwesomeFormatter: Formatter = {
    SuperAwesomeFormatter()
  }()

}