Swift Modules and Imports: Using import and @_exported

Swift uses modules to organize code into reusable units, and the import keyword is how one file gains access to declarations from another module. In this article, you will learn what a Swift module is, how normal imports work, what @_exported does, when re-exporting is useful, and what common problems appear when modules are structured poorly.

Quick answer: Use import to make another module's public declarations available in the current file. Use @_exported import only when you intentionally want one module to re-export another module so downstream files can see it automatically; it is underscored because it is not part of Swift's stable public language surface.

Difficulty: Intermediate

Helpful to know first: You'll understand this better if you know basic Swift syntax, access control like public and internal, and how frameworks or Swift packages group source files.

1. What Is a Swift Module?

A module is a single unit of code distribution in Swift. When you build an app target, framework target, or Swift package product, Swift treats that compiled unit as a module.

Common examples of modules include:

It helps to think of a module boundary as both an organization tool and a visibility boundary. This is why modules matter for architecture, API design, compile times, and dependency management.

2. Why Modules and Imports Matter

Modules are not just a packaging feature. They affect how large Swift projects stay maintainable.

You should use imports whenever a file depends on another module's API. You should not use re-exporting casually, because it can hide real dependencies and make module relationships less obvious.

3. Basic Syntax or Core Idea

The normal import form is simple: place import near the top of the file, followed by the module name.

Importing a module

In this example, the file imports Foundation so it can use types such as Date.

import Foundation

let now = Date()
print(now)

Without importing Foundation, Date would not be available in this file.

What import actually changes

Importing a module does not copy code into your file. It tells the compiler that this file may use declarations exposed by that module.

Using @_exported import

Swift also supports an underscored attribute form that re-exports another module:

@_exported import Foundation

When this appears in a module, files that import your module may gain access to Foundation symbols without writing their own import Foundation. This is convenient in some wrapper modules, but it is more fragile because @_exported is underscored and not considered stable source-level API.

4. Step-by-Step Examples

Example 1: Importing a framework in one file

This example shows the most common use: bringing a framework into scope for one file.

import Foundation

let names = ["Ana", "Ben", "Chris"]
let sortedNames = names.sorted { lhs, rhs in
    lhs.localizedCaseInsensitiveCompare(rhs) == .orderedAscending
}

print(sortedNames)

The comparison method comes from Foundation. The import makes that API available in this file.

Example 2: Importing your own module

Suppose one package module named CoreModels exposes a public type. Another module can import it and use it.

// In module CoreModels
public struct User {
    public let id: Int
    public let name: String

    public init(id: Int, name: String) {
        self.id = id
        self.name = name
    }
}

Now another module can import it:

import CoreModels

let user = User(id: 1, name: "Mina")
print(user.name)

This works because both the type and its initializer are public.

Example 3: Import does not bypass access control

This is one of the most important module rules. Even after import, non-public declarations stay hidden across modules.

// In module Helpers
struct InternalFormatter {
    func format(_ value: String) -> String {
        "*** \(value) ***"
    }
}

In another module:

import Helpers

// This will not compile because InternalFormatter is not public.
// let formatter = InternalFormatter()

The import succeeds, but the symbol is still unavailable because its access level does not allow cross-module use.

Example 4: Re-exporting a dependency with @_exported

Suppose you have a wrapper module named AppSupport that always exposes types from CoreModels.

// In module AppSupport
@_exported import CoreModels

public struct Session {
    public let currentUser: User

    public init(currentUser: User) {
        self.currentUser = currentUser
    }
}

Then a consumer may only import AppSupport:

import AppSupport

let user = User(id: 42, name: "Ravi")
let session = Session(currentUser: user)

print(session.currentUser.name)

This can make downstream code shorter, but it also hides that User really comes from another dependency.

5. Practical Use Cases

6. Common Mistakes

Mistake 1: Assuming import makes every declaration visible

Beginners often think that importing a module grants access to everything inside it. In Swift, visibility still depends on access control.

Problem: This code tries to use a type that is internal in another module, so the compiler cannot expose it across the module boundary.

// In module Utilities
struct TokenBuilder {
    func build() -> String {
        "abc123"
    }
}

// In another module
import Utilities

let builder = TokenBuilder()

Fix: Mark the type and the members you want to expose as public.

// In module Utilities
public struct TokenBuilder {
    public init() {}

    public func build() -> String {
        "abc123"
    }
}

// In another module
import Utilities

let builder = TokenBuilder()

The corrected version works because the type and initializer are now visible outside their defining module.

Mistake 2: Forgetting to import the module in each file that needs it

Imports are normally written per file. Importing a module in one file does not automatically make it available everywhere else in your target.

Problem: This file uses Date without importing Foundation, leading to a compile error such as “cannot find 'Date' in scope”.

// Missing import Foundation

let createdAt = Date()

Fix: Import the required module in the file that uses its declarations.

import Foundation

let createdAt = Date()

The corrected version works because the compiler now knows where Date is defined.

Mistake 3: Using @_exported as if it were a normal stable language feature

