Polyglot
Languages Swift scope
Swift § scope

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 import needed 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
ModifierVisibility
openAnywhere; admits subclassing/override across modules
publicAnywhere; subclassing/override only within the declaring module
internalThe declaring module (default)
fileprivateThe declaring file
privateThe 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 public for the module’s API.
  • Use open sparingly — only when subclassing across modules is intended.
  • Use private for implementation details.
  • Use fileprivate when 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 let by default.
  • Use var only when reassignment is required.
  • The compiler warns on var that 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 public for the module’s API.
  • Use private for implementation details.
  • Use fileprivate sparingly.
  • Use open only when external subclassing is genuinely intended.
  • Use let over var by default.
  • Use empty enums (enum Math { }) for namespaces.
  • Use extensions for protocol conformances and grouping.
  • Use @main for executable entry points.
  • Use SPM for module organisation.
  • Use @testable import in 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.