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
AppFontthat enters an infinite loop because in the propertyrawValuethe function call.encode(self)callsself.rawValuebecause you have declared thatselfconforms toRawRepresentableso the encoder wants to encode the raw value of the enum case.Note what it says in the article
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
switchwhen converting to/from a stringI didn't include SWiftUI or UserDefaults when examining this, instead this was my test code