Access Specifiers in Swift: A Complete Guide
Every Swift project eventually grows beyond a single file. As your codebase expands, you need a way to decide what code is visible where — within a type, within a file, within a module, or to the outside world. That is exactly what access control (also called access specifiers or access modifiers) gives you.
Swift provides five access levels. Understanding them helps you write safer APIs, hide implementation details, and design modules that are easy to use and hard to misuse.
Table of Contents
- Why Access Control Matters
- The Five Access Levels
- What You Can Apply Access Control To
- Access Levels in Detail
- Inheritance and Overriding Rules
- Access Control in Extensions
- Asymmetric Getter and Setter Access
- Testing with
@testable import - Common Patterns and Best Practices
- Quick Reference
Why Access Control Matters
Without access control, every type, property, and method you write is potentially reachable from anywhere in your app. That leads to:
- Tight coupling — other parts of your code depend on details that should stay hidden
- Harder refactoring — changing an internal helper breaks code you did not expect to touch
- Leaky APIs — framework consumers use implementation types you never meant to expose
Access specifiers let you draw boundaries. You expose only what callers need and keep the rest private to your implementation.
The Five Access Levels
From most restrictive to least restrictive:
| Access Level | Scope | Typical Use |
|---|---|---|
private | Enclosing declaration only | Implementation details inside a type |
fileprivate | Same source file | Helpers shared across types in one file |
internal | Same module (target) — default | App-internal APIs |
public | Any module that imports yours | Framework APIs (read-only outside module) |
open | Any module; allows subclassing/overriding | Framework base classes meant to be extended |
Think of access levels as concentric circles. private is the smallest circle; open is the largest.
Visibility Across Modules
When you split code into an App target and a Framework, access levels decide who can see what:
What You Can Apply Access Control To
Access control applies to:
- Top-level types (
class,struct,enum,protocol,typealias) - Properties and constants
- Methods and subscripts
- Initializers
- Nested types
You place the keyword before the declaration:
public class NetworkClient {
private let session: URLSession
internal func fetchData() { }
fileprivate var cache: [URL: Data] = [:]
}
If you omit an access modifier, Swift uses internal by default.
Access Levels in Detail
private
private limits visibility to the enclosing declaration and any extensions of that declaration in the same file.
class BankAccount {
private var balance: Double = 0
func deposit(_ amount: Double) {
balance += amount
}
func withdraw(_ amount: Double) -> Bool {
guard balance >= amount else { return false }
balance -= amount
return true
}
}
let account = BankAccount()
// account.balance = 1000 // ❌ Error: 'balance' is inaccessible due to 'private' protection level
account.deposit(1000) // ✅ Works — balance is modified through the type's API
Use private for:
- Stored properties that should not be mutated from outside
- Helper methods used only inside the type
- Details of an algorithm callers should not depend on
fileprivate
fileprivate is visible anywhere in the same Swift source file, even across different types.
// UserProfile.swift
struct UserProfile {
fileprivate var displayName: String
}
struct ProfileFormatter {
func format(_ profile: UserProfile) -> String {
return "Hello, \(profile.displayName)" // ✅ Same file
}
}
Use fileprivate when:
- Two or more types in the same file need to share state
- You want tighter scope than
internalbut more flexibility thanprivate
Tip: Prefer private when possible. Reach for fileprivate only when multiple types in one file genuinely need access.
internal (Default)
internal is visible anywhere in the same module. A module is typically an app target, framework, or Swift package.
// No keyword needed — internal is the default
class SessionManager {
var currentUser: User?
func logout() {
currentUser = nil
}
}
Everything in your app target can use SessionManager, but code in another framework cannot unless you mark it public or open.
Use internal for:
- Most app code
- Types and methods that are shared across your app but not meant for external consumers
public
public makes a declaration visible to any module that imports yours. It is essential for Swift packages and frameworks.
public struct APIResponse<T: Decodable>: Decodable {
public let data: T
public let statusCode: Int
public init(data: T, statusCode: Int) {
self.data = data
self.statusCode = statusCode
}
}
Important rules for public:
- A
publictype's members are stillinternalby default - You must explicitly mark members
publicif you want them accessible outside the module
public class Logger {
var level: LogLevel = .info // internal — not visible outside module
public func log(_ message: String) { } // visible outside module
}
Use public for:
- Framework and library APIs
- Types you ship in a Swift package
open
open is like public, but it also allows subclassing and overriding outside your module.
open class BaseViewController: UIViewController {
open func setupUI() {
// Default layout
}
}
Another module can do:
import MyUIKitHelpers
class HomeViewController: BaseViewController {
override func setupUI() {
super.setupUI()
// Custom home screen layout
}
}
With public, subclassing and overriding outside the defining module is not allowed.
Use open sparingly:
- Base classes in frameworks designed for extension (e.g. custom view controllers, parsers)
- When you explicitly want third-party subclasses
For most framework APIs, public is enough — it exposes the type without committing to an inheritance contract.
Inheritance and Overriding Rules
Access control interacts with inheritance in predictable ways:
- You cannot override with a more restrictive access level
open>public>internal>fileprivate>privatein terms of visibility- A subclass can override a method and make it more accessible, not less
class Animal {
internal func makeSound() { print("...") }
}
class Dog: Animal {
public override func makeSound() { print("Woof!") } // ✅ More accessible
}
For framework design:
| Goal | Use |
|---|---|
| Expose a type, no external subclassing | public class |
| Allow external subclassing and overriding | open class |
| Hide a type entirely outside the module | internal or lower |
Access Control in Extensions
Extensions follow the same rules, with a few nuances:
- An extension can lower the effective access of members only within its own scope
- You can add
privatehelpers in an extension in the same file as the type
class OrderService {
func placeOrder(for product: Product) {
validate(product)
submit(product)
}
}
private extension OrderService {
func validate(_ product: Product) { /* ... */ }
func submit(_ product: Product) { /* ... */ }
}
private extension is a popular pattern for grouping implementation details cleanly.
┌─────────────────────────────────────────────────────┐
│ OrderService.swift │
│ │
│ ┌───────────────────────────────────────────────┐ │
│ │ class OrderService │ │
│ │ │ │
│ │ placeOrder() ──► public API surface │ │
│ └───────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────┐ │
│ │ private extension OrderService │ │
│ │ │ │
│ │ validate() ──► hidden implementation │ │
│ │ submit() ──► hidden implementation │ │
│ └───────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
Asymmetric Getter and Setter Access
Swift lets you give a property different access levels for reading and writing:
struct Temperature {
private(set) var celsius: Double
init(celsius: Double) {
self.celsius = celsius
}
mutating func setCelsius(_ value: Double) {
celsius = value
}
}
let temp = Temperature(celsius: 25)
print(temp.celsius) // ✅ Readable
// temp.celsius = 30 // ❌ Cannot assign — setter is private
Common combinations:
public private(set) var items: [Item] = [] // Public read, private write
internal private(set) var cache: Data? // Module read, private write
This pattern is ideal for read-only public APIs backed by mutable internal state.
Testing with @testable import
Unit tests live in a separate module (test target). By default, internal members are invisible to tests. Use @testable import:
@testable import MyApp
final class OrderServiceTests: XCTestCase {
func testPlaceOrder() {
let service = OrderService()
// Can access internal members of MyApp
}
}
Notes:
@testable importexposesinternalmembers only- It does not expose
privateorfileprivatemembers - Design testable code by keeping test-critical logic at
internalor by injecting dependencies
Common Patterns and Best Practices
1. Start Restrictive, Loosen When Needed
Default to private, then widen access only when another type genuinely needs it. It is easier to expose an API later than to hide one callers already depend on.
2. Use private(set) for Controlled State
class ViewModel {
private(set) var isLoading = false
func loadData() async {
isLoading = true
defer { isLoading = false }
// fetch...
}
}
3. Mark Framework Surface Area Explicitly
In a Swift package, be deliberate:
public protocol NetworkServiceProtocol {
func request<T: Decodable>(_ endpoint: Endpoint) async throws -> T
}
public final class NetworkService: NetworkServiceProtocol {
public init(session: URLSession = .shared) { /* ... */ }
public func request<T: Decodable>(_ endpoint: Endpoint) async throws -> T { /* ... */ }
}
4. Avoid open Unless You Mean It
open is a long-term contract. Subclasses outside your module can override behavior you may change in future releases. Prefer public plus composition or protocols when possible.
5. Keep Files Focused to Reduce fileprivate Sprawl
If you find yourself using fileprivate heavily, consider whether types belong in the same file or whether a clearer API boundary would help.
Quick Reference
// Most common app code — default internal
class UserRepository { }
// Hide implementation
private func normalizeEmail(_ email: String) -> String { }
// Share within one file
fileprivate struct InternalDTO { }
// Framework API
public struct Config {
public let baseURL: URL
public init(baseURL: URL) { self.baseURL = baseURL }
}
// Framework base class for subclassing
open class Plugin {
open func execute() { }
}
// Read-only outside, writable inside
public private(set) var version: String = "1.0.0"
Summary
Swift access specifiers help you control visibility at five levels:
private— inside the type (and same-file extensions)fileprivate— inside the source fileinternal— inside the module (default)public— visible to importers; no external subclassingopen— visible to importers; allows external subclassing and overriding
Used well, access control keeps your code modular, your APIs clean, and your refactoring safe. When in doubt, start with private or internal, expose only what is necessary, and use public / open deliberately in frameworks and packages.
Happy Swifting!