How do you make a UIView Codable under Swift 6 concurrency?
To see the problem, start with this:
final class MyView: UIView {
var name: String
init(name: String) {
self.name = name
super.init(frame: .zero)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
So far so good. Now I will make this view Codable by adopting Decodable and Encodable:
final class MyView: UIView, Decodable, Encodable {
var name: String
init(name: String) {
self.name = name
super.init(frame: .zero)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
The compiler goes wild. We are told three things:
Main actor-isolated property 'name' can not be referenced from a nonisolated context
Protocol requires initializer 'init(from:)' with type 'Decodable'
Cannot automatically synthesize 'init(from:)' because implementation would need to call 'init()', which is not designated
Well, I can grapple with the last two problems by writing my own init(from:)
, just as I was doing in Swift 5:
final class MyView: UIView, Decodable, Encodable {
var name: String
enum CodingKeys: String, CodingKey {
case name
}
init(from decoder: any Decoder) throws {
let con = try! decoder.container(keyedBy: CodingKeys.self)
self.name = try! con.decode(String.self, forKey: .name)
super.init(frame: .zero)
}
init(name: String) {
self.name = name
super.init(frame: .zero)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
The compiler doesn't like this either. It doesn't like my init(from:)
:
Main actor-isolated initializer 'init(from:)' cannot be used to satisfy nonisolated requirement from protocol 'Decodable'
Oh, so Decodable says that this is supposed to be nonisolated??? That's not very nice of it. So now what?
The compiler suggests as a fix that I might declare my init(from:)
to be nonisolated
too. But I can't do that, because if I do, then I am not permitted to call super.init(frame:)
.
The compiler alternatively suggests that I just throw @preconcurrency
at my adoption of Decodable, like this:
final class MyView: UIView, @preconcurrency Decodable, Encodable {
var name: String
enum CodingKeys: String, CodingKey {
case name
}
init(from decoder: any Decoder) throws {
let con = try! decoder.container(keyedBy: CodingKeys.self)
self.name = try! con.decode(String.self, forKey: .name)
super.init(frame: .zero)
}
init(name: String) {
self.name = name
super.init(frame: .zero)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
OK, that silences the complaint about init(from:)
. But the complaint about name
remains. The only thing I can think of to silence that one is to declare it nonisolated(unsafe)
:
final class MyView: UIView, @preconcurrency Decodable, Encodable {
nonisolated(unsafe) var name: String
enum CodingKeys: String, CodingKey {
case name
}
init(from decoder: any Decoder) throws {
let con = try! decoder.container(keyedBy: CodingKeys.self)
self.name = try! con.decode(String.self, forKey: .name)
super.init(frame: .zero)
}
init(name: String) {
self.name = name
super.init(frame: .zero)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
And now the compiler is happy. But I'm not! I've been forced to say a bunch of stuff I feel I shouldn't be forced to say. Is this really the best that can be done, under the current circumstances?
Note that this is not quite the same as the situation I solved at https://stackoverflow.com/a/69032109/341994 because a UIView is main actor isolated by nature and cannot be otherwise.
At its core, this issue doesn't have to do with UIView
or Codable
specifically, so let's examine a whittled down case:
// Stand-in for Encodable
protocol P {
func f() // note: mark the protocol requirement 'f()' 'async' to allow actor-isolated conformances
}
// Stand-in for UIView
@MainActor
class C: P {
func f() {} // error: main actor-isolated instance method 'f()' cannot be used to satisfy nonisolated requirement from protocol 'P'
// note: add 'nonisolated' to 'f()' to make this instance method not isolated to the actor
}
What you have here are conflicting requirements:
P
is a protocol which isn't isolated to any specific actor (and thus, implicitly nonisolated
); it neither makes any guarantees to conforming types about which actor they can expect methods to get called on, nor sets any limitations on callers to invoke the protocol requirements on a specific actorC
is a class which is @MainActor
-isolated, meaning that unless otherwise noted, it can only be safely accessed from the main actor (and attempting to access it from any other actor requires an async
access)C.f()
here attempts to satisfy both C
's @MainActor
requirement (implicitly) and P
's nonisolated
requirement — which isn't possibleThis is what you see when you first try to implement init(from:)
yourself: because MyView
is isolated to the main actor, its init(from:)
is implicitly also isolated to the main actor, hence the error.
Depending on the actual requirements of P
, it could be enough to actually mark the implementation as nonisolated
:
@MainActor
class C: P {
nonisolated func f() {}
}
With this annotation, we're stating that it's safe to call f()
from any actor isolation (or no actor isolation), hence meeting the requirements specified by P
.
Unfortunately, as you note, f()
now can't access any state which is isolated, because then its implementation would be violating isolation rules:
@MainActor
class C: P {
var s: String = "Hello"
nonisolated func f() {
print(s) // error: main actor-isolated property 's' can not be referenced from a nonisolated context
}
}
There's a fundamental impasse here: as long as there are conflicting isolated
and nonisolated
requirements, there fundamentally can't be a solution; something has to give.
P
with @preconcurrency
just tells the compiler "this protocol was written before Swift Concurrency, so I know what I'm doing". This is useful when the protocol isn't marked as, e.g., @MainActor
, but you know that its requirements will only ever be accessed from the main actor. This may silence the warning, but if you actually end up accessing isolated state from off the main actor, you'll run into undefined behavior (or crash)nonisolated
will allow you to access it from non-isolated contexts, but also... gets rid of the isolation, so it'll be up to you to then ensure coordination of access from multiple isolations simultaneouslyAt the moment, the best you can do is find a way to untangle these conflicting requirements. On thing you can do is have your isolated type vend another type with a snapshot of state that isn't isolated — e.g., have your UIView
expose a struct State: Codable
interface which contains the view's data and can be used to store and re-hydrate a view.
In the future (well, the upcoming Swift 6.2), what you need here is an isolated conformance, a pitched feature which would allow an isolated type to conform to a non-isolated protocol by exposing its requirements only in the context of the original isolation. i.e., a @MainActor
-isolated view could conform to Codable
by having init(from:)
and encode(to:)
only available to call on the main actor. At the time of writing, this functionality isn't yet available, though.