The Power of Namespacing in Swift
Namespacing is a powerful feature that improves code structure. Although being limited in Swift, it can be compensated by the use of nested types. Let’s take a look at how namespacing works in Swift by default and how it can be simulated.
Defining Namespace
Namespace is a named region of program used to group variable, types and methods. Namespacing has following benefits:
- Allows to improve code structure by organizing the elements, which otherwise would have global scope, into the local scopes.
- Prevents name collision.
- Provides encapsulation.
What about Swift? Namespacing is implicit in Swift, meaning that all types, variables (etc) are automatically scoped by the module, which, in its turn, corresponds to Xcode target.
Most of the time no module prefixes are needed to access an externally scoped type:
let zeroOrOne = Int.random(in: 0...1)
print(zeroOrOne) // Prints 0 or 1
Although Int
is declared outside, the scope is figured automatically.
What if names conflict? In case of name collision, local types shadow the external ones:
struct Int {}
let zeroOrOne = Int.random(in: 0...1) // error: type 'Int' has no member 'random'
Local type Int
does not declare method random(in:)
, hence the error. To resolve the ambiguity, the namespace must be explicitly specified. Swift
is the namespace for all foundation types and primitives, including Int
[1]. Article_Namespacing
is the namespace of current Xcode target:
struct Int {}
let zeroOrOne = Swift.Int.random(in: 0...1)
let myInt = Article_Namespacing.Int.init()
What if external names conflict? Another possible case is collision of names from two frameworks. Say, FrameworkA
and FrameworkB
both declare their own Int
types, as depicted below:
The ambiguity cannot be resolved automatically:
import FrameworkA
import FrameworkB
print(Int.init()) // Oops, error: Ambiguous use of 'init()'
It is addressed by adding namespaces:
import FrameworkA
import FrameworkB
print(FrameworkA.Int.init()) // Prints: FrameworkA
print(FrameworkB.Int.init()) // Prints: FrameworkB
Import statement has multiple lesser-known traits, which are worth to be discussed.
Import Statement Grammar
Import by sub-module. Modules have hierarchial structure and could be composed of sub-modules [2]. It is possible to limit imported namespace to sub-modules:
import UIKit.NSAttributedString
func foo() -> UIView { // All good
return UIView()
}
Wonder why UIView
is still accessible? UIKit.NSAttributedString
imports the entire UIKit
, and additionally Foundation
.
Import by symbol. Only the imported symbol (and not the module that declares it) is made available in the current scope:
import class UIKit.NSAttributedString
func foo() -> UIView { // error: Use of undeclared type 'UIView'
return UIView()
}
Note the class
keyword here; other possible options as well as full import statement grammar is available at swift.org.
Namespacing Techniques
The implicit per-module namespacing is often not enough to express complex code structures. The solution is to create pseudo-namespaces by means of nested enum
s.
Why enum? Unlike structs, enums do not have synthesized initializers; unlike classes they do not allow for subclassing, which makes them a perfect candidate to simulate a namespace. Let’s see the practical examples.
Better-organized constants. Different ways to specify constants exist: global variables, properties, config files. Namespace groups constants in a readable, understandable and consistent way, without polluting outer scope. The below example groups constants of a view controller into a namespace:
class ItemListViewController {
...
}
extension ItemListViewController {
enum Constants {
static let itemsPerPage = 7
static let headerHeight: CGFloat = 60
}
}
How the constants will be named if put into global scope? I guess, those are close enough:
let itemListViewControllerItemsPerPage = 7
let itemListViewControllerHeaderHeight: CGFloat = 60
The names look identical, are difficult to read and error-prone to type. No more cumbersome names. Compare with:
ItemListViewController.Constants.itemsPerPage
ItemListViewController.Constants.headerHeight
Factories and factory methods. The creation of objects often contains complex mapping, validations, special cases handling. Namespaced factories and factory methods provide a handy way of keeping creation and mapping logic close the the original type, without polluting the external scope:
struct Item {
...
}
extension Item {
enum Factory {
static func make(from anotherItem: AnotherItem) -> Item {
// Complex computations to map AnotherItem into Item
return Item(...)
}
}
}
// Usage:
let anotherItem = AnotherItem()
let item = Item.Factory.make(from: anotherItem)
Grouping by usage area. Network layer often needs specialized models for requests and responses, which are not used anywhere else, hence are good candidates to be grouped into a namespace:
enum API {
enum Request {
struct UpdateItem {
let id: Int
let title: String
let description: String
}
}
enum Response {
struct ItemList {
let items: [Item]
let page: Int
let pageSize: Int
}
}
}
Such code is self-documented; global scope is not polluted with Request
name, since it is ambiguous without a context.
Summary
The importance of good code structure is difficult to overestimate. Namespacing improves code structure by grouping relevant elements into local scopes and makes code self-documented.
Swift has limited built-in support for namespacing, which can be compensated by the use of nested types as pseudo-namespaces.
The article on Swift Code Style might be of particular interest if looking for more ways to improve code quality.
Thanks for reading!
If you enjoyed this post, be sure to follow me on Twitter to keep up with the new content. There I write daily on iOS development, programming, and Swift.