Skip to content

yysskk/swift-copying

Repository files navigation

swift-copying

CI Documentation Swift Versions Platforms License

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.

Installation

Swift Package Manager

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"]
)

Xcode

  1. Choose File → Add Package Dependencies…
  2. Enter the package URL https://github.com/yysskk/swift-copying.git
  3. Pick a dependency rule, then add the Copying library product to your target.

Usage

import Copying

Structs

@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")

Classes

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

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)

Optional properties

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 nil

Because 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.

How it works

@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
    )
}

What gets copied

The macro generates a parameter only for stored properties that can take part in a copy.

Skipped silently:

  • static (and class) type properties
  • Computed properties (willSet/didSet-only properties are still stored and are included)
  • lazy properties
  • let constants 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 — write var count: Int = 0)
  • A var bound through a tuple pattern, e.g. var (x, y) = (0, 0)
  • A type with no copyable stored properties
  • Applying @Copying to anything other than a struct, class, or actor

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.

Documentation

Full API documentation, including articles on the rules, optional-property semantics, and a getting-started guide, is published to GitHub Pages. It is also available on the Swift Package Index.

Requirements

  • Swift 6.2+
  • macOS 10.15+ / iOS 13+ / tvOS 13+ / watchOS 6+ / visionOS 1+ / Mac Catalyst 13+

License

swift-copying is available under the MIT license. See LICENSE for details.

About

A Swift Macro that generates a copying method for struct and class types, similar to Kotlin's copy function for data classes.

Topics

Resources

License

Contributing

Stars

6 stars

Watchers

0 watching

Forks

Contributors

Languages