A Swift macro that generates a copying method for struct, class, and actor types, similar to Kotlin's copy function for data classes.
@Copying adds a copying method that takes one optional argument per stored property and returns a new instance with the properties you pass replaced and the rest carried over from the original.
Add the package to your Package.swift:
dependencies: [
.package(url: "https://github.com/yysskk/swift-copying.git", from: "1.2.0")
]Then add Copying to your target's dependencies:
.target(
name: "YourTarget",
dependencies: ["Copying"]
)- Choose File → Add Package Dependencies…
- Enter the package URL
https://github.com/yysskk/swift-copying.git - Pick a dependency rule, then add the Copying library product to your target.
import Copying@Copying
struct Person {
let name: String
let age: Int
let email: String
}
let john = Person(name: "John", age: 30, email: "john@example.com")
// Change one property
let olderJohn = john.copying(age: 31)
// Person(name: "John", age: 31, email: "john@example.com")
// Change several at once
let jane = john.copying(name: "Jane", age: 25)
// Person(name: "Jane", age: 25, email: "john@example.com")A class must declare an initializer shaped like a struct's memberwise initializer — one argument per copyable stored property, labelled with the property's name — because the generated method calls it:
@Copying
final class User {
let id: Int
var username: String
var isActive: Bool
init(id: Int, username: String, isActive: Bool) {
self.id = id
self.username = username
self.isActive = isActive
}
}
let user = User(id: 1, username: "johndoe", isActive: true)
let inactiveUser = user.copying(isActive: false)Actors work the same way. Because copying is actor-isolated, call it with await from outside the actor:
@Copying
actor Counter {
let id: Int
var value: Int
init(id: Int, value: Int) {
self.id = id
self.value = value
}
}
let counter = Counter(id: 1, value: 0)
let advanced = await counter.copying(value: 10)Every parameter defaults to nil, meaning "keep the current value". For an optional property, the parameter becomes a double optional so you can still reset it:
@Copying
struct Config {
let name: String
let timeout: Int?
}
let config = Config(name: "default", timeout: 30)
config.copying(timeout: nil) // keeps 30 — the nil literal means "no change"
config.copying(timeout: .some(nil)) // resets to nil
let newTimeout: Int? = nil
config.copying(timeout: newTimeout) // also resets to nilBecause the bare nil literal keeps the current value, reset an optional with .some(nil) or by passing a value of the property's own optional type.
@Copying is an attached member macro. Applying it to:
@Copying
struct Person {
let name: String
let age: Int
}generates the following method inside Person:
func copying(
name: (String)? = nil,
age: (Int)? = nil
) -> Person {
Person(
name: name ?? self.name,
age: age ?? self.age
)
}The macro generates a parameter only for stored properties that can take part in a copy.
Skipped silently:
static(andclass) type properties- Computed properties (
willSet/didSet-only properties are still stored and are included) lazypropertiesletconstants with an initial value, e.g.let maxValue: Int = 100- Immutable tuple bindings, e.g.
let (x, y) = (0, 0)
Rejected with a compile-time error:
- A copyable property without an explicit type annotation, e.g.
var count = 0(a macro cannot infer the type — writevar count: Int = 0) - A
varbound through a tuple pattern, e.g.var (x, y) = (0, 0) - A type with no copyable stored properties
- Applying
@Copyingto anything other than astruct,class, oractor
The generated method inherits the type's access level: an open type produces a public method, and a private type produces a fileprivate one, so the method is callable exactly where the type is.
Full API documentation, including articles on the rules and optional-property semantics, is hosted on the Swift Package Index.
- Swift 6.2+
- macOS 10.15+ / iOS 13+ / tvOS 13+ / watchOS 6+ / visionOS 1+ / Mac Catalyst 13+
swift-copying is available under the MIT license. See LICENSE for details.