Not so long ago I discussed in this post about how to define enums with custom raw types in Swift. Creating enums with cases that can be either of a primitive data type, or a custom one as presented in that previous post, helps achieving a simple, yet quite important goal; to use values in our code that are strongly typed, instead of plain raw values. Doing so adds a lot to writing safer code, and avoid ambiguities and potential problems throughout the implementation process of an app.
Enum cases as custom strongly typed values
Say, for instance, that we have a variety of color values expressed as hexadecimal strings to use in various places around an app. Along with that, there’s also a hypothetical method that converts a hex string to an actual color supposedly named color(fromHex:)
. The simplest thing we could do in order to get an actual color would be to supply a hex string value as argument to that method:
1 2 3 4 5 |
let someColor = color(fromHex: “ff0000”) let anotherColor = color(fromHex: “009192”) // … |
There are two disadvantages here; the first is the unavoidable need to remember the various hex color values. Since that’s rarely feasible, we’ll be resorting to another list containing the full range of available color values, ending up with a significant unnecessary hassle.
The second negative point, which is more important in programming level, is that’s pretty easy to make typing errors, or accidentally pass invalid values as arguments.
Defining an enum with a raw type would lead to counterattack these two issues, as:
- there would be no need to keep all possible color values in our memory, as Xcode would be suggesting them in autocompletion,
- we would be providing the enum’s cases as arguments to
color(fromHex:)
, so no more potential errors.
For example:
1 2 3 4 5 6 7 8 9 10 |
enum AvailableColors: MyColors { case someColor = “ff0000” case anotherColor = “009192” // More cases with hex raw values… } let someColor = color(fromHex: AvailableColors.someColor.rawValue) let anotherColor = color(fromHex: AvailableColors.anotherColor.rawValue) |
Note: To find out more about how to create enums with custom raw type as above, please have a read to this tutorial.
Even better, we could change the signature of the color(fromHex:)
method, so it accepts an AvailableColors
value, instead of the raw value of each case; it would be even simpler then to provide it with color values:
1 2 3 4 5 6 7 8 |
func color(fromHex: AvailableColors) -> Color { // Convert to SwiftUI color… } let someColor = color(fromHex: .someColor) let anotherColor = color(fromHex: .anotherColor) |
As you see, defining strongly typed values can be proved really helpful in the coding process; it ensures that no unwanted mistakes will take place, but besides safety, it also increases clarity, readability and implementation speed in the long term.
But, are enums the only available option in order to define custom strongly typed values?
Enums are not always the best solution
Enums are great candidates for use in cases like the scenario presented above, but they have a limitation; they cannot be extended with new cases.
Trying to do the following, for example, will fail with the error shown as a comment:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
enum SomeStrings: String { case valueOne case valueTwo } extension SomeStrings { case valueThree } // Xcode shows the following error: // Enum ‘case’ is not allowed outside of an enum |
You might think that this is not a big deal, as we write all possible cases in the enum’s body at once in the majority of circumstances. However, what if the enum is defined in a separate module, such as a Swift package or a different project in a workspace, and we don’t have the possibility to introduce new cases within our own project?
It turns out that enums are out of the table in such situations, but we still know that it has to be possible somehow. After all, there are built-in types that allow us to define custom strongly typed values and use them wherever necessary in place of raw values with primitive data types.
Take as example the NotificationCenter
and notifications; we almost never use string values to specify a notification name. We always define names by extending the Notification.Name
type and declaring static properties, before we post or observe for notifications in the NotificationCenter
:
1 2 3 4 5 6 7 8 9 |
// Define a custom notification name. extension Notification.Name { static let myCustomNotification = Notification.Name(“myCustomNotification”) } // Send a notification using the custom name. NotificationCenter.default.post(name: .myCustomNotification, object: nil) |
Obviously the Notification.Name
type does not reside in our project, but it becomes available through the built-in Foundation
framework. Yet, we can extend it, and declare new notification names. Undoubtedly it would be great if we could do the same with custom types, and guess what? We can, and the technique to manage that is described right in the next part.
Properties in structs as strongly typed values
As with enums, structs can also be used as a similar tool in order to define properties that we’ll use as custom strongly typed values. But in contrast to enums, structs are extendible, just like the previously shown example regarding the Notification.Name
type.
There is a quite straightforward technique to apply here, which resembles enums a lot; we deal with raw values as well, but instead of cases we have static properties. The most important part includes the conformance to two particular protocols; RawRepresentable
and Hashable
. Conforming to latter is not mandatory if we are not planning to include values of our custom type to dictionaries or sets, so we can occasionally omit it. Here’s an example:
1 2 3 4 5 |
struct Hex: RawRepresentable, Hashable { } |
The conformance to RawRepresentable
protocol brings along two requirements; to declare a rawValue
property with the data type that raw values are going to have, and a specific initializer:
1 2 3 4 5 6 7 8 9 |
struct Hex: RawRepresentable, Hashable { var rawValue: String init?(rawValue: String) { self.rawValue = rawValue } } |
The above default initializer will construct optional objects. It’s up to us to implement an additional initializer that will be creating non-optional objects, and without an argument label. If necessary, we may define as many parameters as needed:
1 2 3 4 5 6 7 8 9 |
struct Hex: RawRepresentable, Hashable { … init(_ rawValue: String) { self.rawValue = rawValue } } |
Besides the two requirements of the RawRepresentable
protocol and the additional initializer, such a custom type can contain everything else that’s considered to be useful in the implementation process. For instance, you can see right next that the demo Hex
type also contains two read-only computed properties that indicate if the raw string value starts with a hashtag, and if it contains valid characters or not:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
struct Hex: RawRepresentable, Hashable { … var startsWithHashtag: Bool { rawValue.starts(with: “#”) } var containsValidCharacters: Bool { let hexCharacterSet = CharacterSet(charactersIn: “0123456789ABCDEFabcdef”) return rawValue.rangeOfCharacter(from: hexCharacterSet.inverted)?.isEmpty ?? true } } |
Having the above custom type in place, we can extend it anywhere it might be needed and declare static properties that we’ll be using as custom strongly typed values:
1 2 3 4 5 6 7 |
extension Hex { static let red = Hex(“ff0000”) static let green = Hex(“00ff00”) static let blue = Hex(“0000ff”) } |
Moreover, we can now create functions, methods and other types that either accept or return Hex
objects instead of raw values. Suppose that we have the following method that converts a hex color value to RGB:
1 2 3 4 5 |
func convertToRGB(hex: Hex) -> (red: Double, green: Double, blue: Double) { // Convert hex to RGB… } |
Similarly to enum cases, providing a Hex
value as argument leaves no room for misunderstandings about what the given value is, and eliminates any chance to make mistakes:
1 2 3 |
let converted = convertToRGB(hex: .red) |
Of course, the raw value is always there in case we need it; it’s accessible through the rawValue
property:
1 2 3 |
let redRawValue = Hex.red.rawValue |
Overview
If there’s one thing that I have learnt all these years of programming, then that is that errors and mistakes are unavoidable. Since Swift gives us the tools to write safer code, why not to take advantage of that and minimize the risk of problems, even unwanted ones? On top of safety, and as I already mentioned at some point above, using custom defined strongly typed values makes the code more readable and comprehensive. I’m quite confident that enums had been in your arsenal already, but how about structs? I hope that this post has showed new coding paths to you, and ways for optimizations.
Thank you for reading! ????