A singleton is a design pattern that ensures only one instance of a class exists throughout your entire app. Instead of creating new objects every time you need access to something, you access a single shared object that everyone in your app can use.
Think of it like the settings app on your phone. There’s only one settings panel, and everything that needs to read or change a setting goes through that one place. Singletons work the same way in Swift: one object, globally accessible, always the same one.
In this article, you’ll learn what a singleton actually is, how to write one in Swift, the most common patterns you’ll see in the wild, how singletons appear in real iOS apps, and the gotchas you need to watch out for. By the end, you’ll know exactly how and when to use them.
Basic Implementation
Here’s the simplest singleton you can write in Swift:
class AppSettings {
static let shared = AppSettings()
var username: String = ""
var isDarkMode: Bool = false
private init() { }
}
// Accessing the singleton from anywhere in your app
AppSettings.shared.username = "Chris"
let name = AppSettings.shared.usernameLet’s go through each part so you know exactly what’s happening:
| Line | What it does |
|---|---|
class AppSettings | Declares the class. Singletons must be classes in Swift, not structs. Structs are copied when passed around, which defeats the entire point of having one shared object. |
static let shared = AppSettings() | Creates one instance of the class and stores it as a static property. The name shared is a convention, not a requirement. You’ll also see default and instance used. |
var username, var isDarkMode | These are normal properties on the class. They’re stored on the singleton, so any code that reads or writes them is reading and writing the same values. |
private init() { } | This prevents anyone outside the class from calling AppSettings() and creating a second instance. Without this, another developer could accidentally create a duplicate and break the pattern. |
AppSettings.shared.username | This is how you access the singleton from anywhere in your app. You go through the class name, then the static property, then the property you want. |
Breaking It Down
Why static?
The static keyword means the property belongs to the class itself, not to any particular instance. Because of that, you can access it without ever creating an AppSettings object first. You just write AppSettings.shared and you’re there.
Swift also initializes static properties lazily, meaning the singleton isn’t created the first time the class is defined. It’s created the first time someone actually accesses AppSettings.shared. And Swift guarantees that initialization only happens once, even if two pieces of code try to access it at the same time.
Why private init?
Without private init(), another part of your code could write let mySettings = AppSettings() and create a completely separate object. That breaks the whole point of a singleton, because now you have two objects and they don’t share state.
Making the initializer private locks that down. Only the class itself can call AppSettings(), which it does exactly once when creating shared.
Why a class and not a struct?
Structs in Swift are value types. Every time you pass a struct to a function or assign it to a new variable, Swift makes a copy. There’s no such thing as “one shared copy” of a struct.
Classes are reference types. When you pass a class instance around, you’re passing a reference to the same object in memory. That’s what makes the singleton pattern possible: every part of your app is holding a reference to the same object.
private init(). If you skip it, your singleton class still works, but nothing stops someone from creating a second instance. Always include it so the constraint is enforced by the compiler.Variations and Syntax Patterns
Once you understand the basic shape, you’ll start seeing singletons in slightly different forms. Here are the most important variations a beginner needs to recognize and know how to use.
class NetworkManager {
static let shared = NetworkManager()
private init() { }
func fetchData(from url: URL) {
// networking code here
}
}
// Usage
NetworkManager.shared.fetchData(from: someURL)shared is a convention from Apple’s own APIs. When you see .shared on a class, you immediately know it’s a singleton. Stick to this name for consistency with the rest of the iOS ecosystem.class AudioPlayer {
static let `default` = AudioPlayer()
private init() { }
}
// Usage
AudioPlayer.`default`.play()FileManager.default and NotificationCenter.default. The backticks around default are needed because default is a Swift keyword. In your own code, shared is cleaner and more readable.class UserSession {
static let shared = UserSession()
private init() { }
var isLoggedIn: Bool = false
var currentUserId: String?
var authToken: String?
func logOut() {
isLoggedIn = false
currentUserId = nil
authToken = nil
}
}
// Set on login
UserSession.shared.isLoggedIn = true
UserSession.shared.currentUserId = "user_abc123"
// Read from any view in the app
if UserSession.shared.isLoggedIn {
// show home screen
}class Logger {
static let shared: Logger = {
let instance = Logger()
instance.configure()
return instance
}()
private init() { }
private func configure() {
// one-time setup code
}
func log(_ message: String) {
print("[LOG] \(message)")
}
}
Logger.shared.log("App started")static let shared = Logger(), you use a closure: static let shared: Logger = { ... }(). This lets you run additional setup code on the instance before returning it. The closure still only runs once, so the setup only happens once.// URLSession singleton — shared network session
let session = URLSession.shared
// FileManager singleton — access to the file system
let files = FileManager.default
// UserDefaults singleton — persistent key/value storage
let defaults = UserDefaults.standard
// NotificationCenter singleton — broadcast events app-wide
let center = NotificationCenter.default
// UIApplication singleton — represents the running app
let app = UIApplication.shared// No private init — you CAN create other instances, but shared is the default
class ThemeManager {
static let shared = ThemeManager()
var primaryColor: String = "blue"
}
// This is valid — creates a separate instance
let testTheme = ThemeManager()
// But everyone in your app uses this
ThemeManager.shared.primaryColor = "orange"private init(), you can still create other instances. This form is useful in testing, where you might want a fresh, isolated instance. Just be intentional about which form you use.How Singletons Appear in Real iOS Code
Let’s build a realistic example. One of the most common uses for a singleton is a network manager that handles API calls across your app. Every screen that needs data goes through the same manager.
import Foundation
// The singleton — one NetworkManager for the whole app
class NetworkManager {
static let shared = NetworkManager()
private init() { }
private let baseURL = "https://api.example.com"
func fetchRecipes(completion: @escaping ([String]) -> Void) {
guard let url = URL(string: "\(baseURL)/recipes") else { return }
URLSession.shared.dataTask(with: url) { data, response, error in
// handle response and call completion
completion(["Pasta", "Tacos", "Salad"]) // simplified
}.resume()
}
}
// RecipeListView.swift — uses the singleton to load data
struct RecipeListView: View {
@State private var recipes: [String] = []
var body: some View {
List(recipes, id: \.self) { recipe in
Text(recipe)
}
.onAppear {
// Call the singleton from this view
NetworkManager.shared.fetchRecipes { fetched in
recipes = fetched
}
}
}
}
// FavoritesView.swift — a different view, same singleton
struct FavoritesView: View {
var body: some View {
Button("Refresh") {
// Same NetworkManager.shared — same instance, same base URL, same session
NetworkManager.shared.fetchRecipes { _ in }
}
}
}Notice that RecipeListView and FavoritesView both call NetworkManager.shared and they’re both talking to the same object. The baseURL is configured once, and both views benefit from it. If you needed to update an auth token or change the base URL, you’d do it in one place and the whole app picks it up.
Also notice: inside fetchRecipes, you can see URLSession.shared. That’s Apple’s own singleton. Singletons calling singletons is perfectly normal in iOS code.
Common Mistakes to Avoid
1. Forgetting private init()
Without a private initializer, your class isn’t truly a singleton. Nothing stops another developer from writing let myManager = NetworkManager() and creating a second, disconnected object. That object won’t share state with NetworkManager.shared, which leads to confusing bugs. Always add private init() { }.
2. Using a struct instead of a class
This one trips up a lot of beginners. If you accidentally define your singleton as a struct, Swift will make a copy every time you access it. Your properties won’t persist and changes made in one place won’t show up in another. Singletons must be classes.
class and not a struct.3. Overusing singletons for everything
Singletons are easy to reach for, but they’re not always the right tool. When you use a singleton, any part of your app can access or modify its state. That sounds convenient until you have a bug where some view deep in your app is unexpectedly changing shared state and it takes you an hour to track down where.
Use singletons for genuinely shared resources: a network manager, a logger, a user session. If you find yourself creating singletons just to avoid passing data between views, consider other patterns like dependency injection or environment objects in SwiftUI.
4. Expecting singletons to be easy to test
Unit testing is harder when your code calls a singleton directly. You can’t easily swap in a mock or test version. If testability matters to you, consider injecting the singleton as a parameter rather than accessing it directly inside functions. That way, tests can pass in a different object and your code doesn’t change.
Quick Reference
Here’s a cheat sheet of the key singleton syntax forms covered in this article:
| Syntax | What It Does |
|---|---|
| static let shared = ClassName() | Creates the one shared instance. Lazily initialized on first access. |
| private init() { } | Prevents external code from creating additional instances of the class. |
| ClassName.shared.property | How you read or write a property on the singleton from anywhere in your app. |
| ClassName.shared.method() | How you call a method on the singleton. |
| static let shared: T = { let i = T(); i.setup(); return i }() | Closure form: runs setup code once when the singleton is first accessed. |
| URLSession.shared / FileManager.default / UserDefaults.standard | Apple SDK singletons you already use. All follow the same pattern. |
Using AI to Go Deeper with Singletons
AI coding tools like Claude, GitHub Copilot, and Cursor are part of how developers work today. Knowing how to prompt them well is itself a useful skill. Here are some prompts worth keeping handy as you learn this pattern.
When To Use a Singleton
- 🌐
A recipe or social app that makes API calls
You have a recipe list screen, a detail screen, a favorites screen, and a search screen. All four need to make network requests. A
NetworkManager.sharedsingleton handles all of them with a consistent configuration: base URL, auth headers, error handling. You write that logic once and every screen benefits. - 👤
An app with user accounts and login state
When a user logs in, you need to store their ID, auth token, and subscription status. Every screen that shows different content based on login state needs access to that information. A
UserSession.sharedsingleton stores it once and makes it available everywhere without passing it through every function call. - 📋
A to-do or notes app that logs events
You want to track events throughout your app: when a user creates a task, completes one, or opens a specific screen. A
Logger.sharedorAnalyticsManager.sharedsingleton lets any part of the app log events in one consistent place, without needing to pass a logger object around through every view and view model.
Summary
Here’s a quick recap of what you learned about singletons in Swift:
- A singleton ensures only one instance of a class exists throughout your app, accessible from anywhere
- The pattern requires two things:
static let shared = ClassName()andprivate init() { } - Singletons must be classes, not structs — structs are copied and can’t maintain shared state
- Swift lazily initializes static properties and guarantees the initializer only runs once, even with multiple threads
- You’ve already been using Apple’s singletons:
URLSession.shared,FileManager.default,UserDefaults.standard - Use singletons for genuinely shared resources: network managers, user sessions, analytics, logging
- Overusing singletons can make code harder to test and debug — reach for them intentionally, not habitually
The best way to cement this is to write one. Try creating a Logger.shared or AppSettings.shared singleton in a project you’re already working on and route some real data through it.
Once you’re comfortable with singletons, a natural next topic is dependency injection — a pattern that solves many of the testability problems that singletons introduce. The two concepts go hand in hand in professional iOS codebases.

