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.
namein my reduced example), so it's not really any different from "encode/decode data". In a future rewrite, sure, I might completely redo this architecture, one where the view's properties don't constitute a source of truth as they seem to at the moment. So I agree with you 100%. But this app goes back a long way, and for right now, I'm just trying to tweak it far enough to make it iOS 18 / Swift 6 / strict concurrency so I can fix a couple of bugs and put out an update. The major rewrite will take a long time.MainActoredDecodablewith@MainActor init(from decoder: Decoder) throwsMainActorDecodable, you still won't be able to use it with any existingDecoder(because those still requireDecodableconformance), which defeats the purpose in this case