Intermittent crash when presenting SwiftUI sheet with UIPagingViewController

Hello, I attached a crash report to this post I've received from a handful of users.

The stack-trace in the crash report doesn't point to any code in the app, rather, it indicates something happened in UIKitCore related to in progress animation state causing the app to crash.

I was able to track down where the crash is occurring via events tracked in the app. It appears to be happening when a sheet is presented. The sheet has a UIViewControllerRepresentable containing a UIViewUIPageViewController. The page view controller has three pages, each with a UIViewRepresentable containing a UITextView. The purpose of using a UITextView on these pages is to make the keyboard first responder as soon as the page changes.

I was unable to recreate this crash on any of my test devices, so I was wondering if someone has any insight into what might be causing this crash.

Below are some code snippets showing the implementation of each of the before-mentioned views.

Paging View

struct PageViewController: UIViewControllerRepresentable {
  var controllers: [UIViewController]
  @Binding var currPage: Int
  @Binding var pageDirection: UIPageViewController.NavigationDirection
  @Binding var isPageChanging: Bool
   
  func makeCoordinator() -> Coordinator {
    Coordinator(self)
  }
   
  func makeUIViewController(context: Context) -> UIPageViewController {
    let pageViewController = UIPageViewController(transitionStyle: .scroll,
                           navigationOrientation: .horizontal)
     
    if let scrollView = pageViewController.view.subviews.first(where: { $0 is UIScrollView }) as? UIScrollView {
      scrollView.delegate = context.coordinator
    }
     
    pageViewController.setViewControllers([controllers[currPage]],
                       direction: pageDirection,
                       animated: true)
     
    return pageViewController
  }
   
  func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
    if isPageChanging {
      // Allow time for the keyboard to be dismissed
      DispatchQueue.main.asyncAfter(deadline: .now() + 0.85) {
        pageViewController.setViewControllers([controllers[currPage]],
                           direction: pageDirection,
                           animated: true)
      }
    }
  }
   
  class Coordinator: NSObject, UIScrollViewDelegate {
    var parent: PageViewController
     
    init(_ pageViewController: PageViewController) {
      self.parent = pageViewController
    }
     
    func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
      DispatchQueue.main.async { self.parent.isPageChanging = false }
    }
  }
}

Example of view used in Paging View

struct SamplePageInputView: View {
  @Binding var isPageChanging: Bool
  @Binding var currPage: Int
     
  @State private var text = ""

   var submitCallback: () -> Void
   
  var body: some View {
    GeometryReader { _ in
      VStack {
        Text("Title")
          .font(.custom(Fonts.brandHeading.swiftUIFont, size: 32))
          .foregroundColor(Color(ColorPalette.secondaryTint))
          .padding(.bottom, 8)
         
        Text("Subtitle")
          .font(.custom(Fonts.brandBold.swiftUIFont, size: 18))
          .foregroundColor(Color(ColorPalette.secondaryTint))
          .padding(.bottom, 16)
         
          FocusablePageableTextField(text: $text,
                        isFirstResponder: true,
                        placeholder: "placeholder",
                        textAlignment: .center,
                        autocapitalizationType: .words,
                        textContentType: .name,
                        autocorrectionType: .no,
                        activePage: 0,
                        isPageChanging: $isPageChanging,
                        currPage: $currPage,
                        showSpinner: .constant(false),
                        onReturnCallback: submit,
                        onButtonPressCallback: submit)
            .frame(height: 40)
            .padding([.leading, .trailing], 36)
            .padding(.bottom, 8)
            .disabled(isPageChanging || currPage != 0)
      }
    }
  }
   
   private func submit() {
    // ....
     
    submitCallback()
  }
}

See comment below for remaining view code:

Discard this comment...

Code continued...

Focusable Text Field

/**
  Supports first responder and pageable state, so the keyboard only
  shows after the page view has fully transitioned.
 */
struct FocusablePageableTextField: UIViewRepresentable {
  @Binding var text: String
  var isFirstResponder: Bool = false
  var placeholder: String = ""
  var textAlignment: NSTextAlignment = .left
  var autocapitalizationType: UITextAutocapitalizationType = .sentences
  var keybaordType: UIKeyboardType = .default
  var textContentType: UITextContentType? = nil
  var autocorrectionType: UITextAutocorrectionType = .yes
  var activePage: Int
  @Binding var isPageChanging: Bool
  @Binding var currPage: Int
  @Binding var showSpinner: Bool
  var onReturnCallback: () -> Void
  var onButtonPressCallback: () -> Void
   
  // Optional callback for when text field is updated
  var onUpdateCallback: (() -> Void)? = nil
   
