Swift Imports

If you’ve been developing applications in Swift for any length of time, you’ve grown accustomed to often having one or more lines of text at the top of your file. No, I’m not talking about the Xcode generated commented-out metadata when you make a new Swift file (if you’re like me and don’t particularly care for it either, we’ve got you covered) — I’m talking about our friend the import declaration.

Let’s take a look at Swift import declarations.

We’ve all seen this many times:

import UIKit

But this also works:

import UIKit.UITableViewController

let tvc = UITableViewController()
let vc = UIViewController()
let label = UILabel()

And this…almost works:

import class UIKit.UITableViewController

let tvc = UITableViewController()
let vc = UIViewController() // Error
let label = UILabel() // Error

Why?

In these contexts, UITableVIewController in our import statement are actually referring to two different things. Let’s look at the Swift documentation.

import [module]
import [module].[submodule]
import [import kind] [module].[symbol name]

In our first example we’re just importing the UIKit module as usual — no funny business going on there. In our second example, we’re actually importing the entire UITableViewController submodule, whereas in our last example we’re explicitly importing only a class that has the symbol name of UITableViewController.

If we inspect the submodule UITableViewController we see it imports the entire umbrella header for UIKit:

import Foundation
import UIKit
import _SwiftUIKitOverlayShims

//
//  UITableViewController.h
//  UIKit
//
//  Copyright (c) 2008-2017 Apple Inc. All rights reserved.
//

// Creates a table view with the correct dimensions and autoresizing, setting the datasource and delegate to self.
// In -viewWillAppear:, it reloads the table's data if it's empty. Otherwise, it deselects all rows (with or without animation) if clearsSelectionOnViewWillAppear is YES.
// In -viewDidAppear:, it flashes the table's scroll indicators.
// Implements -setEditing:animated: to toggle the editing state of the table.

@available(iOS 2.0, *)
open class UITableViewController : UIViewController, UITableViewDelegate, UITableViewDataSource {
...

Hence why when we do not specify the import kind while importing UITableViewController, we’re actually still just importing the entirety of UIKit.

Note that there are multiple permissible import kinds available: typealias | struct | class | enum | protocol | let | var | func

When?

Although there are limited cases when you may explicitly need to specify the import kind and symbol name, it does help remove excessive symbols you may not need in the particular file you’re working in. As far as performance and binary size goes, any unused symbols are already optimized out of the final binary by the Swift compiler. If there’s no reference to it at compile time then it’s removed, meaning that importing a framework but not using particular parts of it shouldn’t have any negative implications.

Besides decluttering Xcode’s code completion, there is one scenario where it does offer a distinct advantage: modules which both define a particular type of the same name.

Consider two frameworks, FrameworkA and FrameworkB. For the sake of simplicity, they both implement a struct called Foo, but have different behavior. What happens if we import both frameworks?

import FrameworkA
import FrameworkB

func example() {
  let myFoo = Foo(baz: 3) // Using FrameworkA's implementation
  let myFooAgain = Foo(bar: "test") // Using FrameworkB's implementation

  myFoo.doThingFromFrameworkA()
  myFooAgain.doThingFromFrameworkB()
}

Both local variables are of type Foo, but come from different modules. This could be confusing…particularly if we didn’t intend on using FrameworkA’s implementation of Foo. What can we do if we only wanted to use a different part of the framework?

import struct FrameworkA.ThingA
import FrameworkB

func example2() {
  let myFoo = Foo(baz: 3) // Error: Now FrameworkA's definition of Foo is not in scope
  let myFooAgain = Foo(bar: "test") // Still Using FrameworkB's implementation

  let thing = ThingA() // But we still have access to ThingA from FrameworkA!
  myFooAgain.doThingFromFrameworkB()
}

Now there’s no longer two Foos in scope, and we’re explicitly only importing what we intend to use from FrameworkA. Nice!

Export an import?

Before we conclude our spotlight on Swift import declarations, browsing the Swift documentation led me to an interesting declaration attribute that’s currently not officially released: @_exported

According to the docs, applying this attribute to an import declaration exports the import module, submodule, or declaration from the current module. For example, consider if FrameworkA had the import declaration @_exported import FrameworkC. In our application we could then only import FrameworkA and still be able to access FrameworkC thanks to the @_exported attribute.

But considering that it’s a private Swift attribute (as denoted by that pesky underscore 😒 ), we’re probably best off not using it for the time being.