Swift Access Control

Access control lets you restrict which parts of your code can see and use a given declaration, hiding implementation details behind clear boundaries.

The Five Access Levels

Swift provides five access levels, from most to least restrictive: private, fileprivate, internal, public, and open. Every declaration defaults to internal unless you specify otherwise, which is why most code you write never mentions access control at all.

LevelVisible FromTypical Use
privateThe same declaration or extensions in the same fileHiding implementation details
fileprivateAnywhere in the same source fileSharing helpers across related types in one file
internal (default)Anywhere in the same moduleNormal app or library-internal code
publicAny module that imports this onePublic API that cannot be subclassed externally
openAny importing module; can be subclassed or overriddenPublic API explicitly designed for extension

Private and File-Private

private restricts a declaration to its enclosing scope (and extensions of that type in the same file). fileprivate widens that to the whole file, which is useful when two closely related types in one file need to share internals that outside code shouldn't touch.

Private Properties

class BankAccount {
    private var balance: Double = 0

    fileprivate var accountNumber: String

    init(accountNumber: String) {
        self.accountNumber = accountNumber
    }

    func deposit(_ amount: Double) {
        balance += amount
    }

    func currentBalance() -> Double {
        return balance
    }
}

let account = BankAccount(accountNumber: "AC-1001")
account.deposit(250.0)
print(account.currentBalance()) // 250.0
// account.balance would fail to compile — balance is private

Internal — the Default

If you write no access modifier at all, a declaration is internal: visible anywhere inside the same module (your app target or framework), but invisible to any other module that imports it. Most application code stays at this level.

Internal by Default

struct Product {
    var name: String     // internal by default
    var price: Double    // internal by default

    func formattedPrice() -> String {
        return String(format: "$%.2f", price)
    }
}

let item = Product(name: "Keyboard", price: 49.99)
print(item.formattedPrice()) // $49.99
// Visible anywhere inside this module, but not to code
// that imports this module from outside.

Public and Open

public exposes a declaration to other modules but prevents them from subclassing a class or overriding its members. open goes one step further, explicitly allowing external subclassing and overriding — the right choice when you're designing a framework's extension points.

Public API With an Override Point

open class Shape {
    public var name: String

    public init(name: String) {
        self.name = name
    }

    open func area() -> Double {
        return 0.0
    }
}

public class Circle: Shape {
    public var radius: Double

    public init(radius: Double) {
        self.radius = radius
        super.init(name: "Circle")
    }

    public override func area() -> Double {
        return Double.pi * radius * radius
    }
}

let circle = Circle(radius: 3.0)
print(circle.area()) // 28.274333882308138
  • Start with private and widen access only when something outside genuinely needs it
  • Use internal (the default) for anything the rest of your app needs but external packages don't
  • Reserve public and open for real library APIs meant for consumption outside your module
  • Use open only when external modules should be able to subclass or override
Note: Marking a property private(set) lets other code read it while only the type itself can write to it — a common pattern for safely exposing internal state without a full computed property.
Note: public and open only matter across module boundaries, such as a framework versus the app that imports it. Inside a single app target, public behaves almost identically to internal.

Exercise: Swift Access Control

What does the private access level restrict?