makeWidget() vs. init(widget:): Does Swift Prefer Factory Methods or Convenience Inits?
If you’ve spent any time working across different programming languages, you’ve likely encountered two distinct design philosophies for object creation:
- The OOP / Java Style: Factory functions like
WidgetFactory.createWidget()orcontext.makeChildContext(). - The Swifty Style: Rich, descriptive initializers like
Widget(from: context)orNSManagedObjectContext(childOf: viewContext).
When designing Swift APIs—whether for an app architecture or an open-source library—a common question arises: Should you write a make...() factory method or a convenience init?
The short answer: Swift strongly prefers initializers.
Let’s break down why initializers win by default, where Apple’s official API guidelines stand, and the specific scenarios where a make function is actually the right tool for the job.
1. The Swift Philosophy: Initializers Are First-Class Citizens
In many languages (like C++ or legacy Java), constructors are somewhat limited—they don’t participate in protocols, can’t be failable without messy workarounds, and often carry rigid syntax.
Swift turned this on its head by making initializers extraordinarily powerful:
- Failable Initializers (
init?): Express failure natively without throwing exceptions. - Throwing Initializers (
init() throws): Surface initialization errors directly todo-catchblocks. - Convenience Initializers (
convenience init): Layer clean, domain-specific setup logic on top of designated initializers. - Protocol Initializers (
initin protocols): Require conforming types to implement specific construction paths.
Because initializers are so capable, Swift developers naturally reach for Type(...) whenever they want a new instance of Type.
2. What the Official Guidelines Say
Apple’s official Swift API Design Guidelines address this directly under the Naming rules:
Prefer initialization over factory functions when possible.
Good:
let text = String(describing: number)Avoid:
let text = String.makeString(from: number)
When you use init, your code becomes instantly discoverable. Any developer on your team can type NSManagedObjectContext( and press Esc or trigger Xcode’s autocomplete to see every supported way to construct that context.
A Real-World Example: Core Data Child Contexts
Consider creating a child NSManagedObjectContext in Core Data. You could write a factory method:
// 🟨 Factory Method (Feels like Java / Objective-C)
extension NSManagedObjectContext {
func makeChildContext() -> NSManagedObjectContext {
let child = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
child.parent = self
return child
}
}
// Call site:
let childContext = viewContext.makeChildContext()
While functional, it hides the creation logic inside an instance method.
The Swifty approach uses a convenience initializer:
// 🟩 Convenience Init (Idiomatic Swift)
extension NSManagedObjectContext {
convenience init(childOf parent: NSManagedObjectContext, concurrencyType: NSManagedObjectContextConcurrencyType = .mainQueueConcurrencyType) {
self.init(concurrencyType: concurrencyType)
self.parent = parent
}
}
// Call site:
let childContext = NSManagedObjectContext(childOf: viewContext)
Notice how much cleaner the call site is. NSManagedObjectContext(childOf: viewContext) reads like standard Foundation code.
3. When Should You Use a make...() Function?
Preferring initializers doesn’t mean make methods are banned. In fact, Swift’s standard library uses make functions deliberately—and very consistently—in specific architectural patterns.
Scenario A: Producing a Secondary Type (The Iterator Pattern)
The most prominent place you’ll see make in native Swift is when an object acts as a factory or generator for a completely different type.
Consider Swift’s Sequence and AsyncSequence protocols:
// Synchronous iteration
var iterator = collection.makeIterator()
// Asynchronous iteration
var asyncIterator = stream.makeAsyncIterator()
Here, calling stream.makeAsyncIterator() doesn’t return another AsyncStream—it produces a specialized AsyncIteratorinstance designed to traverse the stream. The make prefix clearly communicates: “I am not modifying myself; I am instantiating an auxiliary worker object for you to use.”
Other examples from Apple frameworks include:
// Combine: Publisher produces a ConnectablePublisher
let connectable = publisher.makeConnectable()
Scenario B: Polymorphic / Abstract Factory Patterns
When the exact concrete type returned is hidden behind a protocol or dynamic subtype, initializers can become clunky.
// URLSession creates network tasks
let task = session.dataTask(with: url)
You don’t call URLSessionDataTask(session: url) directly because URLSession configures internal, private subclasses under the hood.
Scenario C: Heavy Side Effects or Caching
Initializers imply fast, straightforward memory allocation. If creating the object involves expensive computation, disk I/O, or checking an internal instance cache, a make function signals to the caller that real work is happening:
let texture = device.makeTexture(descriptor: descriptor) // Metal API
Quick Reference: Decision Matrix
| Scenario | Use init | Use make…() |
Constructing Type A from parameters | ✓ | |
| Adding domain-specific setup to an existing class | ✓ | |
Type A spawns a new instance of Type A (e.g. parent/child contexts) | ✓ | |
Type A creates an instance of Type B (e.g. makeAsyncIterator()) | ✓ | |
| Hiding concrete subclasses behind an abstract factory | ✓ | |
| Heavy side effects, caching, or GPU allocations | ✓ |
The Takeaway
When in doubt, start with an initializer.
Convenience initializers keep your API surface predictable, integrate seamlessly with Xcode’s autocomplete, and align with the rest of the Swift ecosystem. Save make functions for when an object is producing a totally distinct secondary type (like makeAsyncIterator()) or performing complex factory work behind the scenes.