Access Control in Swift: `public`, `private`, `internal`, and More


At Pixar’s studio, not everyone has the same access. Visitors can tour the lobby and gift shop. Animators get into the workstations and render labs. Directors can enter any room — including the story vault where unreleased plots are locked away. Swift’s access control works the same way: it controls which parts of your code can see and use which other parts.

In this guide, you’ll learn Swift’s five access levels, when to use each, and how they help you write cleaner, safer APIs. We won’t cover module architecture or Swift Package Manager — those have their own dedicated posts.

What You’ll Learn

What Is Access Control?

Access control restricts which parts of your code can access specific types, properties, and methods. It’s how you enforce encapsulation — hiding internal details so that other code can only interact with your type through its intended interface.

Think of it like backstage passes at a Pixar movie premiere. The audience (external code) can only see what’s on screen — the finished movie. The production crew (same module) can access the editing room, the sound stage, and the animation files. And the director (same declaration) has access to everything, including early story drafts that nobody else should touch.

Apple Docs: Access Control — The Swift Programming Language

The Five Access Levels

Swift provides five access levels, from most restrictive to most open:

Access LevelScopeWhen to Use
privateSame declaration (type or extension in same file)Implementation details that nothing else should touch
fileprivateSame source fileWhen multiple types in the same file need to share details
internalSame module (default)Most of your app code — visible within your project
publicAny module that imports yoursLibrary APIs — usable but not subclassable outside
openAny module that imports yoursLibrary APIs — usable and subclassable/overridable outside

The default access level is internal — if you don’t write an access keyword, your code is visible throughout your module (your app or framework) but not outside it.

struct Movie {
    var title: String      // internal (default)
    var year: Int           // internal (default)
}
Both properties are internal — accessible anywhere in the same module.

Tip: You almost never need to write internal explicitly. It’s the default, and writing it adds noise without changing behavior.

private and fileprivate

private

private is the most restrictive level. A private member is only visible within the declaration where it’s defined — and any extensions of that type in the same file:

struct PixarCharacter {
    private var secretCatchphrase = "I'm a hidden gem"
    var name: String

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

    func reveal() {
        print("\(name) whispers: \(secretCatchphrase)")
    }
}

let woody = PixarCharacter(name: "Woody")
woody.reveal()
// woody.secretCatchphrase  // ❌ Error: 'secretCatchphrase' is private
Woody whispers: I'm a hidden gem

fileprivate

fileprivate opens access to everything in the same source file. This is useful when you have helper types or extensions in the same file that need to share data:

struct Animation {
    fileprivate var frameCount: Int = 0

    mutating func addFrame() {
        frameCount += 1
    }
}

extension Animation {
    func summary() -> String {
        // ✅ Works because fileprivate allows same-file access
        "Animation has \(frameCount) frames"
    }
}

var anim = Animation()
anim.addFrame()
anim.addFrame()
print(anim.summary())
Animation has 2 frames

If frameCount were private instead of fileprivate, the extension would still be able to access it only if it’s in the same file (Swift allows private access from extensions in the same file). But fileprivate also lets other types in the same file access it:

struct AnimationDebugger {
    func inspect(_ animation: Animation) {
        // ✅ Works with fileprivate — same file
        print("Frames: \(animation.frameCount)")
    }
}
Frames: 2

Note: In Swift, private members are accessible from extensions of the same type in the same file. This was changed in Swift 4 to make extensions more practical.

public vs open

When building a library or framework, you need public or open to expose APIs to other modules.

public lets external code use your type but not subclass it or override its methods:

// In a framework called PixarKit
public struct RenderSettings {
    public var quality: Int
    public init(quality: Int) {
        self.quality = quality
    }
}

open goes further — external code can subclass the class and override its methods:

// In a framework called PixarKit
open class Renderer {
    open func render() {
        print("Default rendering")
    }
}

// In the app that imports PixarKit
// class CustomRenderer: Renderer {
//     override func render() {
//         print("Custom rendering with sparkles!")
//     }
// }
Use public when you want external code to use your API.
Use open when you want external code to customize it via subclassing.

Tip: In app code (not a framework), you rarely need public or open. These matter when you’re building reusable libraries or Swift packages that other projects import.

Access Control in Practice

Here’s a practical example showing how access control protects a character’s internal state:

struct GameCharacter {
    let name: String
    private(set) var health: Int

    init(name: String, health: Int) {
        self.name = name
        self.health = health
    }

    mutating func takeDamage(_ amount: Int) {
        health = max(0, health - amount)
        print("\(name) took \(amount) damage! Health: \(health)")
    }

    mutating func heal(_ amount: Int) {
        health = min(100, health + amount)
        print("\(name) healed! Health: \(health)")
    }
}

var buzz = GameCharacter(name: "Buzz", health: 100)
buzz.takeDamage(30)
buzz.heal(10)
print(buzz.health) // ✅ Can read health
// buzz.health = 999 // ❌ Can't set — private(set)
Buzz took 30 damage! Health: 70
Buzz healed! Health: 80
80

The private(set) modifier is a common pattern: it lets external code read the property but only the type itself can write to it. The character’s health can only change through controlled methods like takeDamage() and heal() — nobody can cheat by setting health = 999 directly.

Tip: private(set) is one of the most useful access control patterns in Swift. It gives you read-only properties from the outside while keeping full control inside.

Common Mistakes

Making Everything private

Over-restricting can backfire when extensions in the same file can’t access what they need:

// ❌ Too restrictive — other types in the file can't access
struct StudioAsset {
    private var data: [String] = []
}

// A helper in the same file can't see 'data'
// struct AssetLoader {
//     func load(into asset: StudioAsset) {
//         asset.data.append("frame.png")  // ❌ Error
//     }
// }
// ✅ Use fileprivate when same-file access is needed
struct StudioAsset {
    fileprivate var data: [String] = []
}

struct AssetLoader {
    func load(into asset: inout StudioAsset) {
        asset.data.append("frame.png")  // ✅ Works
        print("Loaded \(asset.data.count) asset(s)")
    }
}
Loaded 1 asset(s)

Forgetting That internal Is the Default

// ❌ Unnecessary — internal is already the default
internal struct Movie {
    internal var title: String
    internal var year: Int
}
// ✅ Clean — no need to write internal explicitly
struct Movie {
    var title: String
    var year: Int
}

Writing internal explicitly adds visual noise without changing behavior. Save access control keywords for when you’re intentionally restricting (private, fileprivate) or expanding (public, open) access beyond the default.

What’s Next?

  • Access control restricts visibility of your code to enforce encapsulation.
  • Swift has five levels: private, fileprivate, internal (default), public, and open.
  • Use private for implementation details. Use fileprivate for same-file sharing.
  • public exposes APIs to other modules; open additionally allows subclassing.
  • private(set) is a practical pattern for read-only properties with internal write access.

With protocols defining contracts and access control protecting your APIs, you have a solid foundation for writing well-structured Swift code. Revisit Protocols in Swift and Extensions in Swift to see how these concepts work together in practice.