Swift By Rahul

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

  1. Why Access Control Matters
  2. The Five Access Levels
  3. What You Can Apply Access Control To
  4. Access Levels in Detail
  5. Inheritance and Overriding Rules
  6. Access Control in Extensions
  7. Asymmetric Getter and Setter Access
  8. Testing with @testable import
  9. Common Patterns and Best Practices
  10. 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.

Without vs with access control Without Access Control View VM API Everything can reach everything With Access Control View VM API private Only public API is exposed

The Five Access Levels

From most restrictive to least restrictive:

Access LevelScopeTypical Use
privateEnclosing declaration onlyImplementation details inside a type
fileprivateSame source fileHelpers shared across types in one file
internalSame module (target) — defaultApp-internal APIs
publicAny module that imports yoursFramework APIs (read-only outside module)
openAny module; allows subclassing/overridingFramework base classes meant to be extended

Think of access levels as concentric circles. private is the smallest circle; open is the largest.

Access level scopes from private to open open Any module + subclass / override public Any module that imports yours internal (default) Same module / target fileprivate Same source file private Enclosing type Each outer ring can see everything inside it

Visibility Across Modules

When you split code into an App target and a Framework, access levels decide who can see what:

What each module can access MyFramework Module internal API public API open base class private fileprivate MyApp Module imports MyFramework ✅ Can use public & open ❌ Cannot see internal ❌ Cannot see private ✅ Can subclass open only Another App Module imports MyFramework ✅ public types ✅ open subclassing ❌ MyApp internals ❌ Framework internal

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
private access scope BankAccount.swift (same file) class BankAccount private balance deposit() withdraw() Outside caller ❌ balance ✅ methods

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 internal but more flexibility than private

Tip: Prefer private when possible. Reach for fileprivate only when multiple types in one file genuinely need access.

Comparing private, fileprivate, and internal scope private Type A member visible Scope: inside Type A + same-file extension fileprivate UserProfile.swift Type A Type B Scope: entire file Both types can share internal MyApp Module File1 File2 File3 Scope: whole module Default access level

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 public type's members are still internal by default
  • You must explicitly mark members public if 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
public type members default to internal public class Logger var level internal (default) Hidden outside module public func log() explicitly public Visible to importers Other module ❌ level Other module ✅ log()

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.

public vs open — subclassing from another module public class BaseVC public func setupUI() External Module ❌ Cannot subclass Use when inheritance is not part of your API open class BaseVC open func setupUI() External Module ✅ Can subclass & override Use for framework extension points

Inheritance and Overriding Rules

Access control interacts with inheritance in predictable ways:

  1. You cannot override with a more restrictive access level
  2. open > public > internal > fileprivate > private in terms of visibility
  3. 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:

GoalUse
Expose a type, no external subclassingpublic class
Allow external subclassing and overridingopen class
Hide a type entirely outside the moduleinternal or lower
Override access must be equal or more permissive Superclass internal func makeSound() override ✅ Subclass public override func makeSound() Rule: override access ≥ superclass access ❌ Cannot override with private if superclass is internal

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 private helpers 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.

private(set) — different read and write access var celsius private(set) Getter internal / public ✅ Anyone can read Setter private ❌ External write Controlled mutation — only the type itself can assign

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 import exposes internal members only
  • It does not expose private or fileprivate members
  • Design testable code by keeping test-critical logic at internal or by injecting dependencies
@testable import visibility MyApp Target private fileprivate internal ← exposed to tests public / open @testable import MyAppTests Target ❌ private — blocked ❌ fileprivate — blocked ✅ internal — accessible ✅ public — accessible Separate module, special import

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.

Which access level should I use? Start Only this type → private Same file only → fileprivate App / module → internal Framework API → public / open

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 file
  • internal — inside the module (default)
  • public — visible to importers; no external subclassing
  • open — 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!