Reputation: 5950
I have a very interesting crash, that only happens under very specific circumstances. I've already submitted a bug report to Apple, but maybe someone here has seen a similar crash, knows what's going on, and knows a workaround?
A minimal project showing the crash can be found at https://github.com/kevinrenskers/SwiftUICrash but I've also added the related code below. The project has 3 views: RootView
, DetailsView
and ListView
. RootView
embeds either the DetailsView
or the ListView
.
The crash happens when you press the trailing navigation bar button in DetailsView
to switch back to the ListView
. The app crashes with the error "precondition failure: attribute failed to set an initial value: 71”.
When you use the Button
in the middle of the screen to switch back to the ListView
however, the crash does NOT happen. And when you remove the .resizable()
modifier from the background image, the crash also does NOT happen.
Also, if you change Group
into NavigationView
inside of RootView
, the app doesn't crash. Sadly that's not an option for my real-world app though.
import SwiftUI
final class AppStore: ObservableObject {
@Published var showingDetails = true
}
struct RootView: View {
@EnvironmentObject private var store: AppStore
var body: some View {
Group {
if store.showingDetails {
DetailsView()
} else {
ListView()
}
}
}
}
struct DetailsView: View {
@EnvironmentObject private var store: AppStore
var body: some View {
NavigationView {
ZStack {
GeometryReader { geo in
Image("bg")
.resizable()
.aspectRatio(contentMode: .fill)
.edgesIgnoringSafeArea(.all)
.frame(width: geo.size.width, height: geo.size.height)
}
Button("List") {
self.store.showingDetails = false // <- this works fine
}
.padding(20)
.background(Color.white)
}
.navigationBarTitle(Text("Details"))
.navigationBarItems(trailing: trailingNavigationBarItem)
}
}
private var trailingNavigationBarItem: some View {
Button("List") {
self.store.showingDetails = false // <- this crashes the app!
}
}
}
struct ListView: View {
@EnvironmentObject private var store: AppStore
var body: some View {
NavigationView {
Button("Load details") {
self.store.showingDetails = true
}
.padding(20)
.background(Color.white)
.navigationBarTitle("List")
}
}
}
Upvotes: 4
Views: 3971
Reputation: 5950
In the end the fix that works is to use a custom UIImageView via UIViewRepresentable.
struct CustomImage: UIViewRepresentable {
var image: UIImage
var frame: CGRect
func makeUIView(context: Context) -> UIView {
let imageView = UIImageView(frame: frame)
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.image = image
let view = UIView(frame: frame)
view.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(imageView)
return view
}
func updateUIView(_ uiView: UIView, context: Context) {
}
}
Which is then used like this:
GeometryReader { geo in
CustomImage(image: UIImage(named: "bg")!, frame: CGRect(x: 0, y: 0, width: geo.size.width, height: geo.size.height))
}
.edgesIgnoringSafeArea(.all)
See also https://github.com/kevinrenskers/SwiftUICrash/tree/workarounds/CustomImage.
Upvotes: 0
Reputation: 435
Try replacing the group in RootView
with a @ViewBuilder
annotation:
struct RootView: View {
@EnvironmentObject private var store: AppStore
@ViewBuilder
var body: some View {
if store.showingDetails {
DetailsView()
} else {
ListView()
}
}
}
I am not sure how reliable that is in general. I had mixed success inserting @ViewBuilder
annotations in the past, but this seems to fix the issue with the nested NavigationView
.
Upvotes: 4
Reputation: 5950
Edit: this workaround causes problems with iPad's split navigationview. See my other answer for a better workaround.
A workaround is to wrap the RootView
's Group
in a NavigationView
, with a hidden navigationbar (every nested view can potentially have its own navigationbar, not all of them have one):
struct RootView: View {
@EnvironmentObject private var store: AppStore
var body: some View {
NavigationView {
Group {
if store.showingDetails != nil {
DetailsView(bg: store.showingDetails!)
} else {
ListView()
}
}
.navigationBarHidden(true)
.navigationBarTitle("")
}
}
}
The crash is still very very weird though.
Upvotes: 1
Reputation: 258285
Here is alternate workaround (actually it is just avoiding possibility of this issue), and as it was tested has no undesirable side-effects. Just for consideration...
The idea is not to remove DetailsView
, but make it explicitly inactive & hidden. Tested with Xcode 11.2 / iOS 13.2 with no crash.
struct RootView: View {
@EnvironmentObject private var store: AppStore
var body: some View {
ZStack {
ListView()
.zIndex(store.showingDetails ? 0 : 1) // << bring to front
DetailsView()
.opacity(store.showingDetails ? 1 : 0) // << hide
.disabled(!store.showingDetails) // << deactivate
}
}
}
No changes in other views.
Upvotes: 1