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:
- Open - The least restrictive access level
- Public - Similar to open, but with some restrictions for subclassing and overriding
- Internal - The default access level
- File-private - Restricts access to within the current source file
- 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):
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:
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:
// 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:
// 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:
// 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:
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:
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:
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:
// 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:
// 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 andBook
structpublic
so they can be used from other modules - We've kept implementation details like
databasePath
private
- We've used
private(set)
for thebooks
array to allow reading but not modifying - The
Book
struct usesfileprivate(set)
to allow theLibrary
class to modify availability
Best Practices for Access Control
-
Use the minimum access level required: Always use the most restrictive access level that still lets you accomplish your task.
-
Make implementation details private: Hide internal implementation details from consumers of your code.
-
Public API should be well-documented: Everything you expose as
public
oropen
becomes part of your API contract. -
Consider using
private(set)
: This allows properties to be read publicly but modified only within the class. -
Be aware of default access level: Remember that the default is
internal
, which might be more permissive than you need. -
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:
private
(accessible within the declaring entity)fileprivate
(accessible within the source file)internal
(accessible within the module)public
(accessible from outside the module)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
- Swift Documentation: Access Control
- Apple Developer: Swift Programming Language
- WWDC Sessions on Swift
Exercises
-
Create a
BankAccount
class with appropriate access control that keeps the balance readable but only modifiable throughdeposit
andwithdraw
methods. -
Design a
GameEngine
class with various access levels to separate the public API from internal implementation details. -
Create a framework with both
public
andopen
classes, then in a separate app project, try to extend and override them to understand the differences. -
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! :)