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.

  • A module groups related types, functions, protocols, and extensions.
  • Each Swift file belongs to exactly one module at compile time.
  • Code in one module cannot automatically see code from another module.
  • You use import to access declarations from a different module.
  • Access control matters: importing a module does not override rules like private, internal, or public.

Common examples of modules include:

  • The Swift standard library, available automatically.
  • Apple frameworks such as Foundation.
  • Your own framework targets.
  • Swift package modules such as Networking or CoreModels.

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.

  • Separation of concerns: networking code, business logic, and UI-related code can live in different modules.
  • Safer APIs: only declarations marked with appropriate access levels cross module boundaries.
  • Reuse: one framework or package can be shared across multiple apps or targets.
  • Cleaner dependencies: imports make it clear which external code a file depends on.
  • Scalability: module boundaries help teams reason about large codebases.

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

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.

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

  • If a type is public or otherwise visible across the module boundary, you may use it.
  • If a type is internal to that other module, importing does not make it visible.
  • Import is file-based in practice: one file importing a module does not automatically import it for every other file. One exception is extension members, such as the String methods Foundation adds: once any file in a module imports Foundation, those members are visible in the module's other files unless the MemberImportVisibility upcoming feature (SE-0444, Swift 6.1+) is enabled.

Using @_exported import

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

Swift
@_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.

Swift
import Foundation

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

print(sortedNames)
Output✓ Tested · Swift 6.4
["Ana", "Ben", "Chris"]

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.

Swift
// 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:

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

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

In another module:

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

Swift
// 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:

Swift
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

  • Splitting a large app into feature modules such as authentication, networking, persistence, and shared models.
  • Creating a reusable framework with a clear public API for multiple apps.
  • Building a facade module that wraps lower-level modules behind a simpler interface.
  • Using @_exported in an internal platform layer where many consumers always need the same shared dependency.
  • Keeping test helpers in a separate module so production targets are not forced to depend on testing utilities.

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.

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

// In another module
import Utilities

let builder = TokenBuilder()
Compiler output✓ Tested · Swift 6.4
error: no such module 'Utilities'

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

Swift
// 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”.

Swift
// Missing import Foundation

let createdAt = Date()

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

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

Swift
@_exported import Foundation
@_exported import CoreModels
@_exported import Networking
Compiler output✓ Tested · Swift 6.4
error: no such module 'CoreModels'

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

Swift
// 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.

Swift
import CoreModels
Compiler output✓ Tested · Swift 6.4
error: no such module '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.

Swift
// 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.

Swift
// 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.

Swift
// 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.

Swift
// 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

  • @_exported is underscored, which means it is not treated like a stable public language feature in the same way as ordinary Swift syntax.
  • Importing a module does not expose declarations that remain internal, fileprivate, or private.
  • If two imported modules expose the same type name, you may need module qualification to avoid ambiguity.
  • A file can compile with fewer explicit imports when re-exports are present, but that convenience can make real dependencies harder to see.
  • No such module errors usually come from package, target, or build settings problems rather than Swift syntax mistakes.
  • Some APIs seem available automatically because of umbrella imports or surrounding project setup, but relying on that can make code less portable to other targets or packages.

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:

  • CoreModels defines shared data types.
  • UserFeatures uses those data types.
  • AppFacade re-exports CoreModels and exposes higher-level functionality.

The code below shows a simple version of that structure.

Swift
// 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

  • A Swift module is a compiled unit of code such as a framework, app target, or package module.
  • Use import to access another module's visible declarations in a file.
  • Importing a module does not bypass access control rules.
  • @_exported import re-exports a module so downstream imports may see it automatically.
  • @_exported is convenient but should be used carefully because it is underscored and can hide dependencies.
  • Errors like No such module usually indicate dependency or build configuration problems.

11. Practice Exercise

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

  • Create a module named SharedTypes with a public Product struct.
  • Create a second module named StoreKitLite that uses Product.
  • First, make the consumer import both modules explicitly.
  • Then, change StoreKitLite to re-export SharedTypes with @_exported.
  • Observe how the consumer code changes.

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.

Show solution
Swift
// 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))

Where AI Coding Assistants Get This Wrong

Patterns AI tools commonly produce for this topic, and what to use instead.

Often suggested: Uses public import CoreModels in a facade module and expects clients that write import AppFacade to see User automatically.

Instead: public import only allows CoreModels types to appear in the facade's public API. Clients still need their own import CoreModels unless the facade uses @_exported import (an underscored feature).

Tested with Swift 6.4: a client of a facade that uses public import gets "cannot find 'User' in scope", and the same client compiles once the facade uses @_exported import.

Often suggested: Recommends @_implementationOnly import to hide a dependency from clients.

Instead: Use internal import Dependency (SE-0409, Swift 6.0+). The compiler then rejects any use of the dependency's types in public or @inlinable declarations.

@_implementationOnly is deprecated and warns in Swift 6.4, while internal import is the supported, checked replacement.

Often suggested: Claims the Swift 6 language mode makes plain import internal by default, so migrating will break every public API that mentions an imported type.

Instead: In Swift 6 mode a bare import is still public. Imports become internal by default only with -enable-upcoming-feature InternalImportsByDefault (.enableUpcomingFeature("InternalImportsByDefault")), and with that on you mark the ones you need as public import.

Tested with -swift-version 6: a public function that returns an imported type compiles with a bare import and fails only when the upcoming feature is enabled.

Check Your Understanding

Pick an answer for each question, then check your score.

1. What is a Swift module?
Show answer

Answer: A compiled unit of code, such as an app target, framework, or package module. All files in a module share one namespace; other modules reach them with import.

2. After import MyLibrary, can you use its internal declarations?
Show answer

Answer: No, only what the module makes public or open. Importing doesn't bypass access control. (Unit tests can use @testable import to reach internal code.)

3. What does @_exported import Foundation do inside a module?
Show answer

Answer: Re-exports Foundation, so code that imports your module sees Foundation too. It's convenient but underscored (unofficial), and it can hide where a dependency really comes from.

4. What does the error No such module 'X' usually mean?
Show answer

Answer: A dependency or build setup problem, such as the package not being added to the target. Check that the package or framework is linked to the target that contains the import.

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.