I want to store Date with @AppStorage by satisfying one of its requirements: where Value : RawRepresentable, Value.RawValue == String:
extension Date: RawRepresentable {
// use TimeInterval(Double)
// format is locale independent
public var rawValue: String {
String(describing: timeIntervalSinceReferenceDate)
}
public init?(rawValue: String) {
guard let timeInterval = TimeInterval(rawValue) else {
// this should never happen. But if it does, return nil @AppStorage var is set to initial value
print("⚠️ Date failed to parse rawValue String \"\(rawValue)\" as TimeInterval aka Double")
return nil
}
self.init(timeIntervalSinceReferenceDate: timeInterval)
}
}
I have other types I want to store in @AppStorage, like Color.
It comes down to the “what if two people did this” problem. What if Apple adds its own RawRepresentable for Date in iOS 16? What if one of your dependencies does? It’d be bad if they picked a different string representation or a different raw type altogether. (I think you could make a good case that date.rawValue should give you timeIntervalSinceEpoch.)
That's the cost, yeah. It might be possible to solve this with nested property wrappers; a quick simplified attempt seems to work.
(code)
import SwiftUI
@propertyWrapper
struct Wrap {
var wrappedValue: Int
}
extension Wrap: RawRepresentable {
var rawValue: String {
wrappedValue.description
}
init?(rawValue: String) {
guard let value = Int(rawValue) else {
return nil
}
wrappedValue = value
}
}
struct Test {
@AppStorage("x") @Wrap var x: Int = 5
}
Test().x = 6
print(UserDefaults.standard.object(forKey: "x"))
I meant "…SinceReferenceDate" but once again the fact that it's ambiguous sort of points out why you shouldn't do this: someone else might have different opinions.
What I think Jordan is saying is that this ambiguity is exactly what leads to conflicting definitions: one person might try to write a conformance in terms of timeIntervalSince1970, and another might want timeIntervalSinceReferenceDate, and the specifics of how those could collide can lead to serious issues.
(If you, today, wrote an implementation using timeIntervalSince1970, and Apple added a conformance in the future in terms of timeIntervalSinceReferenceDate, not only would your code no longer compile because of the redundant conformance, but you couldn't even switch over to the official implementation directly because it would conflict with the behavior all of your other code could be expecting.)
It's potentially even worse than not compiling. When your app is running without being recompiled on the new OS version, you'll have two different conformances to the protocol at once. Swift does an impressive job of supporting multiple conformances and using the correct one depending on the static context, but this does break in some cases. In this specific case it's pretty likely to work (since you can't dynamic cast to RawRepresentable), but if it doesn't then existing installs of your app will appear to work on the new OS version but the persisted dates will all be wrong.
Protocols with the same name from different libraries are different protocols, and there won't be issues with using existing compiled apps. To recompile with the new version of the library you may need to switch to fully-qualifying the protocol name (i.e. extension String: MyApp.SuperProtocol), which is annoying but not a major problem. If you want to declare conformances to both of the SuperProtocols in your code and they have overlapping requirements that need different implementations, then you have to use @_implements.
One of the "exciting" implications of Swift's namespace lookup rules is that adding a new public symbol should be a semver major version bump, but everyone ignores that.
The concept of "owing" the code is rather informal. You'd normally own a protocol when it's in the library you're writing, but many Swift frameworks are split into multiple sub-frameworks. Would SwiftNIO and SwiftNIO-SSL be able to "own" each other's protocols? They are on an entirely different repo, but maintain by the same group. Adding even more ceremony would only increase friction of splitting a large codebase (than it already has). What about generics? Do I own Collection if its Element is MyType?
Now, other languages have done that, e.g., Rust's Orphan Rule, but much of their entire ecosystems are designed around such restriction such as Rust having both From and Into. It's possible to do something similar in Swift, but it's a pretty big problem, not to mention source-compatibility issue that will surely ensue. (why yes, we have migrators, but if possible, I'd rather not.) One can pitch & play around with the idea, of course.
It might also help prevent misuse of protocol conformances, so we’d be be getting something in return for that pain.
I think you could make a good case that the lack of ceremony in this very specific situation (a conformance where the module owns neither the type nor protocol) is actively harmful. It’s an additional commitment that is part of the conformance but is never spelled out.
Swift does not support types having multiple constrained conformances, so no - you can't base a protocol conformance-related ownership claim on generic constraints.
"Ownership" of the types is really just a shorthand for whether or not you are in a good position to ensure that protocol conformances are unique at runtime. As you say, it is not a perfect measure, but it may be enough to prevent casual misuse without inconveniencing the majority of developers.