Exploring A Kotlin-Inspired Custom Copy Functionality in Swift

- 9 mins

Coming from a web/Javascript and Android dev background, using Javascript and Kotlin, I’ve always appreciated the expressiveness they offered. While working with Swift I found myself missing features from those that make life easier, especially when it comes to using immutable data structures to ensure safe application state management.

One such feature is the copy() method in Kotlin data classes, or the object literal spread syntax of modern Javascript, which allow you to create a new instance of an object with some properties changed, while keeping the rest the same. This is incredibly useful for working with immutable state management and Unidirectional Data Flow design pattern, which are key features in the declarative UI frameworks in those platforms–VueJS and ReactJS for Javascript, and Jetpack Compose for Android.

While working with Swift initially, I always felt that this expressive object cloning ability was missing in the language (or at least I thought so–see bottom section for more on this).

In fact, a while back I built a Redux-like state management package, Turnstate, where I needed this feature. I even used the Swift Keypath API to try to achieve it, but it wasn’t that ergornomic and capable as the Kotlin data class .copy() feature or the Javascript object spread syntax.

Fast forward to Swift version 5.9 being released in 2023 with Macros and all was in place for me to build a more robust solution. I decided to work on a Swift macro on GitHub to auto-generate a copy() method for struct types, mimicking what Kotlin developers have enjoyed for years.

The repo is here: SwiftCopyableMacro

In this post, I’ll walk through what it does, how it works under the hood, edge cases, caveats, and where we can still improve things. No fluff.

Installation (SPM)

Add this to your Package.swift dependencies:

.package(url: "https://github.com/ugommirikwe/SwiftCopyableMacro.git", from: "0.2.0")

Then import and use the macro:

import CopyableMacro

What this macro does

It saves you from writing this crap:

struct User {
  let name: String
  let email: String?
  let age: Int
  let isAdmin: Bool
}

If you ever needed to create a new instance with a few fields changed, you’d have to do:

let newUser = User(name: old.name, email: nil, age: old.age, isAdmin: old.isAdmin)

This is okay if you’re doing it once. It’s not okay when it’s 20+ lines long and used everywhere. So this macro gives you a .copy(...) function with optional named parameters. Similar to Kotlin’s copy() method.

@Copyable
struct User {
  let name: String
  let email: String?
  let age: Int
  let isAdmin: Bool
}

Now you can write:

let updated = user.copy(email: nil)

…and you get back a new instance with everything the same except email is now nil. The original user remains unchanged.

How it works under the hood

At compile time, the macro:

  1. Reads all stored properties of a struct.
  2. Generates a copy(...) method with each parameter defaulted to nil.
  3. Builds an initializer call that uses argName ?? self.argName for non-optionals and argName directly for optionals.

This lets explicit nil override and fallback logic coexist cleanly.

What works

You can pass any number of properties to the copy(…) call and it works. You can also leave them all out and just clone the original value.

let modified = user.copy(age: 50, isAdmin: true)

Default values and custom initialisers are supported. So you can write:

@Copyable
struct Person {
  let name: String
  var nickname: String? = nil

  init(name: String, nickname: String? = nil) {
    self.name = name
    self.nickname = nickname
  }
}

…and still call person.copy(nickname: “Joey”) without any issues.

Example usage

@Copyable
struct Post {
  let id: UUID
  let title: String
  let content: String?
  let author: String
  let tags: [String]?
}

let original = Post(id: .init(), title: "Old Title", content: "blah", author: "ugo", tags: ["swift", "macros"])

let modified = original.copy(
  title: "New Title",
  content: nil,
  tags: ["swift"]
)

Yes, the content field is now explicitly set to nil. I had to make an update to the macro to handle this, so it’s a recent addition. Thanks to an exchange I had with Michael Long on LinkedIn.

Edge cases and caveats

Optional types

Custom initialisers

Supported. The macro doesn’t try to rewrite your initialisers. It uses the public one Swift generates or the one you wrote — as long as it covers all properties.

Computed properties

Not supported (and probably shouldn’t be). Only stored properties are copied.

Enums, nested types, or other advanced struct patterns

Should work fine in many cases, but I haven’t tested all permutations. Open an issue if you hit a wall.

Known limitations

  1. Does not support nested updates. If your struct contains another @Copyable struct, you’ll need to manually call .copy(…) on the nested type first. (Could be recursive in future.)

  2. Doesn’t support enums or classes. This is strictly for structs for now.

  3. Copying very large structs can affect performance. Swift value types are copied at runtime, which can incur stack or heap allocations depending on the type’s size. Consider splitting very large structs into smaller units.

  4. Doesn’t support computed properties or non-stored vars. If you use var foo: Int { ... }, it won’t be part of the copy method.

  5. Parameter ordering rules still apply. Swift requires parameters with no default value to come before those with default values. So if the macro tries to put t: String? = nil before other: Int, you’ll get a compiler error.

Splitting complex types

If you find yourself defining massive structs like:

struct Profile {
  let id: UUID
  let name: String
  let contactInfo: ContactInfo
  let preferences: Preferences
  let settings: Settings
}

You’re better off breaking it down into smaller chunks like:

let updated = profile.copy(
  contactInfo: profile.contactInfo.copy(email: nil),
  settings: profile.settings.copy(theme: .dark)
)

This keeps each copy() small and focused. For more on large value types in Swift, see Swift Forums on large structs and Apple’s performance tips.

More on large value types in Swift:

Room for improvement

The Case Against This Macro

I first mentioned this work on LinkedIn and got valuable feedback, notably from Michael Long in response to John Sundell’s post on LinkedIn and the conversation around it.

John’s article, “Let vs var for Swift struct properties” and Michael’s Medium post “The case against immutable objects” both reinforced that Swift’s value semantics already isolate copies, which led to the question: Is this macro necessary at all?

While value semantics mean mutation of one struct copy doesn’t affect others, the macro still:

  1. Eliminates tedious boilerplate when you need to change multiple fields.
  2. Provides a clear, reusable pattern for incidental mutations.
  3. Handles explicit resetting of optional properties (.copy(email: nil)).

Conclusion

I built this macro to scratch an itch with immutable structs, but the discussions that followed revealed it’s probably solving the wrong problem. Swift’s value semantics already provide the safety that immutability seeks to guarantee, and fighting against mutable structs often creates more problems than it solves.

The real value here was learning Swift’s macro system and understanding when not to use it. Sometimes the best code is the code you don’t write.

Check it out: SwiftCopyableMacro on GitHub

Pull requests and issues welcome! 🙏

Ugo

Ugo

Full stack polyglot software developer