CopyWithUpdatesMacro/README.md
Allows copying a Swift class or struct while changing arbitrary fields.
The @CopyWithUpdates macro can be applied to a struct or a class to generate a function that takes facultative arguments, one for each of the target's fields, and returns a new instance that is a copy of the original, except for the provided arguments. It supports optionals.
Every so often, there is a need for a copy of a struct, but with differences in one or more fields. The correct way to do it is to invoke init with the intended values for each field, but that's unwieldy. What often happens is that the fields that have to be changed are made mutable, an assignment copy is made, and the change is made after the copy:
// original
let s1 = LargeStruct(a: 5, b: 2, c: 3, d: 7, e: 4, f: 1, g: 6)
// cumbersome copy changing b to 2
let s2 = LargeStruct(a: s1.a, b: 2, c: s1.c, d: s1.d, e: s1.e, f: s1.f, g: s1.g)
// reasonably simple copy + change, but requires that s3 AND b be mutable even if neither is changed outside this context
var s3 = s1
s3.b = 2
Making the instance mutable is a small trade-off, that can even be solved by using some variant of an initialization block. However, making the fields mutable just for the sake of convenient initialization can be seen as a big anti-pattern.
To avoid this need to make the fields mutable, @CopyWithUpdates provides the boilerplate needed in the form of func with(...), that handles the boilerplate of calling init with both the changed and the kept values:
// the solution proposed here
let s4 = s1.copyWithUpdates(b: 2)
// the same, but changing two fields
let s5 = s1.copyWithUpdates(c: 4, g: 7)
// will change f to nil (if field f is defined as Optional)
let s6 = s1.copyWithUpdates(f: nil)
The complete behavior is:
nil for a field that is non-optional is the same as not including that field in the call, i.e. the value from the original struct will be used;nil for an optional field will make that field nil;.some(nil) for an optional field is the same as not including that field in the call, i.e. the value from the original struct will be used.The macro can also apply to classes. In either classes or structs, there will have to be a memberwise init. In structs this is automatically synthesized by the compiler unless a custom init is defined in the struct declaration. It can also be generated via Xcode's autocompletion features.
Annotate the target type with the @CopyWithUpdates macro to generate func with(...) -> Self:
import CopyWithUpdates
@CopyWithUpdates
struct Report {
let venue: String
let sponsor: String?
let drinks: [String]
let complexStructure: [Date: [(String, Int)]]
let characters: [String]?
let budget: Double
}
The code after expansion:
struct Report {
let venue: String
let sponsor: String?
let drinks: [String]
let complexStructure: [Date: [(String, Int)]]
let characters: [String]?
let budget: Double
public func copyWithUpdates(venue: String? = nil, sponsor: String?? = .some(nil), drinks: [String]? = nil, complexStructure: [Date: [(String, Int)]]? = nil, characters: [String]?? = .some(nil), budget: Double? = nil) -> Self {
Self (
venue: venue ?? self.venue,
sponsor: sponsor == .none ? nil : self.sponsor,
drinks: drinks ?? self.drinks,
complexStructure: complexStructure ?? self.complexStructure,
characters: characters == .none ? nil : self.characters,
budget: budget ?? self.budget
)
}
}
All the files in this package are copyright of the package contributors mentioned in the NOTICE file and licensed under the Apache 2.0 License, which is permissive for business use.