July 2, 2020
Update (2/23/2020): In addition to the issue this post originally described, I've reported another, similar issue for SwiftUI in AppKit. See the bottom of this post for more info.
Update (11/14/2020): I've added some more symbols I've seen in crashes with similar causes, which hopefully will be indexed by Google and help developers facing similar issues. My go-to strategy for fixing internal SwiftUI crashes (especially on macOS) has been "Is something not Equatable? Make it Equatable."
I've been working lately on rewriting some of CIFilter.io in SwiftUI. One crash I came across that initially stumped me would happen when resetting my UIHostingController
's rootView
to a view that was equivalent to the one already set (mostly, this happened when transitioning the window from full to split-screen mode). The crash only had one stackframe that started with AG::LayoutDescriptor::compare
:
#0 0x00007fff4a9b6069 in AG::LayoutDescriptor::compare(unsigned char const*, unsigned char const*, unsigned char const*, unsigned long, unsigned int) ()
Initially I had no idea what was going on here, but I found two clues. One was this tweet:
Looks like Alexey Demedeckiy ran into this a while back, and the solution was to make an enum with many cases conform to Equatable
. I realized that I also had an enum (FilterParameterType
) with 20+ cases which I was now using as part of the SwiftUI view state.
The second clue was that the crashing method was called compare
: my enum wasn't equatable, but it seems like SwiftUI was doing something under the hood to try to compare it anyways.
I spent a bit of time creating a sample project and whitling down my code into a minimal reproduction case, and I realized that the following triggers the same crash:
enum Type {
case one(info: String)
case two(info: String)
case three(info: String)
case four(info: String)
case five(info: String)
case six(info: String)
case seven(info: String)
case eight(info: String)
case nine(info: String)
case ten(info: String)
}
struct ContentView: View {
@State var type: Type = .one(info: "one")
var body: some View {
VStack(alignment: .leading) {
Button(action: {
self.type = .one(info: "one")
}, label: {
Text("Tap here to crash")
})
}
}
}
Tapping that button (twice, for some reason?) crashes the app!
This was a super, super interesting bug to find. Even more interesting is that the crash doesn't happen if you do any of the following:
Equatable
From what I can tell, SwiftUI's trying hard to compare the enum, even if it's not equatable, but I guess 10 cases is the point at which it gives up.
The takeaway: if you use enums as part of your SwiftUI view state, make them Equatable.
A test project which reproduces this issue is available at NGSwiftUIEquatableCrashExample. I've filed this issue with Apple as FB7847416.
About a month after this issue, I noticed another, similar issue. It seems a similar crash can happen in SwiftUI running on top of AppKit (in macOS apps) with the following code:
struct NormalExperienceState {
var one: String?
var two: String?
var three: String?
var four: String?
var five: String?
var six: String?
var seven: String?
var eight: String?
var nine: String?
var ten: String?
var eleven: String?
var twelve: String?
var thirteen: String?
}
struct ContentView: View {
@State var state = NormalExperienceState()
var body: some View {
Text("Hello, world!").padding()
}
}
The crash seems to happen most frequently in objc_release
, StateStorageBox
, swift_initStructMetadata
, or outlined copy of
various SwiftUI structs. (Putting these keywords here in case someone is googling and needs to find them.) Similarly, making the struct conform correctly to Equatable
solves the problem.
I've created another sample project (with linked feedback number) for this.
I'm Noah, a software developer based in the San Francisco Bay Area. I focus mainly on full stack web and iOS development