iosNudge

Explain the access control levels in Swift.

EasySwift 2 min read

TL;DR

Access control decides who can see your code. From most open to most closed: open, public, package, internal, fileprivate, private. The default is internal: visible in the same module only. Use the smallest level that works.

The six levels

LevelVisible inNotes
openAny moduleCan be subclassed and overridden outside the module. Classes only
publicAny moduleCan be used, but not subclassed or overridden outside
packageModules in the same Swift packageSwift 5.9+
internalSame moduleThe default
fileprivateSame file
privateSame declaration (and its extensions in the same file)

A module is one target: your app, a framework, or a Swift package library.

open vs public

This is the most common question.

// In module NetworkKit
open class BaseClient {
    open func headers() -> [String: String] { [:] }
    public func send() { }
}

public class FinalClient { }
// In the app module
import NetworkKit

class MyClient: BaseClient {
    override func headers() -> [String: String] { ["Auth": "x"] }  // OK, open
    // override func send() { }   // error: send is public, not open
}

// class Other: FinalClient { }  // error: public class cannot be subclassed here

public protects the library author. They can change internals without breaking subclasses they did not plan for.

private vs fileprivate

struct Account {
    private var balance = 0

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

extension Account {
    func describe() -> String { "Balance: \(balance)" }  // OK, same file
}

// In the same file, but outside Account:
func peek(_ a: Account) -> Int {
    // a.balance    // error with private, OK with fileprivate
    0
}

Since Swift 4, extensions in the same file can see private members. So private is usually enough. Use fileprivate only when another type in the same file needs access.

private(set): read anywhere, write inside

final class Cart {
    private(set) var items: [String] = []

    func add(_ item: String) { items.append(item) }
}

let cart = Cart()
cart.add("Book")
print(cart.items)      // OK
// cart.items = []     // error: setter is private

You can mix levels: public private(set) var count = 0.

package

Useful when one Swift package has several modules that need to share code, but apps that use the package should not see it.

// In module CoreModels
package struct CacheKey { package let raw: String }

// In module Networking, same package: can use CacheKey
// In the app (outside the package): cannot

Rules you should know

  • A type's members cannot be more visible than the type in practice. A public method on an internal type is still only internal.
  • A function cannot be more visible than its parameter and return types.
struct Secret { }                    // internal
// public func leak() -> Secret { }  // error: Secret is internal
  • @testable import MyApp lets tests access internal declarations.
  • Default for members of a public type is still internal. You must mark each member public.

Why it matters

  • Smaller API surface. Fewer things can break.
  • Clearer intent. Readers know what is meant for outside use.
  • Better performance. The compiler can optimize private and final code more, for example with static dispatch.

Common follow-up questions

  • Can a struct be open? No. Only classes and class members can be open, because only classes support inheritance.
  • Does @testable import give access to private? No. Only internal and above.
  • What is an access level on an import? Since Swift 6 you can write internal import Foo or private import Foo. It stops the dependency from leaking into your public API.
  • What access level should protocol members have? They take the protocol's access level. You cannot mark requirements differently.
  • Is private the same as in Objective-C? No. Objective-C has no real access control for methods. Swift checks it at compile time.