Bringing Kotlin’s .copy Semantics to Swift Structs
On Android, Kotlin developers rely on data class to model immutable state objects. When a field needs to change, the .copy function produces a new instance with the updated value while carrying over everything else. The pattern keeps state single-sourced and makes rendering code safe from accidental mutation.
Swift’s struct offers value semantics, but no equivalent to .copy. Recreating that convenience in Swift quickly turns into boilerplate, especially with structs that have numerous properties or nested structs. A functional builder pattern helps close the gap, and with code generation it becomes nearly as ergonomic as Kotlin’s built-in solution.
The Problem with Default Initializers
The compiler-generated memberwise initializer already allows you to specify all properties when creating a fresh struct. That luxury disappears the moment you give a property a default value or define a custom init. Once either step is taken, you must write your own initializer that captures every field.
Even after that initializer exists, manually copying a struct field-by-field for one small override is exactly the kind of repetitive code that leads to mistakes and cluttered call sites. This becomes particularly painful when the struct’s fields are themselves structs that need to be preserved as-is.
Attempting a Parameter Default Approach
The obvious answer in Swift is a copy function whose parameters default to the current instance’s values. That mirrors Kotlin’s approach where the data class knows its current state and uses it as fallbacks.
Unfortunately Swift blocks this directly. You cannot reference self in a default parameter value. So a function signature like:
func copy(name: String? = nil) -> Self
fails as soon as you try to bind that default to a property value. Setting defaults to nil and conditionally unwrapping works for non-optional properties, but it immediately breaks with optional properties—there is no way to distinguish “I didn’t pass a value” from “I explicitly want to set this to nil.”
The Builder Alternative
A workable solution comes from a functional builder pattern. Instead of passing overrides as parameters, the copy function takes a closure that receives a mutable builder object:
extension MyStruct {
func copy(_ configure: (inout Builder) -> Void) -> MyStruct {
var builder = Builder(current: self)
configure(&builder)
return builder.build()
}
}
The builder holds all properties, initialized from the original struct. Inside the closure you assign new values only for the fields you actually want to change. Optional fields present no ambiguity here: since you are assigning to the builder’s properties directly, you can set a value to nil without any special machinery.
This pattern doesn’t match Kotlin’s one-call ergonomics exactly, but it gets close and stays fully explicit about which fields are overwritten.
Stamp Out Boilerplate with Sourcery
Writing a builder per struct by hand is maintainable for a few types, but grows monotonous across many state objects. Sourcery, the Swift code generator, can eliminate that entirely. A custom stencil template takes each annotated struct and generates:
- a full initializer covering every property,
- the builder class that mirrors the struct’s fields,
- and a
copyextension method that threads both together.
With the template in place, new immutable structs get the full copy functionality for free. Kotlin’s data class remains the more compact syntax, but this combination of a builder pattern and generated source makes Swift state handling nearly as pleasant.



