/ SWIFT

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 Power of Namespacing in Swift

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 enums.

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.