Kevin Renskers
Kevin Renskers

Reputation: 5950

SwiftUI crash: "precondition failure: attribute failed to set an initial value: 71”

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

Answers (4)

Kevin Renskers
Kevin Renskers

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

ingoem
ingoem

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

Kevin Renskers
Kevin Renskers

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

Asperi
Asperi

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

Related Questions