I'm working on an app that enables users to choose default and custom fonts.
// MARK: - CustomFont
enum CustomFont: String, CaseIterable, Identifiable, Codable {
case baskerville = "Baskerville"
case chalkboardSE = "ChalkboardSE-Regular"
var displayTitle: String {
switch self {
case .baskerville:
return "Baskerville"
case .chalkboardSE:
return "Chalkboard SE"
}
}
var id: String {
rawValue
}
}
// MARK: - AppFont
enum AppFont: Hashable, Codable {
case `default`
case custom(font: CustomFont)
}
// MARK: - RawRepresentable
extension AppFont: RawRepresentable {
init?(rawValue: String) {
let data = rawValue.data(using: .utf8)!
self = try! JSONDecoder().decode(AppFont.self, from: data)
}
var rawValue: String {
let data = try! JSONEncoder().encode(self) // EXC_BAD_ACCESS CRASH HERE
return String(decoding: data, as: UTF8.self)
}
}
I need to persist those fonts as Codable RawRepresentable enums in AppStorage.
// MARK: - ContentView
struct ContentView: View {
@AppStorage("selectedFont") private var selectedFont: AppFont = .custom(font: .baskerville)
var body: some View {
VStack {
Picker(selection: $selectedFont) {
Text("Default")
.tag(AppFont.default)
ForEach(CustomFont.allCases) { customFont in
Text(customFont.displayTitle)
.tag(AppFont.custom(font: customFont))
}
} label: {
Text("Select font")
}
if case let .custom(font) = selectedFont {
Text("Hello, world!")
.font(.custom(font.rawValue, size: 17))
} else {
Text("Hello, world!")
}
}
}
}
I know for a fact that it's the Picker
that crashes the app.
The app hangs for several seconds, and the JSON encoder crashes with the generic EXC_BAD_ACCESS error.
I've already tried embedding the enum into a struct. But it yielded the same result.
Here's a concise sample project.
I don't know if it's on me or if I should submit a bug report. Does anybody have any idea of what I might be doing wrong? Thank you!
It is not the Picker that crashes the app at all, instead it is the encoding of AppFont
that enters an infinite loop because in the property rawValue
the function call .encode(self)
calls self.rawValue
because you have declared that self
conforms to RawRepresentable
so the encoder wants to encode the raw value of the enum case.
Note what it says in the article
Remember, that AppStorage only supports RawRepresentable where the RawValue's associatedtype is of type Int or String. So the rawValue property has to return an Int or a String.
But the AppFont enum does not have a RawValue of type String or Int, in fact the enum can not be made to properly conform to RawRepresentable since it has a case with an associated value.
Maybe this should be seen as a compiler bug and that the compiler should produce an error here.
To work around this you can skip the Codable support and use with switch
when converting to/from a string
extension AppFont: RawRepresentable {
private static let defaultRawValue = "default"
init?(rawValue: String) {
switch rawValue {
case Self.defaultRawValue:
self = .default
default:
guard let font = CustomFont(rawValue: rawValue) else { return nil }
self = .custom(font: font)
}
}
var rawValue: String {
switch self {
case .default:
return Self.defaultRawValue
case .custom(let font):
return font.rawValue
}
}
}
I didn't include SWiftUI or UserDefaults when examining this, instead this was my test code
var rawStrings = [AppFont.default.rawValue]
for font in CustomFont.allCases {
let string = AppFont.custom(font: font).rawValue
rawStrings.append(string)
}
for string in rawStrings {
if let font = AppFont(rawValue: string) {
print(font)
}
}