I have a love-hate relationship with nil.
Don’t get me wrong. Swift’s type system is a lifesaver. I remember the bad old days of Objective-C, sending messages to nil pointers and praying nothing exploded. When Swift came along with Optional<T>, I drank the Kool-Aid. I wrapped everything. I unwrapped everything. I had if let chains nesting four levels deep.
But recently, while refactoring a legacy module in our main app (still running Swift 6.1, don’t ask), I hit a wall. I was looking at a View Controller that was 80% optional binding logic and 20% actual functionality. It was a mess. Every single method started with a guard clause. The code wasn’t doing work; it was just apologizing for the data that might not be there.
That’s when I remembered the Null Object Pattern. It’s an old-school trick, sure, but in Swift, it actually feels surprisingly modern if you pair it with protocols.
The Problem with “Nothing”
Here’s the scenario I ran into last Tuesday. We have a User object. Sometimes a user is logged in, sometimes they’re a guest. The standard Swift way to handle this is an Optional.
struct User {
let id: String
let name: String
let isAdmin: Bool
}
// The "Standard" Way
var currentUser: User?
// Later in the code...
if let user = currentUser {
if user.isAdmin {
showAdminPanel()
} else {
print("User is not admin")
}
} else {
print("User is guest")
}
This looks fine in isolation. But spread this check across fifty different files, and you have a codebase riddled with nil-checks. I got tired of typing currentUser?.name ?? "Guest" every five minutes.
The Null Object pattern takes a different approach. Instead of using nil to represent the absence of a value, you use a concrete object that does nothing.
Protocol-Oriented Null Objects
I decided to rip out the optionals in the analytics service I was building. Instead of checking if the service existed, I just wanted to call methods on it and not care if they did anything.
Here is how I set it up. First, a protocol defines the behavior:
protocol AnalyticsService {
func track(event: String)
func updateUser(id: String)
}
Then, the real implementation. This is the one that actually talks to the backend (we’re using a custom wrapper around Firebase here):
class LiveAnalytics: AnalyticsService {
func track(event: String) {
print("Sending event to cloud: \(event)")
// Actual network call code...
}
func updateUser(id: String) {
print("Updating user context: \(id)")
}
}
Now, the magic bit. The Null Object. This struct conforms to the protocol but its implementation is completely empty. It’s a black hole.
struct NullAnalytics: AnalyticsService {
func track(event: String) {
// Do nothing. Intentionally.
}
func updateUser(id: String) {
// Crickets.
}
}
Why this cleaned up my code
The biggest win wasn’t the definition—it was the usage. In my dependency injection container, I stopped making the service optional. It is always there.
class ViewModel {
private let analytics: AnalyticsService
// If no service is provided, default to the Null Object
init(analytics: AnalyticsService = NullAnalytics()) {
self.analytics = analytics
}
func buttonTapped() {
// No guard let. No optional chaining. Just do it.
analytics.track(event: "Button_Tapped")
}
}
I ran a quick check on the PR where I introduced this. I deleted about 140 lines of code just by removing if let checks and coalescing operators. The cyclomatic complexity dropped significantly because the branching logic for “is this nil?” just vanished.
It’s not a silver bullet (obviously)
I’m not saying you should banish Optionals. Please don’t do that. Optionals are semantically correct when “nothing” is a valid state that you need to react to explicitly.
The Null Object pattern is dangerous if you need to know something is missing. If I use a NullDatabase and I try to save a user, the app won’t crash, but my data won’t save either. It fails silently. That’s a nightmare to debug if you aren’t expecting it.
When to actually use it
After messing around with this pattern for a few years, I’ve found it fits best in these specific spots:
- Analytics and Logging: You rarely want your app to crash or change behavior just because the logger is missing.
- Default View Models: In SwiftUI previews, passing a Null Object that returns placeholder data is way cleaner than mocking a full network stack.
- Delegate patterns: Instead of
delegate?.didSomething(), initialize your delegate property with a no-op implementation. It saves you that one extra character of typing, which adds up.
A SwiftUI specific trick
One place I’m using this heavily right now is in SwiftUI environment objects. Usually, if you forget to inject an EnvironmentObject, your app crashes at runtime. It’s annoying.
Instead, I’ve started using a default value in the Environment key that returns a Null Object.
private struct AnalyticsKey: EnvironmentKey {
static let defaultValue: AnalyticsService = NullAnalytics()
}
extension EnvironmentValues {
var analytics: AnalyticsService {
get { self[AnalyticsKey.self] }
set { self[AnalyticsKey.self] = newValue }
}
}
Now, if I forget to inject the dependency in a specific view hierarchy, nothing blows up. The buttons just don’t track events. For a production app, maybe you want the crash so you catch the bug. But for working on isolated components? It’s a blessing.
Performance note
I was curious if this added overhead. I profiled a loop calling a method on a NullObject struct versus unwrapping an Optional 10,000 times. The difference was negligible on my M3 Pro. The compiler is smart enough to inline empty function calls in many cases. So don’t worry about the CPU cycles. Worry about the readability.
So yeah, give nil a break. Sometimes nothing is better than something, but sometimes a fake something is better than nothing.
