Skip to main content

Swift Access Control Basics

Access control is a crucial concept in object-oriented programming that allows you to restrict access to parts of your code from other parts of your codebase. In Swift, access control helps you hide implementation details of your code and specify a preferred interface through which your code can be accessed and used.

Introduction to Access Control

Access control in Swift defines the visibility and accessibility of classes, structures, enumerations, properties, methods, initializers, and subscripts. By controlling what code can access what parts, you create clear boundaries between different parts of your code, which leads to:

  • Better encapsulation
  • Lower coupling
  • More maintainable code
  • Clearer API boundaries

Let's dive into how Swift implements access control and how you can use it to write better, more maintainable code.

Swift Access Levels

Swift provides five different access levels:

  1. Open - The least restrictive access level
  2. Public - Similar to open, but with some restrictions for subclassing and overriding
  3. Internal - The default access level
  4. File-private - Restricts access to within the current source file
  5. Private - The most restrictive access level, limits access to within the declaring entity

Let's examine each access level in more detail.

Internal Access (Default)

If you don't specify an access level, Swift uses internal by default. This means the entity can be accessed from any file within the same module (app or framework):

swift
class SomeInternalClass {       // implicitly internal
var someInternalProperty = 0 // implicitly internal
func someInternalMethod() {} // implicitly internal
}

Private Access

The private access level restricts the use of an entity to the enclosing declaration:

swift
class SomeClass {
private var privateProperty = 0

func accessPrivateProperty() {
// Can access privateProperty here
print(privateProperty)
}
}

let instance = SomeClass()
// instance.privateProperty = 10 // Error: privateProperty is inaccessible
instance.accessPrivateProperty() // This works!

File-private Access

The fileprivate access level restricts the use of an entity to its defining source file:

swift
// In File: UserModel.swift
fileprivate class DatabaseManager {
fileprivate func connect() {
print("Connected to database")
}
}

class UserModel {
func saveUser() {
let db = DatabaseManager()
db.connect() // This works because it's in the same file
}
}

// In another file, DatabaseManager would be inaccessible

Public and Open Access

The public access level allows entities to be used in any source file from their defining module, and also in a source file from another module that imports the defining module:

swift
// In a framework called MyFramework
public class NetworkManager {
public var timeout = 30

public func fetchData() {
print("Fetching data...")
}
}

// In an app that imports MyFramework
let network = NetworkManager()
network.timeout = 60
network.fetchData()

The open access level is similar to public, but it specifically allows classes and class members to be subclassed and overridden outside of their defining module:

swift
// In a framework called MyFramework
open class BaseViewController {
open func configure() {
print("Default configuration")
}
}

// In an app that imports MyFramework
class LoginViewController: BaseViewController {
override func configure() {
super.configure()
print("Login-specific configuration")
}
}

Access Control Syntax

To specify an access level, place the access level modifier before the entity's declaration:

swift
public class PublicClass {}
internal class InternalClass {}
fileprivate class FilePrivateClass {}
private class PrivateClass {}

public var publicVariable = 0
internal let internalConstant = 0
fileprivate func filePrivateFunction() {}
private func privateFunction() {}

Access Control for Properties

You can give a property's getter and setter different access levels. This is useful when you want a property to be readable publicly but only writable within your own code:

swift
class AccountSettings {
// Anyone can read the username, but only the class can modify it
private(set) public var username: String = "guest"

func login(as newUsername: String) {
username = newUsername
}
}

let settings = AccountSettings()
print(settings.username) // "guest" - Reading is allowed
// settings.username = "admin" // Error: Cannot assign to property - setter is private
settings.login(as: "admin") // This works because it uses the internal method
print(settings.username) // "admin"

Access Control for Initializers

Initializers can have their own access levels:

swift
class Database {
public init() {
// This initializer can be called from anywhere
print("Creating a database with default settings")
}

fileprivate init(connectionString: String) {
// This initializer can only be called from within the same file
print("Creating a database with custom connection string")
}
}

// In the same file:
let customDB = Database(connectionString: "mysql://localhost:3306")

// In a different file:
let defaultDB = Database() // This works
// let customDB = Database(connectionString: "...") // Error: This initializer is fileprivate

Real-World Example: Building a Library

