Objective-C
Singleton Pattern
ARC
iOS Development
Memory Management

How do I implement an Objective-C singleton that is compatible with ARC?

Data Structures & Algorithms practice on Codemia

Step through 300 algorithm problems with animated visualisers that show the data structure changing as the code runs.

Practice algorithms

Introduction

Under ARC, you do not manually retain or release the singleton instance, but you still need to guarantee that only one instance is created. The standard Objective-C solution is a class method backed by dispatch_once, which gives thread-safe, one-time initialization.

A Minimal ARC-Friendly Singleton

Put the shared accessor in the header and make direct initialization unavailable so callers are pushed toward the single instance.

objectivec
1// SettingsManager.h
2#import <Foundation/Foundation.h>
3
4NS_ASSUME_NONNULL_BEGIN
5
6@interface SettingsManager : NSObject
7
8+ (instancetype)sharedManager;
9- (instancetype)init NS_UNAVAILABLE;
10+ (instancetype)new NS_UNAVAILABLE;
11
12@property (nonatomic, copy) NSString *environmentName;
13
14@end
15
16NS_ASSUME_NONNULL_END

Then implement the singleton in the .m file:

objectivec
1// SettingsManager.m
2#import "SettingsManager.h"
3
4@interface SettingsManager ()
5- (instancetype)initPrivate;
6@end
7
8@implementation SettingsManager
9
10+ (instancetype)sharedManager {
11    static SettingsManager *sharedInstance = nil;
12    static dispatch_once_t onceToken;
13    dispatch_once(&onceToken, ^{
14        sharedInstance = [[self alloc] initPrivate];
15    });
16    return sharedInstance;
17}
18
19- (instancetype)initPrivate {
20    self = [super init];
21    if (self) {
22        _environmentName = @"production";
23    }
24    return self;
25}
26
27@end

ARC manages the object's lifetime normally. The crucial part is that dispatch_once ensures the block runs exactly one time, even if multiple threads call sharedManager at the same moment.

Why dispatch_once Matters

Without a synchronization mechanism, two threads could both see an uninitialized static variable and create separate instances. dispatch_once solves that race without forcing you to manage locks manually.

It also keeps the code simple. Older singleton examples often override memory-management methods such as retain or release, but those patterns belong to the pre-ARC era and should not be copied into modern code.

Preventing Extra Copies

If your singleton might be copied, implement copyWithZone: and mutableCopyWithZone: so both operations return the same shared instance.

objectivec
1// SettingsManager.m
2#import "SettingsManager.h"
3
4@interface SettingsManager () <NSCopying, NSMutableCopying>
5- (instancetype)initPrivate;
6@end
7
8@implementation SettingsManager
9
10+ (instancetype)sharedManager {
11    static SettingsManager *sharedInstance = nil;
12    static dispatch_once_t onceToken;
13    dispatch_once(&onceToken, ^{
14        sharedInstance = [[self alloc] initPrivate];
15    });
16    return sharedInstance;
17}
18
19- (instancetype)initPrivate {
20    self = [super init];
21    if (self) {
22        _environmentName = @"production";
23    }
24    return self;
25}
26
27- (id)copyWithZone:(NSZone *)zone {
28    return self;
29}
30
31- (id)mutableCopyWithZone:(NSZone *)zone {
32    return self;
33}
34
35@end

That is not always required, but it is a good defensive step for service-style objects that should never be duplicated.

Using the Singleton Cleanly

A singleton is usually best for cross-cutting services such as configuration, logging, or analytics. Access it from application code through the shared method:

objectivec
1SettingsManager *settings = [SettingsManager sharedManager];
2settings.environmentName = @"staging";
3
4NSLog(@"Current environment: %@", [SettingsManager sharedManager].environmentName);

The class should stay focused. If the singleton turns into a bag of unrelated global state, testability and maintainability both degrade quickly.

Testability still matters

Even a correctly implemented singleton can make unit testing harder if too much application state flows through it. When possible, keep the singleton thin and let other objects depend on protocols or explicit collaborators rather than reaching into shared global state from everywhere.

Common Pitfalls

The biggest mistake is overriding allocWithZone: or old retain-cycle methods copied from outdated blog posts. Under ARC, those implementations add noise and often create surprising behavior without solving any real problem.

Another issue is exposing a normal init method while also advertising a shared instance. That gives callers two code paths and makes the singleton rule easy to bypass. Mark init and new unavailable, then use a private initializer internally.

Finally, be careful with mutable state. A singleton is globally reachable, which means it becomes a shared concurrency surface. If multiple threads mutate its properties, you still need normal synchronization or a design that avoids shared writable state.

Summary

  • Under ARC, the standard singleton pattern is dispatch_once plus a static shared instance.
  • Mark init and new unavailable so consumers use the shared accessor.
  • Use a private initializer for the actual setup work.
  • Return self from copy methods if the object must remain unique.
  • Keep singleton responsibilities narrow, because global mutable state becomes hard to test and reason about.

Related reading
Course
Intermediate
27 lessons
15 hours
DSA Fundamentals

Master algorithmic patterns and data structures through hands-on LeetCode-style problems - from arrays and hashing to dynamic programming and advanced graphs.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Data Structures & Algorithms practice on Codemia

Step through 300 algorithm problems with animated visualisers that show the data structure changing as the code runs.

Practice algorithms

All Rights Reserved.