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