The issue is that the type of the wrappedValue given to Capitalized ends up being a Trimmed, and not the String that's within the Trimmed.
You can see that for yourself by building a little debugging wrapper:
@propertyWrapper struct InspectType<T> {
var wrappedValue: T
init(wrappedValue: T) {
self.wrappedValue = wrappedValue
print("The type of the `wrappedValue` is: \(T.self)")
}
}
struct Sample {
@InspectType @Trimmed var testValue: String = " hello, world! "
}
_ = Sample() // prints: "The type of the `wrappedValue` is: Trimmed"
You need to generalize your Capitalized to handle both a String, and another "String-like" thing. I think LosslessStringConvertible seems like a decent choice to describe a "String-like" thing, so I'll go with that. Here's how I would do this:
@propertyWrapper struct Capitalized<S: LosslessStringConvertible> {
private var storage: S
public var wrappedValue: S {
get { storage }
set { storage = S.init(newValue.description.capitalized)! }
}
init(wrappedValue: S) {
self.storage = S.init(wrappedValue.description.capitalized)!
}
}
Which gives us this error:
error: generic struct Capitalized requires that Trimmed conform to LosslessStringConvertible
Which makes sense, because in the compositional case of @Capitalized @Trimmed var testValue, the property wrappers will have a nest type like Capitlaized<Trimmed>. Naturally, we just need to define that conformance:
extension Trimmed: CustomStringConvertible {
var description: String { self.wrappedValue }
}
extension Trimmed: LosslessStringConvertible {
init?(_ string: String) {
self.init(wrappedValue: string)
}
}
Now it works!
struct Sample {
@Trimmed var trimmedString: String = " hello, world! "
@Capitalized var capitalizedString: String = " hello, world! "
@Capitalized @Trimmed var testValue: String = " hello, world! "
}
print(Sample().testValue) // prints "Hello, World!"
You might notice however, that the inverse composition doesn't work:
struct Sample {
// ❌ error: cannot convert value of type 'Capitalized<String>' to specified type 'String'
@Trimmed @Capitalized var inverted: String = " hello, world! "
}
If we want this to work, we need to do the same generifying of Trimmed. Here's how that might look, all together:
import Foundation
@propertyWrapper struct Capitalized<S: LosslessStringConvertible> {
private var storage: S
public var wrappedValue: S {
get { storage }
set { storage = S.init(newValue.description.capitalized)! }
}
init(wrappedValue: S) {
self.storage = S.init(wrappedValue.description.capitalized)!
}
}
extension Capitalized: CustomStringConvertible {
var description: String { self.wrappedValue.description }
}
extension Capitalized: LosslessStringConvertible {
init?(_ string: String) {
self.init(wrappedValue: S(string)!)
}
}
@propertyWrapper struct Trimmed<S: LosslessStringConvertible> {
private var storage: S
public var wrappedValue: S {
get { storage }
set { storage = S.init(newValue.description.trimmingCharacters(in: .whitespacesAndNewlines))! }
}
init(wrappedValue: S) {
self.storage = S.init(wrappedValue.description.trimmingCharacters(in: .whitespacesAndNewlines))!
}
}
extension Trimmed: CustomStringConvertible {
var description: String { self.wrappedValue.description }
}
extension Trimmed: LosslessStringConvertible {
init?(_ string: String) {
self.init(wrappedValue: S(string)!)
}
}
struct Sample {
@Trimmed var trimmedString: String = " hello, world! "
@Capitalized var capitalizedString: String = " hello, world! "
@Capitalized @Trimmed var testValue: String = " hello, world! "
@Trimmed @Capitalized var inverted: String = " hello, world! "
}
print(Sample().inverted)