Because @_exported is convenient, it can be tempting to use it everywhere. That is risky, especially in public libraries or long-lived package APIs.

Problem: This module re-exports dependencies broadly, which can hide real module relationships and tie consumers to an underscored implementation detail.

@_exported import Foundation
@_exported import CoreModels
@_exported import Networking

Fix: Prefer explicit imports in consumer files, and use @_exported only in carefully chosen wrapper modules where re-exporting is intentional and documented.

// In the wrapper module, keep re-exports minimal or avoid them entirely.
import CoreModels

public struct Session {
    public let userID: Int

    public init(userID: Int) {
        self.userID = userID
    }
}

// In consumer files, be explicit about dependencies.
import CoreModels
import AppSupport

The corrected version is clearer because each file shows its actual dependencies directly.

Mistake 4: Confusing “No such module” with a Swift syntax issue

When Swift reports No such module, the problem is usually not the import keyword itself. The build system cannot find or link the module.

Problem: This code imports a module that is not added to the target, package manifest, or build configuration, so the compiler cannot resolve it.

import CoreModels

Fix: Add the dependency to the target or package correctly, then rebuild. In Swift Package Manager, that means declaring the dependency and attaching the product to the target.

// Package.swift excerpt
// products: [.library(name: "CoreModels", targets: ["CoreModels"])]
// dependencies: [...]
// target dependencies: ["CoreModels"]

The corrected setup works because the compiler can now locate and build the imported module.

7. Best Practices

Practice 1: Keep imports explicit in most application code

Explicit imports make dependencies easy to trace. This helps maintenance, code review, and refactoring.

// Less preferred when overused through re-exports
import AppSupport

// Preferred when the file directly depends on these modules
import Foundation
import CoreModels
import AppSupport

This approach makes it obvious which APIs the file truly depends on.

Practice 2: Design small public surfaces across module boundaries

The more symbols you expose publicly, the harder your module is to evolve safely. Keep helper types internal unless they are part of the intended API.

// Less preferred: everything exposed
public struct JSONParser {}
public struct RawRequestState {}
public struct InternalRetryTracker {}

// Preferred: only the real API is public
public struct JSONParser {}
struct InternalRetryTracker {}

This keeps module boundaries cleaner and reduces accidental coupling.

Practice 3: Use @_exported only for intentional facade modules

If you decide to re-export, do it because the module is deliberately acting as a facade or platform layer, not just to save typing.

// Reasonable: one carefully documented facade module
@_exported import CoreModels

public enum AppAPI {
    public static let version = "1.0"
}

This is much safer than scattering many re-exports across unrelated modules.

8. Limitations and Edge Cases

9. Practical Mini Project

Here is a small example of how modules and imports can be organized in a realistic package layout. Imagine three modules:

The code below shows a simple version of that structure.

// Module: CoreModels
public struct User {
    public let id: Int
    public let name: String

    public init(id: Int, name: String) {
        self.id = id
        self.name = name
    }
}

// Module: UserFeatures
import CoreModels

public struct UserGreeter {
    public init() {}

    public func greeting(for user: User) -> String {
        "Hello, \(user.name)!"
    }
}

// Module: AppFacade
@_exported import CoreModels
import UserFeatures

public struct WelcomeService {
    private let greeter = UserGreeter()

    public init() {}

    public func message(for user: User) -> String {
        greeter.greeting(for: user)
    }
}

// Consumer module
import AppFacade

let user = User(id: 7, name: "Leah")
let service = WelcomeService()

print(service.message(for: user))

This mini project shows both normal imports and a deliberate re-export. In a real project, you would document clearly that AppFacade re-exports CoreModels, because consumers may otherwise not realize where User originates.

10. Key Points

11. Practice Exercise

Try this exercise to reinforce the difference between importing and re-exporting.

Expected output: A printed product description such as Featured: Keyboard.

Hint: Make sure the struct, initializer, and used properties are public, or the second module will not be able to expose them across module boundaries.

// Module: SharedTypes
public struct Product {
    public let name: String

    public init(name: String) {
        self.name = name
    }
}

// Module: StoreKitLite
@_exported import SharedTypes

public struct StoreFront {
    public init() {}

    public func featuredText(for product: Product) -> String {
        "Featured: \(product.name)"
    }
}

// Consumer
import StoreKitLite

let product = Product(name: "Keyboard")
let store = StoreFront()

print(store.featuredText(for: product))

12. Final Summary

Swift modules are the language's core tool for organizing and sharing code across targets, packages, and frameworks. The import keyword tells the compiler which external module a file depends on, while access control decides which declarations are actually visible. That distinction is essential: importing a module gives you access only to the API that module intentionally exposes.

You also saw that @_exported import can re-export a dependency through another module. That can be useful in a facade or wrapper layer, but it should be used carefully because it hides direct dependencies and relies on an underscored feature. In most codebases, explicit imports are the safer default. As a next step, study Swift access control and Swift Package Manager target dependencies, because both are tightly connected to how modules behave in real projects.