When you build a screen that spawns lots of game effects, frames can suddenly start stuttering.
If you keep creating short-lived objects like bullets or particles with init, Instruments shows a memory graph jumping like a sawtooth.
That is where the Swift object pool pattern comes in.
Here is the conclusion first: an object pool is a reuse technique that creates objects in advance, lends them out, and takes them back instead of creating them every time. Its purpose is fundamentally different from the often-confused Flyweight pattern.
This article explains what object pools are, how they differ from Flyweight, and how to implement one in Swift.
What Is the Object Pool Pattern?
An object pool pre-creates multiple expensive-to-create objects and stores them in a pool.
Instead of creating one when needed, you take one from the pool and return it when finished.
The key is reuse. The goal is to reduce the memory allocation and deallocation costs caused by repeatedly performing init and deinit.
The one-line summary of an object pool is: “Don’t create; borrow and return.”
It is especially useful in situations like these.
- Objects such as bullets and particles that are created and destroyed in large numbers over short periods
- Resources whose creation is expensive, such as network connections and threads
- Views that are continuously swapped on screen, such as the
UITableViewCellreuse queue
In fact, if you are an iOS developer, you have probably already used an object pool. dequeueReusableCell is an object pool built by Apple.
How Is It Different from Flyweight?
Both can be confusing because they give the impression of “using objects sparingly.”
However, their purposes are completely different.
An object pool is a way to reuse objects such as bullets at different times. Once you use and return this bullet, the next bullet takes its place.
Flyweight is a way for multiple places to reference one shared object simultaneously. For example, when rendering 10,000 trees in a forest, you keep one copy of shared data such as color and texture, and pass position values separately.
The differences can be summarized in a table.
| Category | Object Pool | Flyweight |
|---|---|---|
| Purpose | Reduce creation and destruction costs | Reduce memory usage |
| Reuse model | Borrow and return (time division) | Share simultaneously (space division) |
| State | Each object retains its own state | Shared state separated from external state |
| Typical examples | Cell reuse, particles | Font glyphs, map icons |
In short, an object pool means “take turns reusing,” while Flyweight means “share among everyone.”
How to Build an Object Pool in Swift
The structure is simpler than you might expect: one array for available objects, plus two methods for lending and returning them.
Here is a simple generic pool.
final class ObjectPool<T> {
private var available: [T] = []
private let factory: () -> T
init(factory: @escaping () -> T) { self.factory = factory }
func acquire() -> T { available.popLast() ?? factory() } // Create a new one if none exists
func release(_ item: T) { available.append(item) } // Return it when finished
}
acquire() takes an object from the pool when one remains, and creates a new one only when the pool is empty.
When you return it with release(), the next request reuses that object.
Applying this to particles produces a flow like the following.
let pool = ObjectPool<Particle> { Particle() }
let p = pool.acquire() // Borrow from the pool
p.reset(at: point) // Resetting state is important!
// ...After it is used up on screen
pool.release(p) // Return it
There is one thing you must remember: a returned object retains its previous state.
So when lending it out again, you must perform an initialization such as reset() to prevent ghost data.
When to Use—and Avoid—an Object Pool
Using it everywhere just because it is useful can backfire.
Here are the guidelines I arrived at after using it myself.
Recommended in these cases.
- When dozens or more objects are repeatedly created and destroyed every second
- When creating a single object has a noticeably high cost
- When the memory graph jumps like a sawtooth and GC (garbage collection)/ARC (Automatic Reference Counting) overhead becomes visible
Think twice in these cases.
- If the objects are lightweight and created only once or twice occasionally, pool management costs more
- If you forget to return objects, the pool empties and you end up creating new ones every time
- In a multithreaded environment, pool access requires a lock or queue synchronization
Swift has many value types (structs) and ARC is fairly efficient, so you do not need to introduce a pool by default.
Measure with Instruments first, then introduce a pool once you confirm a bottleneck.
An object pool is a reuse pattern that saves creation costs, while Flyweight is a sharing pattern that saves memory.
Once you clearly understand the difference, you can choose the right tool for each situation.
Measure first and introduce it precisely when needed. I hope you experience the moment when your frame rate becomes noticeably smoother.

