Scope and modules
Swift’s organisational unit is the module — a unit of code distribution corresponding to a framework or executable target. Within a module, types and functions are file-scoped by default but visible to all files in the same module. Across modules, access control modifiers (open, public, internal, fileprivate, private) determine visibility. The conventional Swift project uses the Swift Package Manager (SPM) for module structure, with Package.swift declaring targets and dependencies. The combination — module-as-distribution-unit, the five access modifiers, file-scope-with-module-visibility, the SPM organisation — is the substance of Swift’s organisational model.
Modules
A module is a unit of code distribution — a framework, a Swift Package target, an Xcode framework, or an executable. Within a module, files share visibility (subject to access control); across modules, only public and open symbols are visible.
import Foundation // import a system module
import MyLibrary // import a third-party module
import struct MyLibrary.Person // import a specific symbol
import class MyLibrary.Network.Client // import nested symbol
The conventional contemporary discipline:
- One module per Swift Package target.
- Cross-module imports via
import. - Within a module — no
importneeded for code in the same target.
Access control
Five modifiers, from most to least permissive:
open class Foo { } // outside-module subclassing/override admitted
public class Foo { } // outside-module access only
internal class Foo { } // module-only (default)
fileprivate class Foo { } // file-only
private class Foo { } // type-and-extension only
| Modifier | Visibility |
|---|---|
open | Anywhere; admits subclassing/override across modules |
public | Anywhere; subclassing/override only within the declaring module |
internal | The declaring module (default) |
fileprivate | The declaring file |
private | The declaring type and its extensions in the same file |
public struct API {
public let host: String // visible to consumers
internal var connection: Connection // visible within module
private var cache: [String: Data] = [:] // private to API and its extensions
public init(host: String) {
self.host = host
self.connection = Connection(host: host)
}
}
The conventional discipline:
- Default to
internal— most code. - Use
publicfor the module’s API. - Use
opensparingly — only when subclassing across modules is intended. - Use
privatefor implementation details. - Use
fileprivatewhen multiple types in the same file need shared access.
File scope vs module scope
Within a module, files share visibility (modulo private/fileprivate):
// File: User.swift
struct User {
let name: String
}
// File: Service.swift
struct Service {
func process(user: User) { // OK; same module
// ...
}
}
No import is needed for code in the same module.
For cross-file private, use fileprivate:
// File: Helpers.swift
fileprivate func helper() { } // visible only in Helpers.swift
private func reallyPrivate() { } // (in this file, equivalent to fileprivate)
Variable declarations and scoping
let and var declarations are block-scoped:
func example() {
let x = 5
if x > 0 {
let y = 10
// x and y visible here
}
// x visible; y NOT visible here
}
Swift admits variable shadowing:
let x = 5
do {
let x = 10 // shadows outer x
print(x) // 10
}
print(x) // 5
The mechanism is conventional in if let, guard let, and similar binding contexts.
Top-level declarations
In a Swift script (a .swift file run directly), top-level statements are admitted:
// File: hello.swift
let greeting = "Hello"
print(greeting)
In a module (library or executable target), top-level declarations are admitted but executable statements are confined to functions or main entry points.
main entry point
Three forms:
// In a single-file script:
print("hello") // top-level statement runs
// In a multi-file executable, the @main attribute:
@main
struct App {
static func main() {
print("hello")
}
}
// Or with async entry point (Swift 5.5+):
@main
struct App {
static func main() async throws {
let data = try await fetch()
print(data)
}
}
The @main attribute marks the entry point; conventional in modern Swift packages.
Constants vs variables
let maxRetries = 3 // constant
var attempts = 0 // variable
attempts += 1 // OK
maxRetries = 5 // ERROR: cannot assign to let
The conventional discipline:
- Use
letby default. - Use
varonly when reassignment is required. - The compiler warns on
varthat is never reassigned.
Nested types
Types may be nested inside other types:
struct API {
struct Request {
let url: URL
let method: String
}
struct Response {
let status: Int
let body: Data
}
enum Error: Swift.Error {
case network
case timeout
case invalid(reason: String)
}
}
let req = API.Request(url: ..., method: "GET")
The mechanism admits substantial namespace organisation — API.Request is distinct from a top-level Request.
Extensions
The extension keyword admits adding methods, properties (computed only), nested types, initializers, and protocol conformances to existing types — including types from other modules:
extension String {
func shout() -> String {
uppercased() + "!"
}
}
print("hello".shout()) // "HELLO!"
extension Int {
var isEven: Bool {
self.isMultiple(of: 2)
}
}
print(5.isEven) // false
Extensions admit substantial reuse and feature addition; the conventional Swift discipline uses extensions for protocol conformances (admits substantial separation of concerns):
struct Person {
let name: String
let age: Int
}
extension Person: Equatable { } // synthesised conformance
extension Person: Hashable { } // synthesised conformance
extension Person: CustomStringConvertible {
var description: String { "\(name) (\(age))" }
}
Constants in modules
Module-level constants admit substantial organisation:
// File: Constants.swift
public enum AppConstants {
public static let maxRetries = 3
public static let timeout: TimeInterval = 30
public static let apiVersion = "v1"
}
// Usage:
let retries = AppConstants.maxRetries
The empty-enum pattern (no cases) admits a namespace — a struct-like container that cannot be instantiated.
public enum Math {
public static let pi: Double = 3.14159
public static func square(_ n: Double) -> Double { n * n }
}
print(Math.pi)
print(Math.square(5)) // 25.0
The pattern is the conventional Swift form for “static functions and constants in a namespace”.
import variants
import Foundation // entire module
import struct Foundation.Date // single type
import func Foundation.NSLog // single function
import enum MyLib.Status // single enum
import class UIKit.UIView // class
import protocol Foundation.Codable // protocol
@_exported import OtherModule // re-export to consumers
@testable import MyApp // admit access to internal symbols (in tests)
The @testable import admits access to internal symbols in test targets — conventional for unit testing.
Common patterns
Module organisation (SPM)
MyApp/
├── Package.swift
├── Sources/
│ ├── MyApp/ # main module
│ │ ├── App.swift
│ │ ├── User.swift
│ │ └── Service.swift
│ └── MyAppCore/ # shared core module
│ ├── Models.swift
│ └── Helpers.swift
└── Tests/
└── MyAppTests/
└── ServiceTests.swift
// Package.swift
let package = Package(
name: "MyApp",
targets: [
.executableTarget(name: "MyApp", dependencies: ["MyAppCore"]),
.target(name: "MyAppCore"),
.testTarget(name: "MyAppTests", dependencies: ["MyApp"]),
]
)
Public API surface
// File: PublicAPI.swift
public struct Client {
private let connection: Connection // hidden implementation
public let configuration: Configuration
public init(configuration: Configuration) {
self.configuration = configuration
self.connection = Connection(config: configuration)
}
public func fetch(_ request: Request) async throws -> Response {
try await connection.send(request)
}
}
The conventional discipline keeps the public API small; implementation details are internal or private.
Constants namespace
public enum AppConfig {
public static let bundleID = "com.example.app"
public static let timeout: TimeInterval = 30
public enum URLs {
public static let api = URL(string: "https://api.example.com")!
public static let docs = URL(string: "https://docs.example.com")!
}
}
// Usage:
let url = AppConfig.URLs.api
Extension for protocol conformance
// In one file:
struct User {
let id: Int
let name: String
}
// In another file (or same):
extension User: Codable { } // synthesised
extension User: Equatable { }
extension User: Hashable { }
extension User: CustomStringConvertible {
var description: String { "User(id: \(id), name: \(name))" }
}
The pattern admits substantial separation — the type’s intrinsic structure is in one declaration; conformances and ancillary methods in extensions.
Restricted-visibility helpers
struct Service {
private var cache: [String: Data] = [:]
public func fetch(_ key: String) async throws -> Data {
if let cached = cache[key] {
return cached
}
let data = try await fetchFromNetwork(key)
return data
}
private func fetchFromNetwork(_ key: String) async throws -> Data {
// ...
}
}
@_exported re-exports
// In a wrapper module:
@_exported import Foundation
// Consumers of the wrapper module get Foundation symbols directly.
The mechanism admits substantial library composition; rare in application code.
Subscripts and computed properties
public struct Settings {
private var values: [String: Any] = [:]
public subscript(key: String) -> Any? {
get { values[key] }
set { values[key] = newValue }
}
public var count: Int { values.count }
}
let s = Settings()
s["debug"] = true // subscript
let n = s.count // computed property
The mechanism admits substantial encapsulation — internal state is hidden; the public API uses subscripts and computed properties.
A note on the conventional discipline
The contemporary Swift scope advice:
- Use
internal(the default) for routine declarations. - Use
publicfor the module’s API. - Use
privatefor implementation details. - Use
fileprivatesparingly. - Use
openonly when external subclassing is genuinely intended. - Use
letovervarby default. - Use empty enums (
enum Math { }) for namespaces. - Use extensions for protocol conformances and grouping.
- Use
@mainfor executable entry points. - Use SPM for module organisation.
- Use
@testable importin unit tests.
The combination — modules as units of distribution, the five access control modifiers, file-scope-with-module-visibility, the extension mechanism, the SPM organisation — is the substance of Swift’s organisational model. The discipline produces clear, well-encapsulated code with substantial flexibility for module-level reuse.