Access control determines which code may name a declaration. A small app may initially place everything in one target, but even there private state helps protect invariants. A library needs a more deliberate boundary: consumers should see the supported interface rather than every helper used to implement it.
Start with the structs lesson. This reference adds rules about who can construct, inspect and mutate a value. Access control is a compiler-enforced source-code boundary; it does not authenticate a user, encrypt a secret, or authorize a server request. Those application requirements remain separate.
Files, modules and packages are different boundaries
A module is a unit imported by other code, often corresponding to an app or framework target. A source file is one file within that module. A package groups modules through the build system. Folder names alone do not turn several files into separate modules.
Internal is the normal default and allows access within the defining module. Private restricts a member to its enclosing declaration and same-file extensions of that declaration. Fileprivate makes a declaration available across its source file. Choose the smallest boundary that actually supports the collaboration between your declarations.
Package access, available since Swift 5.9, supports sharing declarations among modules in the same configured package without exposing them to unrelated packages. Public exposes a supported API to importing modules. Open adds external subclassing and overriding permissions for applicable class declarations and members. Structs do not become open because value types do not participate in class inheritance.
A public type does not publish every member
Members of a public type are generally internal by default. Mark the actual initializer, properties and methods public when external callers need them. A synthesized memberwise initializer for a public struct is not automatically a public construction API. Write an explicit initializer when publishing a library value.
A public signature cannot depend on a less-visible type that its callers cannot name. Parameter types, result types and related constraints participate in the access boundary. If a private helper leaks through a public return type, redesign the return value or deliberately publish the appropriate type.
Publishing a member creates a maintenance promise. Consumers may depend on its name, argument labels and behavior. Keep internal storage details private so you can change them without requiring callers to understand a new representation. An API that returns a useful value is often more stable than one exposing an editable implementation container.
Check the interface in a small client target that imports the library normally. A test using testable import can accidentally make an incomplete public surface look usable because it sees internal declarations. The ordinary client reveals missing public initializers and inaccessible result types. Keep this check focused on the supported consumer workflow rather than publishing helpers solely to make the demonstration compile.
Private, testing and inheritance choices
Same-file extensions of a type can access that type's private members, which lets you organize implementation without exposing it to every declaration in the file. Fileprivate is appropriate when genuinely separate declarations in the same file must cooperate; it should not become a default merely to silence an access error.
A test target is normally another module. Testable import can make internal declarations accessible when the product module is compiled with testing enabled. It does not make private members a public test surface. Prefer testing observable behavior through a meaningful interface instead of widening implementation details solely to assert their current representation.
For classes, public supports use from another module but does not generally grant external subclassing and overriding. Open signals that extensibility has been considered. Designing an externally subclassable class requires attention to initialization, invariants and overrides. Final can communicate that inheritance is intentionally closed even when use of the class is public.
Official references and review
Read the Swift access-control guide and its official source. Review a library from an importing client's perspective: can it construct the value, read needed information and perform supported actions without reaching into storage? Every public member should have a concrete consumer and a documented behavioral contract.
Swift Access Control and Module Boundaries Examples
Each example shows the code, the output it produces and what each part does.
Read a count without freely changing it
Private(set) restricts the setter while retaining the property's ordinary getter visibility. Callers use remove instead of assigning an arbitrary negative count. The initializer's precondition states a programmer contract; a user-input boundary should validate first or use a failable or throwing initializer for expected invalid data.
struct StockCounter {
private(set) var count: Int
init(count: Int) {
precondition(count >= 0)
self.count = count
}
mutating func remove(_ amount: Int) -> Bool {
guard amount > 0, amount <= count else { return false }
count -= amount
return true
}
}
var stock = StockCounter(count: 8)
print(stock.remove(3))
print(stock.count)
print(stock.remove(9))
print(stock.count)true
5
false
5Swift Access Control and Module Boundaries in Real Projects
Worked exercise: Publish a validated identifier
Create a public library-facing identifier with an explicit public initializer. Reject blank text using a failable initializer, store a normalized value, and allow reading without mutation. The example can run as one Swift source file; the public declarations become meaningful when moved into a library module.
public struct ProductCode {
public let value: String
public init?(_ raw: String) {
let cleaned = raw.trimmingCharacters(in: .whitespacesAndNewlines)
guard !cleaned.isEmpty else { return nil }
value = cleaned
}
}
import Foundation
if let code = ProductCode(" PEN ") {
print(code.value)
}
print(ProductCode(" ") == nil)PEN
trueFoundation provides the trimming API. Only the supported construction and read interface are public. A two-module test is needed to verify the exported API in its real packaging; running one file proves its behavior but does not itself exercise an importing client's visibility. Add business-specific code restrictions deliberately rather than infer them from nonblank text.
Swift Access Control and Module Boundaries FAQ
What is the difference between private and fileprivate?
Private confines a member to its enclosing declaration and extensions of that declaration in the same file. Fileprivate permits access from other declarations throughout the defining file. That difference matters when several types share one file. Prefer private when only the owning type needs the detail, and expand to fileprivate only for an intentional collaboration. Neither modifier establishes a runtime permission boundary between users of the application.
Why cannot another module call my public struct initializer?
The struct's visibility and its members' visibility are separate choices. A public struct does not automatically expose its synthesized memberwise initializer as public, and ordinary members usually remain internal unless marked otherwise. Write an explicit public initializer that expresses the intended construction contract. This also gives you a stable place for validation and argument labels rather than publishing whichever stored-property layout happens to exist today.
Should I use open instead of public everywhere?
No. Open applies to class extensibility across module boundaries and is a stronger design commitment. An external subclass can override supported behavior, so initialization and invariants need an extension-safe design. Most value types and many classes need only a public usable interface. Decide whether external inheritance is a supported feature before granting it. Using the broadest access automatically makes future changes harder without giving ordinary callers a clearer API.
Does testable import expose private implementation details?
It enables access to internal declarations from a test module when testing support is enabled for the product module. It does not erase all visibility rules or turn private members into an exported API. Tests should normally assert behavior that matters to consumers. If an internal helper represents an important independently testable operation, an internal boundary may be appropriate; avoid making every private field internal merely to mirror the implementation in tests.
Swift Access Control and Module Boundaries Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
A library should expose a struct's construction to another module. What should its author provide?
Swift Access Control and Module Boundaries Quiz
2 questions to check what you learned.
What is the usual default access level for Swift declarations?
- internal
- public
- open
What does private(set) change on an otherwise internal property?
- It makes every method public.
- It restricts writes while leaving the getter's normal visibility available.
- It encrypts the stored value.