Singleton in Swift


Introduction to Singletons in Swift

A Singleton is a design pattern that ensures a class has only one instance throughout the application's lifecycle. In Swift, singletons are widely used for shared resources, such as:

  • ✔ Network Managers
  • ✔ Database Connections
  • ✔ User Preferences
  • ✔ Logging Services

This guide explains how to implement and use Singletons in Swift efficiently.

1. What is a Singleton?

A Singleton restricts the instantiation of a class to one single instance and provides a global access point to it.

Key Characteristics of a Singleton:

  • Single Instance: Only one object exists.
  • Globally Accessible: The instance can be accessed anywhere in the app.
  • Lazy Initialization: Created only when first needed.

2. Creating a Singleton in Swift

A Singleton is implemented using a static constant inside the class.

Basic Singleton Example

class Logger {
     static let shared = Logger() // Singleton instance

     private init() {} // Private initializer prevents multiple instances

     func log(message: String) {
         print("Log: \(message)")
     }
}

// Accessing Singleton
Logger.shared.log(message: "Singleton instance accessed!")

Output

Log: Singleton instance accessed!

static let shared ensures only one instance exists.
private init() prevents external instantiation.

3. Singleton with Shared Data

Singletons are often used to store global app settings or user preferences.

class AppSettings {
     static let shared = AppSettings()

     private init() {} // Prevents external instances

     var theme: String = "Light"
}

// Accessing Singleton
AppSettings.shared.theme = "Dark"
print(AppSettings.shared.theme)

Output

Dark

✔ Changes persist globally within the app.

4. Singleton with Network Requests

Singletons are commonly used for managing API calls with URLSession.

import Foundation

class APIClient {
     static let shared = APIClient()

     private init() {}

     func fetchData(from url: String) {
         guard let url = URL(string: url) else { return }

         URLSession.shared.dataTask(with: url) { data, response, error in
             if let data = data {
                 print("Data received: \(data.count) bytes")
             }
         }.resume()
     }
}

// Usage
APIClient.shared.fetchData(from: "https://jsonplaceholder.typicode.com/todos/1")

✔ Prevents multiple instances of APIClient.
✔ Ensures consistent network requests across the app.

5. Thread-Safe Singleton (DispatchQueue)

To avoid race conditions in multithreaded environments, use DispatchQueue.

class SafeSingleton {
     static let shared = SafeSingleton()

     private init() {}

     func performTask() {
         DispatchQueue.global(qos: .background).async {
             print("Task executed safely")
         }
     }
}

// Accessing Singleton
SafeSingleton.shared.performTask()

✔ Ensures safe execution across multiple threads.

6. Singleton with Dependency Injection

Singletons can be injected into other classes for modularity.

class DatabaseManager {
     static let shared = DatabaseManager()

     private init() {}

     func connect() {
         print("Database connected")
     }
}

class UserService {
     let database: DatabaseManager

     init(database: DatabaseManager = .shared) {
         self.database = database
     }

     func fetchUser() {
         database.connect()
         print("User data fetched")
     }
}

// Usage
let userService = UserService()
userService.fetchUser()

Output

Database connected
User data fetched

✔ Allows dependency injection for testing.

7. Avoiding Singleton Pitfalls

Common Problems & Solutions:

Problem Solution
Global State Mutation Use private(set) var for controlled access.
Difficult to Test Inject Singleton as a dependency.
Memory Leaks Ensure weak references in closures.
Thread Safety Issues Use DispatchQueue for synchronization.

Conclusion

Singletons are powerful but should be used with caution. When implemented correctly, they provide efficient access to shared resources and improve app architecture.