  func makeUIView(context: UIViewRepresentableContext<FocusablePageableTextField>) -> UITextField {
    let textField = UITextField(frame: .zero)
    textField.placeholder = placeholder
    textField.textAlignment = textAlignment
    textField.autocapitalizationType = autocapitalizationType
    textField.keyboardType = keybaordType
    textField.textContentType = textContentType == nil ? .none : textContentType!
    textField.autocorrectionType = autocorrectionType
     
    // Prevents the textfield from growing when text exceeds its bounds
    textField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
     
    // Style
    textField.layer.cornerRadius = 20
    textField.layer.borderWidth = 2
    textField.layer.borderColor = ColorPalette.primaryTint.cgColor
     
    textField.font = UIFont(name: Fonts.brandBold.uiktFont, size: UIScreen.main.bounds.height > 812 ? 18 : 14)
    textField.textColor = ColorPalette.secondaryTint
     
    textField.rightView = UIView()
    textField.rightViewMode = .always
    textField.rightView?.translatesAutoresizingMaskIntoConstraints = false
    textField.rightView?.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.height > 568 ? 35 : 30).isActive = true
    textField.addPadding(.left(UIScreen.main.bounds.height > 568 ? 35 : 30))
     
    textField.rightView?.addSubview(context.coordinator.continueButton)
    textField.rightView?.addConstraintsWithFormat(format: "H:|[v0]|", views: context.coordinator.continueButton)
    textField.rightView?.addConstraintsWithFormat(format: "V:|[v0]|", views: context.coordinator.continueButton)
     
    textField.delegate = context.coordinator
    context.coordinator.continueButton.addTarget(context.coordinator, action: #selector(context.coordinator.buttonCallback), for: .touchUpInside)
    return textField
  }

  func makeCoordinator() -> FocusablePageableTextField.Coordinator {
    return Coordinator(self)
  }

  func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext<FocusablePageableTextField>) {
    uiView.text = text
    DispatchQueue.main.async { self.onUpdateCallback?() }
    if !isPageChanging && currPage == activePage && isFirstResponder && !context.coordinator.didBecomeFirstResponder {
      DispatchQueue.main.async {
        uiView.becomeFirstResponder()
      }
      context.coordinator.didBecomeFirstResponder = true
    }

    if isPageChanging {
      uiView.endEditing(true)
      context.coordinator.didBecomeFirstResponder = false
    }
  }
   
  class Coordinator: NSObject, UITextFieldDelegate {
    private let parent: FocusablePageableTextField
    var didBecomeFirstResponder = false

    init(_ parent: FocusablePageableTextField) {
      self.parent = parent
    }

    func textFieldDidChangeSelection(_ textField: UITextField) {
      DispatchQueue.main.async { [weak self] in
        guard let self = self else { return }
         
        self.parent.text = textField.text ?? ""
         
      }
    }
     
    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
      parent.onReturnCallback()
      return true
    }
     
    @objc func buttonCallback(sender: UIButton) {
      parent.onButtonPressCallback()
    }
     
    // View components
    let continueButton: UIButton = {
      let button = UIButton(type: .custom)
      button.setImage(UIImage(named: "Icon-Chevron-Blue"), for: .normal)
      button.imageView?.contentMode = .scaleAspectFill
      button.translatesAutoresizingMaskIntoConstraints = false
       
      return button
    }()
  }
}

View containing Paging View (Presented by the Sheet)

struct PagesView: View {     
  @State private var currPage = 0
  @State private var pageDirection: UIPageViewController.NavigationDirection = .forward
  @State private var isPageChanging = false
     
  var body: some View {
    VStack {
      PageControlView(numberOfPages: 3, currentPage: $currPage) // UIPageControl
      PageViewController(controllers: [
        AnyView(SamplePageInputView(isPageChanging: $isPageChanging,
                       currPage: $currPage,
                       submitCallback: nextPage)),
         
        AnyView(SamplePageInputView(isPageChanging: $isPageChanging,
                      currPage: $currPage,
                      submitCallback: nextPage)),
         
        AnyView(SamplePageInputView(isPageChanging: $isPageChanging,
                      currPage: $currPage,
                      submitCallback: nextPage))]
         
        .map { UIHostingController(rootView: $0)},
                currPage: $currPage, pageDirection: $pageDirection, isPageChanging: $isPageChanging)
    }
  }
   
  private func nextPage() {
    if (!isPageChanging) {
      pageDirection = .forward
      currPage += 1
      isPageChanging = true
    }
  }
   
  private func previousPage() {
    if (!isPageChanging) {
      pageDirection = .reverse
      currPage -= 1
      isPageChanging = true
    }
  }
}
Intermittent crash when presenting SwiftUI sheet with UIPagingViewController
 
 
Q