We drastically improved the analytics code of the NotesViewModel class in the past few episodes, but we still use a few string literals at the call site. That is something we address in this episode.
Eliminating string literals has several benefits. The most obvious benefit is that we don't need to worry about typos. There are a few other benefits that we explore in this episode.
Adding Type Safety
To eliminate the string literals at the call site, we need to declare a few types. Add a Swift file to the Models group, name it Kind.swift, and declare an enum with name Kind.
Notes can be categorized by their type or kind. That is what the Kind enum defines. We define two cases for now, blank and template. A template has a name and that name is stored as an associated value of the template case.
import Foundation
enum Kind {
// MARK: - Cases
case blank
case template(String)
}
Open Note.swift and change the type of the kind property to Kind.
import Foundation
internal struct Note {
// MARK: - Properties
let title: String
let body: String
let kind: Kind
...
}
We also need to update the implementation of the static example property in the extension for Note.
extension Note {
static var example: Self {
.init(
title: "Morbi volutpat ...",
body: "In hac habitasse ...",
kind: .blank
)
}
}
Let's integrate the Kind enum into the Journey API. Open Journey.swift and revisit the Property enum. The associated value of the kind case is of type String. We change the type to Kind.
enum Property {
// MARK: - Cases
case kind(Kind)
case source(String)
case wordCount(Int)
...
}
We also need to update the computed value property of the Property enum. Even though the compiler doesn't complain, passing an object of type Kind to the analytics service is likely to result in a problem. It is safer to convert the Kind object to a string.
Revisit Kind.swift and declare a computed property with name rawValue of type String. In the body of the computed property, we use a switch statement to map or convert each case to a string. Notice that the raw value of the template case includes its associated value.
import Foundation
enum Kind {
// MARK: - Cases
case blank
case template(String)
// MARK: - Properties
var rawValue: String {
switch self {
case .blank:
return "blank"
case .template(let name):
return "template " + name
}
}
}
Head back to Journey.swift. We use rawValue in the computed value property of the Property enum to convert the Kind object to a string.
var value: Any {
switch self {
case .kind(let kind): return kind.rawValue
case .source(let source): return source
case .wordCount(let wordCount): return wordCount
}
}
Removing String Literals
Open NotesViewModel.swift. With the Kind enum integrated into the Journey API, we can eliminate a few more string literals at the call site. The associated value of the kind case is provided by the kind property of the Note object.
import Foundation
internal final class NotesViewModel {
// MARK: - Public API
func createNote(_ note: Note) {
Journey.createNote
.properties(
.source("home"),
.kind(note.kind)
).send()
}
func updateNote(_ note: Note) {
Journey.updateNote
.properties(
.source("home"),
.kind(note.kind),
.wordCount(note.wordCount)
).send()
}
func deleteNote(_ note: Note) {
Journey.deleteNote
.properties(
.kind(note.kind),
.wordCount(note.wordCount)
).send()
}
}
Creating Intuitive APIs
The steps we need to take to change the type of the associated value of the source case of the Property enum are similar. Add a Swift file to the Models group, name it Source.swift, and declare an enum with name Source. The raw values of the Source enum are of type String. We define two cases, home and siri.
import Foundation
enum Source: String {
// MARK: - Cases
case home
case siri
}
Integrating the Source enum into the Journey API is straightforward. Open Journey.swift and revisit the Property enum. Change the type of the associated value of the source case to Source.
enum Property {
// MARK: - Cases
case kind(Kind)
case source(Source)
case wordCount(Int)
...
}
Updating the computed value property isn't difficult because the Source enum already defines a raw value for each case.
var value: Any {
switch self {
case .kind(let kind): return kind.rawValue
case .source(let source): return source.rawValue
case .wordCount(let wordCount): return wordCount
}
}
It is time to remove the remaining string literals in NotesViewModel.swift. The associated value of the source case is no longer a string. It is a Source object instead and that is a welcome improvement. Not only does it eliminate typos, it makes the Journey API more intuitive.
import Foundation
internal final class NotesViewModel {
// MARK: - Public API
func createNote(_ note: Note) {
Journey.createNote
.properties(
.source(.home),
.kind(note.kind)
).send()
}
func updateNote(_ note: Note) {
Journey.updateNote
.properties(
.source(.home),
.kind(note.kind),
.wordCount(note.wordCount)
).send()
}
func deleteNote(_ note: Note) {
Journey.deleteNote
.properties(
.kind(note.kind),
.wordCount(note.wordCount)
).send()
}
}
You no longer need to remember what the possible values of the source property are. Xcode shows you a list of options defined by the Source enum. The Journey API is type safe and implicitly documented. Extending the API is as simple as extending the Source enum.
What's Next?
We no longer need to use string literals at the call site and that is important. Eliminating the risk for typos is good, but I hope you can appreciate the other benefits the Journey API introduces, such as type safety and consistency. Because the associated values of the kind and source cases of the Property enum are no longer of type String, the compiler throws an error if we make a mistake.
In the next episode, we change gears and focus on the testability of the analytics library we are building. Journey is tightly coupled to the GoogleAnalyticsClient class and that compromises its testability. We address this by taking a protocol-oriented approach.