Nested Naming in Swift Code#
Swift supports nested naming with other types, although it does not have dedicated naming keywords. Let's take a look at how we can use nested types to optimize the structure of our code.
Most Swift developers are accustomed to naming types based on their actual names in the structure. For example, PostTextFormatterOption (an Option for formatting Posts using a Text Formatter type). This may be because we have brought the terrible naming habits we developed in Objective-C & C into Swift.
Let's take the types mentioned above as examples and see the implementation of Post, PostTextFormatter, and PostTextFormatterOption:
struct Post {
let id: Int
let author: User
let title: String
let text: String
}
class PostTextFormatter {
private let options: Set
init(options: Set) {
self.options = options
}
func formatTitle(for post: Post) -> String {
return post.title.formatted(withOptions: options)
}
func formatText(for post: Post) -> String {
return post.text.formatted(withOptions: options)
}
}
enum PostTextFormatterOption {
case highlightNames
case highlightLinks
}
Now let's see what happens if we make the above types nested within Post:
struct Post {
class TextFormatter {
enum Option {
case highlightNames
case highlightLinks
}
private let options: Set
init(options: Set) {
self.options = options
}
func formatTitle(for post: Post) -> String {
return post.title.formatted(withOptions: options)
}
func formatText(for post: Post) -> String {
return post.text.formatted(withOptions: options)
}
}
let id: Int
let author: User
let title: String
let text: String
}
One great advantage of nested types is that we can quickly see the structure and relationships between types. We also reduce the length of initialization code, making it shorter and easier to read (the options parameter type changes from Set< PostTextFormatterOption > to Set< Option >).
The calling hierarchy is also more concise and clear - everything related to Post becomes Post.namespace. Formatting the text of a post looks like this:
let formatter = Post.TextFormatter(options: [.highlightLinks])
let text = formatter.formatText(for: post)
However, using nested types also has a significant downside. The code looks "upside down" because the actual content of the parent type is pushed to the bottom. Let's try to fix this problem by moving the code for nested types from above to below (and adding some MARKs for clarity):
struct Post {
let id: Int
let author: User
let title: String
let text: String
// MARK: - TextFormatter
class TextFormatter {
private let options: Set
init(options: Set) {
self.options = options
}
func formatTitle(for post: Post) -> String {
return post.title.formatted(withOptions: options)
}
func formatText(for post: Post) -> String {
return post.text.formatted(withOptions: options)
}
// MARK: - Option
enum Option {
case highlightNames
case highlightLinks
}
}
}
Whether to place nested types at the top or bottom is purely a personal preference. I prefer to have the content of the parent type at the top - while still enjoying the convenience of nested types.
In fact, there are several other ways to implement naming and nested types in Swift.
Using Extensions for Nested Types#
Another option for implementing nested types is using extensions. This method allows us to maintain the hierarchy relationship during implementation and calling, while clearly separating each type.
Here's an example:
struct Post {
let id: Int
let author: User
let title: String
let text: String
}
extension Post {
class TextFormatter {
private let options: Set
init(options: Set) {
self.options = options
}
func formatTitle(for post: Post) -> String {
return post.title.formatted(withOptions: options)
}
func formatText(for post: Post) -> String {
return post.text.formatted(withOptions: options)
}
}
}
extension Post.TextFormatter {
enum Option {
case highlightNames
case highlightLinks
}
}
Using Typealiases#
Typealiases can also be used to achieve similar nested type code in the original code (although they are not actually nested types). Although this method does not have a nested hierarchy in implementation, it reduces the length of the code and the calling looks the same as using nested types.
Here's the code:
struct Post {
typealias TextFormatter = PostTextFormatter
let id: Int
let author: User
let title: String
let text: String
}
class PostTextFormatter {
typealias Option = PostTextFormatterOption
private let options: Set
init(options: Set) {
self.options = options
}
func formatTitle(for post: Post) -> String {
return post.title.formatted(withOptions: options)
}
func formatText(for post: Post) -> String {
return post.text.formatted(withOptions: options)
}
}
enum PostTextFormatterOption {
case highlightNames
case highlightLinks
}
Conclusion#
Using nested types helps to write elegant and structured code, making the relationships between multiple types clearer - both in implementation and calling.
However, due to different implementation methods, there may be different challenges and side effects - so I think it is important to choose the appropriate implementation based on the actual situation, in order to achieve a beautiful result.
What do you think? Which of the above techniques do you prefer? Or do you have other methods? Let me know your questions and opinions on Twitter@johnsundell.
Thanks for reading! 🚀