Swift: Swift Extensions and Access Control
Extensions let you add new functionality to existing classes, structs, enums, or protocols. Access control makes your code interfaces clearer and encapsulation tighter. This lesson combines both to help you write more elegant Swift code.
1. What You'll Learn
- Add methods and properties to existing types using
extension - Conform to protocols via extensions
- Computed property extensions
- Swift's five access control levels
- Access control best practices
2. A Backend Developer's Real Story
(1) Pain Point: Can't Add Functionality to Foundation's String
While working on date formatting, Bob realized that Date had no method to convert to a human-readable "how long ago" format. He wrote a utility function:
func timeAgo(from date: Date) -> String {
let seconds = Date().timeIntervalSince(date)
if seconds < 60 { return "\(Int(seconds))s ago" }
if seconds < 3600 { return "\(Int(seconds / 60))m ago" }
return "\(Int(seconds / 3600))h ago"
}
// Must pass the date as argument every time
print(timeAgo(from: someDate))
Every call requires passing the argument and invoking a global function -- the code doesn't feel natural. Bob wanted Date to return this format on its own -- just like a built-in method.
(2) The extension Solution
extension Date {
func timeAgo() -> String {
let seconds = Date().timeIntervalSince(self)
if seconds < 60 { return "\(Int(seconds))s ago" }
if seconds < 3600 { return "\(Int(seconds / 60))m ago" }
return "\(Int(seconds / 3600))h ago"
}
}
// Looks like a built-in Date method
print(someDate.timeAgo())
Extensions give Date a new method -- no inheritance, no modifying the source.
(3) Benefits: Natural Syntax + High Cohesion
| Dimension | Global Function | Extension |
|---|---|---|
| Call style | timeAgo(from: date) |
date.timeAgo() |
| Code organization | Scattered everywhere | Tightly grouped with type |
| Discoverability | Must manually import utility module | Xcode autocomplete |
| Maintainability | Global namespace pollution | Type-specific namespace |
3. Extensions
(1) Extension Syntax
Extensions can add the following to existing types:
| Can Add | Example | Limitation |
|---|---|---|
| Computed properties | var area: Double { ... } |
Cannot add stored properties |
| Instance/type methods | func reversed() -> String |
Allowed |
| Initializers | init?(json: [String: Any]) |
Cannot add deinit |
| Subscripts | subscript(idx: Int) -> T |
Allowed |
| Nested types | enum Status { ... } |
Allowed |
| Protocol conformance | extension Type: Protocol |
Allowed |
graph TB
A["extension SomeType {"] --> B["Computed Properties"]
A --> C["Methods"]
A --> D["Initializers"]
A --> E["Subscripts"]
A --> F["Nested Types"]
A --> G["Protocol Conformance"]
A --> H["}"]
▶ Example: Adding Extensions to String
// ============================================
// Adding practical extensions to String
// ============================================
extension String {
// Computed property -- check if this is a valid email
var isValidEmail: Bool {
return contains("@") && contains(".")
}
// Method -- truncate to first N characters
func prefix(_ maxLength: Int) -> String {
guard count > maxLength else { return self }
return String(self.prefix(maxLength)) + "..."
}
// Method -- reverse the string
func reversed() -> String {
return String(self.reversed())
}
}
let email = "alice@example.com"
print("\(email) is valid: \(email.isValidEmail)")
let longText = "This is a very long string that needs truncation"
print(longText.prefix(20))
print("Hello".reversed())
Output:
TEXT 📖 Display onlyalice@example.com is valid: true This is a very lon... olleHNote: Extensions cannot override existing methods. If an extension defines a method with the same name as an existing one, the compiler will report an error.
(2) Conforming to Protocols via Extensions
Separate protocol conformance from type definition by implementing it in an extension:
// ============================================
// Conforming to protocols via extensions
// ============================================
// 1. Define the base type
struct User {
let id: Int
let name: String
let email: String
}
// 2. Conform to Hashable via extension (for Set and Dictionary keys)
extension User: Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
// 3. Conform to Equatable via extension
extension User: Equatable {
static func == (lhs: User, rhs: User) -> Bool {
return lhs.id == rhs.id
}
}
// 4. Usage
let users: Set<User> = [
User(id: 1, name: "Alice", email: "alice@example.com"),
User(id: 1, name: "Alice", email: "alice@example.com"), // Duplicate -- deduplicated
User(id: 2, name: "Bob", email: "bob@example.com")
]
print("Users count: \(users.count)") // 2
Output:
TEXT 📖 Display onlyUsers count: 2Tip: Placing protocol conformance in extensions is for code organization -- the type definition contains only core data, and behaviors are added via extensions. This is a common pattern in the Swift standard library.
4. Access Control
(1) Five Access Levels
Swift provides five access control levels, from most open to most closed:
graph TB
A[Access Control Pyramid] --> B["open - Most open, anyone can access/inherit/override"]
A --> C["public - Anyone can access, but cannot inherit/override"]
A --> D["internal - Within the current module (default level)"]
A --> E["fileprivate - Within the current file"]
A --> F["private - Within the enclosing declaration's braces"]
| Level | Keyword | Access Scope | Usable On |
|---|---|---|---|
| Most open | open |
Any module, can inherit and override | Classes, class methods |
public |
Any module, but cannot inherit/override | Classes, methods, properties | |
| Default | internal |
Current module (App or Framework) | All types |
fileprivate |
Current file | Types, methods, properties | |
| Most closed | private |
Current declaration scope (braces) | Properties, methods |
▶ Example: Access Control in Action
// ============================================
// Using the five access levels
// ============================================
// Public class available to all modules
public class BankAccount {
// Publicly readable, privately writable
public private(set) var balance: Double
// Private field -- only accessible within this class
private var accountNumber: String
private var transactionHistory: [String] = []
// Accessible within this file -- for logging
fileprivate var lastTransactionDate: Date?
// Accessible within this module -- for internal auditing
internal var branchCode: String
public init(accountNumber: String, balance: Double, branchCode: String) {
self.accountNumber = accountNumber
self.balance = balance
self.branchCode = branchCode
}
public func deposit(amount: Double) {
balance += amount
transactionHistory.append("Deposit: \(amount)")
lastTransactionDate = Date()
}
// Private helper method
private func log(_ message: String) {
print("[Private] \(message)")
}
}
let account = BankAccount(accountNumber: "123-456", balance: 1000, branchCode: "BR-001")
print("Balance: \(account.balance)") // public getter is readable
// account.balance = 2000 // Compile error! public private(set) prevents external writes
// print(account.accountNumber) // Compile error! private property
// print(account.lastTransactionDate) // Compile error! fileprivate, inaccessible from another file
print("Branch: \(account.branchCode)") // internal, accessible within the same module
Output:
TEXT 📖 Display onlyBalance: 1000.0 Branch: BR-001
(2) Access Control Principles
| Principle | Explanation |
|---|---|
| Least privilege | Default to private, loosen only when needed |
| Stable interface | Public APIs are contracts once published -- modify carefully |
| Module boundaries | public for a framework defines the module boundary; internal is the internal implementation |
| getter/setter | public private(set) pattern: publicly readable, privately writable |
5. Extensions and Access Control
(1) private Sharing in the Same File
private members are accessible within extensions in the same file:
// ============================================
// Visibility of private in extensions
// ============================================
struct UserAccount {
private var token: String = "secret-token"
let name: String
func getToken() -> String {
return token
}
}
// Extensions in the same file can access private members
extension UserAccount {
func resetToken() {
token = "new-token" // Accessible! Same file
}
}
var account = UserAccount(name: "Alice")
print(account.getToken())
account.resetToken()
print(account.getToken())
Output:
TEXT 📖 Display onlysecret-token new-tokenNote: In Swift 4+, extensions in the same file can access
privatemembers. If you need cross-file sharing, usefileprivate.
(2) Best Organization Pattern with Extensions
// Core data definition
struct Product {
let id: Int
let name: String
let price: Double
}
// MARK: - Equatable Conformance
extension Product: Equatable {
static func == (lhs: Product, rhs: Product) -> Bool {
return lhs.id == rhs.id
}
}
// MARK: - Codable Conformance
extension Product: Codable { }
// MARK: - Utility Methods
extension Product {
func formattedPrice() -> String {
return "$\(String(format: "%.2f", price))"
}
var priceWithTax: Double {
return price * 1.08
}
}
▶ Example: Adding Nested Types and Convenience Initializers via Extensions
Extensions can add not only methods and computed properties, but also nested types and initializers:
// ============================================
// Adding nested types and convenience initializers via extensions
// ============================================
// Base type
struct Book {
let title: String
let author: String
let year: Int
}
// Extension: add nested types and convenience initializers
extension Book {
// Nested enum -- genres
enum Genre: String {
case fiction = "Fiction"
case nonfiction = "Non-fiction"
case science = "Science"
case history = "History"
}
// Nested struct -- reading stats
struct ReadingStats {
var pagesRead: Int
var totalPages: Int
var progress: Double {
totalPages > 0 ? Double(pagesRead) / Double(totalPages) * 100 : 0
}
}
// Convenience initializer -- create from a JSON dictionary
init?(from dict: [String: Any]) {
guard let title = dict["title"] as? String,
let author = dict["author"] as? String,
let year = dict["year"] as? Int else {
return nil
}
self.init(title: title, author: author, year: year)
}
}
// Usage
let book1 = Book(title: "Swift Programming", author: "Apple", year: 2024)
let genre = Book.Genre.science
print("\(book1.title) - Genre: \(genre.rawValue)")
let stats = Book.ReadingStats(pagesRead: 120, totalPages: 350)
print("Reading progress: \(String(format: "%.1f", stats.progress))%")
// Create from dictionary
let dict = ["title": "Advanced Swift", "author": "Alice", "year": 2025] as [String: Any]
if let book2 = Book(from: dict) {
print("Created from dictionary: \(book2.title) (\(book2.author), \(book2.year))")
}
Output:
TEXT 📖 Display onlySwift Programming - Genre: Science Reading progress: 34.3% Created from dictionary: Advanced Swift (Alice, 2025)
6. Full Example: User Management System (Extension + Access Control)
// ============================================
// Full example: User management system
// Features: comprehensive use of extension + access control
// ============================================
import Foundation
// 1. Core user type
public class User {
public let id: Int
public let name: String
public let email: String
// Publicly readable, internally writable
public private(set) var score: Int = 0
public private(set) var isActive: Bool = true
// Accessible within the module
internal var lastLogin: Date?
// Private
private var loginHistory: [Date] = []
public init(id: Int, name: String, email: String) {
self.id = id
self.name = name
self.email = email
}
public func login() {
let now = Date()
lastLogin = now
loginHistory.append(now)
isActive = true
}
public func addScore(_ points: Int) {
// Internal validation
guard points > 0 else { return }
score += points
}
}
// MARK: - Description Extension
extension User {
public func description() -> String {
return "User(\(id): \(name), score: \(score), active: \(isActive))"
}
// Determine based on last login time
public var isOnline: Bool {
guard let last = lastLogin else { return false }
return Date().timeIntervalSince(last) < 300
}
}
// MARK: - Activity Log Extension
extension User {
// fileprivate -- accessible only within this file
fileprivate func generateActivityReport() -> String {
return """
User Report - \(name)
=================
Score: \(score)
Active: \(isActive)
Last Login: \(lastLogin?.description ?? "Never")
"""
}
public func printReport() {
print(generateActivityReport())
}
}
// 3. Usage example
let user = User(id: 1, name: "Alice", email: "alice@example.com")
user.login()
user.addScore(50)
user.addScore(30)
print(user.description())
print("Online: \(user.isOnline)")
user.printReport()
// user.score = 100 // Compile error -- public private(set)
// user.loginHistory // Compile error -- private
Output:
TEXT 📖 Display onlyUser(1: Alice, score: 80, active: true) Online: true User Report - Alice ================= Score: 80 Active: true Last Login: 2026-07-30 12:00:00 +0000
❓ FAQ
internal. If you don't write any access control modifier, the default is internal -- accessible within the current module. For App development (single module), internal is equivalent to public. For Framework development, you must explicitly write public or open to expose to external modules.public allows external module access but not inheritance or overriding. open allows external modules to inherit classes and override methods. Only classes and class methods can use open. Safety principle: don't use open unless necessary.📖 Summary
extensioncan add computed properties, methods, initializers, subscripts, and nested types to existing types- Extensions cannot add stored properties or override existing methods
- Conforming to protocols via extensions is the standard Swift code organization pattern
- Five access levels: open > public > internal > fileprivate > private
- The
public private(set)pattern enables "publicly readable, privately writable" - Extensions in the same file can access
privatemembers
📝 Exercises
- Basic: Add an
extensiontoIntwith asquaredcomputed property and anisEvencomputed property. Test4.squared(outputs 16) and3.isEven(outputs false). - Intermediate: Add an
extensiontoDatewith aformattedDatemethod (returns a "yyyy-MM-dd" formatted string) and anisTodaycomputed property. Useextensionto conform toComparableif not already conformed. - Challenge: Create a
SecureStoreclass that uses access control to manage sensitive data. Requirements:publicsave(key:value:)andread(key:)methods;internalcache clearing method;privateencryption/decryption logic;fileprivatelogging method. Use extensions to group protocol conformance and utility methods into different MARK blocks.