Let's create a more complex example that demonstrates how access control helps in building maintainable code. We'll create a simple library management system:

swift
// Library.swift
public class Library {
// Public API
public var name: String
public private(set) var books: [Book]

// Implementation details
private var databasePath: String
private let maxBooksPerPerson = 5

public init(name: String) {
self.name = name
self.books = []
self.databasePath = "/data/\(name.lowercased())"
loadBooksFromDatabase()
}

// Public methods - the API
public func addBook(_ book: Book) {
validateBook(book)
books.append(book)
saveToDatabase()
}

public func lendBook(id: String, to person: String) -> Bool {
guard let index = findBookIndex(with: id) else {
return false
}

if books[index].isAvailable {
books[index].borrowedBy = person
books[index].isAvailable = false
saveToDatabase()
return true
}
return false
}

// Private implementation details
private func validateBook(_ book: Book) {
// Validate book data
}

private func findBookIndex(with id: String) -> Int? {
return books.firstIndex { $0.id == id }
}

private func loadBooksFromDatabase() {
// Load books from a database
print("Loading books from \(databasePath)")
}

private func saveToDatabase() {
// Save books to a database
print("Saving to \(databasePath)")
}
}

// Book model
public struct Book {
public let id: String
public let title: String
public let author: String
public fileprivate(set) var isAvailable: Bool
public fileprivate(set) var borrowedBy: String?

public init(id: String, title: String, author: String) {
self.id = id
self.title = title
self.author = author
self.isAvailable = true
self.borrowedBy = nil
}
}

And here's how someone would use this library:

swift
// In another file
let cityLibrary = Library(name: "City Central Library")

// Add books
cityLibrary.addBook(Book(id: "B001", title: "Swift Programming", author: "John Doe"))
cityLibrary.addBook(Book(id: "B002", title: "iOS Development", author: "Jane Smith"))

// Lend a book
if cityLibrary.lendBook(id: "B001", to: "Mike Johnson") {
print("Book lent successfully")
} else {
print("Failed to lend book")
}

// Check library status
for book in cityLibrary.books {
if book.isAvailable {
print("\(book.title) by \(book.author) - Available")
} else {
print("\(book.title) by \(book.author) - Borrowed by \(book.borrowedBy ?? "unknown")")
}
}

// Cannot modify the book's availability directly
// book.isAvailable = true // Error: Cannot assign to property: 'isAvailable' setter is fileprivate

In this example:

  • We've made the Library class and Book struct public so they can be used from other modules
  • We've kept implementation details like databasePath private
  • We've used private(set) for the books array to allow reading but not modifying
  • The Book struct uses fileprivate(set) to allow the Library class to modify availability

Best Practices for Access Control

  1. Use the minimum access level required: Always use the most restrictive access level that still lets you accomplish your task.

  2. Make implementation details private: Hide internal implementation details from consumers of your code.

  3. Public API should be well-documented: Everything you expose as public or open becomes part of your API contract.

  4. Consider using private(set): This allows properties to be read publicly but modified only within the class.

  5. Be aware of default access level: Remember that the default is internal, which might be more permissive than you need.

  6. Open only what needs to be subclassed: Use open sparingly, only for classes and members that are specifically designed for inheritance.

Summary

Swift's access control system provides flexible tools to hide implementation details and expose clean interfaces for your code. By properly managing access control, you create more maintainable, safer code with clear boundaries between different components.

Access levels from most restrictive to least restrictive:

  1. private (accessible within the declaring entity)
  2. fileprivate (accessible within the source file)
  3. internal (accessible within the module)
  4. public (accessible from outside the module)
  5. open (accessible and overridable from outside the module)

By understanding and applying these concepts, you can design better Swift classes, structures, and APIs that are both safe to use and easy to maintain.

Additional Resources

Exercises

  1. Create a BankAccount class with appropriate access control that keeps the balance readable but only modifiable through deposit and withdraw methods.

  2. Design a GameEngine class with various access levels to separate the public API from internal implementation details.

  3. Create a framework with both public and open classes, then in a separate app project, try to extend and override them to understand the differences.

  4. Refactor an existing class to use more appropriate access control modifiers. Look for opportunities to make implementation details private.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)