Crash when using UIView.animateKeyframes - NSInternalInconsistencyException: You must provide a value (key=contents)

Hello, I am looking for some help today about a crash that randomly appeared for no explicit reason. A bit of context, I have 2 apps/targets within the project and the only thing that changes between the two is pretty much the API and the colors/logos.

I have published an update for both apps, the related code hasn't changed since the previous versions (it has been in production for 6 months already), and it's only crashing in the same app, not in the other one somehow. It happens on multiple iOS versions and devices as well. Of course, I am unable to reproduce the crash.

I am using Firebase Crashlytics, which gives me this error message :

Fatal Exception: NSInternalInconsistencyException - You must provide a value (key=contents)

And in the Xcode Organizer, the highlighted stack trace line responsible for the crash is the following :

3  UIKitCore  0x1c2ad5970  -[_UIViewDeferredAnimation addAnimationFrameForValue:] + 216 (UIView.m:2792)

About the code, I have a custom searchBar and a cancelButton embedded in a UIStackView, I am animating the cancelButton visibility using .isHidden to replicate the experience you get when using a UISearchController in the navigationBar.

About the crash, it seems to be triggered when animating keyframes using UIView.animateKeyframes, with multiple UIView.addKeyframe within the animation block, at least that's why I understand from the stack trace.

The code is really simple, when the searchBarTextField becomes the first responder and begins editing, I animate the cancelButton in. The cancelButtonAnimationOptions are the followings : .curveEaseInOut, .beginFromCurrentState and .allowUserInteraction.

func textFieldDidBeginEditing(_ textField: UITextField) {
  showSearchBarCancelButton()
}
func showSearchBarCancelButton() {
  let duration = Layout.animationDuration
  let options = Layout.cancelButtonAnimationOptions

  UIView.animateKeyframes(withDuration: duration, delay: 0, options: options, animations: {
    UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 1, animations: {
      self.cancelButton.isHidden = false
      self.cancelButton.superview?.layoutIfNeeded()
    })
    UIView.addKeyframe(withRelativeStartTime: 0.7, relativeDuration: 0.3, animations: {
      self.cancelButton.alpha = 1
    })
  })
}

Here is a preview of the stack trace :
(Check Log.crash in the attachments for the full version) :

Exception Type:  EXC_CRASH (SIGABRT)
Exception Codes: 0x0000000000000000, 0x0000000000000000
Triggered by Thread:  0

Last Exception Backtrace:
0   CoreFoundation                	0x1c057de88 __exceptionPreprocess + 164 (NSException.m:202)
1   libobjc.A.dylib               	0x1b98ab8d8 objc_exception_throw + 60 (objc-exception.mm:356)
2   Foundation                    	0x1bae99b4c -[NSAssertionHandler handleFailureInMethod:object:file:lineNumber:description:] + 188 (NSException.m:242)
3   UIKitCore                     	0x1c2ad5970 -[_UIViewDeferredAnimation addAnimationFrameForValue:] + 216 (UIView.m:2792)
4   UIKitCore                     	0x1c2ad5208 -[_UIViewDeferredKeyframeAnimation addAnimationFrameForValue:] + 72 (UIView.m:2901)
5   UIKitCore                     	0x1c2ad5148 __93-[UIViewKeyframeAnimationState addKeyframeWithRelativeStartTime:relativeDuration:animations:]_block_invoke + 368 (UIView.m:2651)
6   CoreFoundation                	0x1c0587588 __NSDICTIONARY_IS_CALLING_OUT_TO_A_BLOCK__ + 24 (NSDictionaryHelpers.m:10)
7   CoreFoundation                	0x1c0611bc0 -[__NSDictionaryM enumerateKeysAndObjectsWithOptions:usingBlock:] + 212 (NSDictionaryM_Common.h:311)
8   UIKitCore                     	0x1c2b636c8 -[UIViewKeyframeAnimationState addKeyframeWithRelativeStartTime:relativeDuration:animations:] + 156 (UIView.m:2638)
9   UIKitCore                     	0x1c2b635dc +[UIView(UIViewKeyframeAnimations) addKeyframeWithRelativeStartTime:relativeDuration:animations:] + 132 (UIView.m:15468)
10  MyApp                   	    0x102b657f8 closure #1 in FavoritesViewController.showSearchBarCancelButton() + 196 (FavoritesViewController+Search.swift:45)
11  MyApp                   	    0x102c04198 thunk for @escaping @callee_guaranteed () -> () + 28 (<compiler-generated>:0)
12  UIKitCore                     	0x1c28011e4 +[UIView _setupAnimationWithDuration:delay:view:options:factory:animations:start:animationStateGenerator:completion:] + 512 (UIView.m:15213)
13  UIKitCore                     	0x1c292e140 +[UIView(UIViewKeyframeAnimations) animateKeyframesWithDuration:delay:options:animations:completion:] + 156 (UIView.m:15458)
14  MyApp                   	    0x102b655a4 FavoritesViewController.showSearchBarCancelButton() + 44 (FavoritesViewController+Search.swift:44)
15  MyApp                   	    0x102b655a4 FavoritesViewController.textFieldDidBeginEditing(_:) + 44 (FavoritesViewController+Search.swift:12)
16  MyApp                   	    0x102b655a4 @objc FavoritesViewController.textFieldDidBeginEditing(_:) + 224 (<compiler-generated>:11)
17  UIKitCore                     	0x1c2c949c8 -[UITextField _notifyDidBeginEditing] + 104 (UITextField.m:1910)
18  UIKitCore                     	0x1c2952554 -[UITextField _becomeFirstResponder] + 196 (UITextField.m:1842)
19  UIKitCore                     	0x1c294ffa4 -[UIResponder becomeFirstResponder] + 516 (UIResponder.m:392)
20  UIKitCore                     	0x1c294fd90 -[UIView(Hierarchy) becomeFirstResponder] + 120 (UIView.m:12379)
21  UIKitCore                     	0x1c2803bc4 -[UITextField becomeFirstResponder] + 152 (UITextField.m:1496)
22  MyApp                   	    0x102ab6840 CustomSearchBar.onSearchBarTap(_:) + 36 (CustomSearchBar.swift:36)
23  MyApp                   	    0x102ab6840 @objc CustomSearchBar.onSearchBarTap(_:) + 88 (<compiler-generated>:35)

Thanks for your help!

Something is trying to create an animation for the CALayer.contents, and in this case since contents is likely nil on whatever view is being animated its tripping over an assumption in the keyframe animation tracking that the value provided will not be nil. Since only specific views will animate contents it's unusual that a tracking data structure is even being created for that key.

We would certainly appreciate a feedback request with all the information you have on hand.

Thanks for your reply, the error message definitely makes sense now.

What you will find below :

  • The code of the ViewController where the crash is occurring (I have omitted styling and data related code)
  • The code of the CustomSearchBar and a preview of its view hierarchy from the Interface Builder
  • The complete stack trace (Incident.crash)

Some explanation regarding the controller properties :

  • extendedNavBarContainerView: vertical UIStackView used to expand the navigationBar, it contains the searchContainerView.
  • searchContainerView: horizontal UIStackView containing the searchBar and the cancelButton.

The extendedNavBarContainerView is placed below the navigationBar with the help of the additionalSafeAreaInsets property while being constrained to the top of the safe area. In my code, it also contains another view in addition to the searchContainerView (Could the crash be caused from this additional view ? I don't think so since I'm calling layoutIfNeeded() on the cancelButton superview, which is searchContainerView). Initially, the extendedNavBarContainerView is hidden, and is animated in with the method showExtendedNavBar() when the data has loaded, allowing the user to search through. Next, the cancelButton is animated in when the searchBarTextField becomes the first responder and begins editing, and is animated out on tap.

Let me know if I missed something and you would like further information.

class ViewController: UICollectionViewController {

  // MARK: - Properties
  lazy var searchBar = CustomSearchBar.loadFromNib()
  lazy var cancelButton = UIButton(type: .system)
  lazy var searchContainerView = UIStackView()
  lazy var extendedNavBarContainerView = UIStackView()

  // MARK: - Lifecycle
  override func viewDidLoad() {
		super.viewDidLoad()
		setupSearchBar()
		setupNavigationBar()
	}

	// MARK: - Setup
	private func setupSearchBar() {
		searchBar.searchTextField.delegate = self

		cancelButton.alpha = 0
		cancelButton.isHidden = true
		cancelButton.setContentHuggingPriority(.required, for: .horizontal)

		searchContainerView.axis = .horizontal
		searchContainerView.alignment = .center
		searchContainerView.distribution = .fill
		searchContainerView.insetsLayoutMarginsFromSafeArea = false
		searchContainerView.isLayoutMarginsRelativeArrangement = true

		searchContainerView.addArrangedSubview(searchBar)
		searchContainerView.addArrangedSubview(cancelButton)
  }

	private func setupNavigationBar() {
		extendedNavBarContainerView.isHidden = true
		extendedNavBarContainerView.axis = .vertical
		extendedNavBarContainerView.alignment = .fill
		extendedNavBarContainerView.distribution = .fill
		extendedNavBarContainerView.insetsLayoutMarginsFromSafeArea = false
		extendedNavBarContainerView.isLayoutMarginsRelativeArrangement = true
		extendedNavBarContainerView.translatesAutoresizingMaskIntoConstraints = false

		extendedNavBarContainerView.addArrangedSubview(searchContainerView)
		view.addSubview(extendedNavBarContainerView)

		NSLayoutConstraint.activate([
			extendedNavBarContainerView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
			extendedNavBarContainerView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
			extendedNavBarContainerView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor)
		])
	}

	// MARK: - Actions
	func showExtendedNavBar() {
		let itemsHeight = [searchContainerHeight]
		let extendedNavBarHeight = extendedNavBarHeight(with: itemsHeight)
		extendedNavBarContainerView.isHidden = false

		UIView.animate(withDuration: Layout.animationDuration) {
			self.additionalSafeAreaInsets.top = extendedNavBarHeight
			self.view.layoutIfNeeded()
		}
	}

	func hideExtendedNavBar() {
			UIView.animate(withDuration: Layout.animationDuration) {
				self.additionalSafeAreaInsets.top = 0
				self.view.layoutIfNeeded()
			} completion: { _ in
				self.extendedNavBarContainerView.isHidden = true
			}
	}

	// MARK: - Search
	func showSearchBarCancelButton() {
		let duration = Layout.animationDuration
		let options = Layout.cancelButtonAnimationOptions

		UIView.animateKeyframes(withDuration: duration, delay: 0, options: options, animations: {
			UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 1, animations: {
				self.cancelButton.isHidden = false
				self.cancelButton.superview?.layoutIfNeeded()
			})
			UIView.addKeyframe(withRelativeStartTime: 0.7, relativeDuration: 0.3, animations: {
				self.cancelButton.alpha = 1
			})
		})
	}

	func hideSearchBarCancelButton() {
		let duration = Layout.animationDuration
		let options = Layout.cancelButtonAnimationOptions

		UIView.animateKeyframes(withDuration: duration, delay: 0, options: options, animations: {
			UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 1, animations: {
				self.cancelButton.isHidden = true
				self.cancelButton.superview?.layoutIfNeeded()
			})
			UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 0.3, animations: {
				self.cancelButton.alpha = 0
			})
		})
	}

	// MARK: - Delegate
	func textFieldDidBeginEditing(_ textField: UITextField) {
		showSearchBarCancelButton()
	}

	func textFieldShouldReturn(_ textField: UITextField) -> Bool {
		textField.resignFirstResponder()
		return false
	}
}

CustomSearchBar

class CustomSearchBar: UIView {

  // MARK: - IBOutlets
  @IBOutlet weak var backgroundView: UIView!
  @IBOutlet weak var searchTextField: UITextField!

  // MARK: - Properties
  override var intrinsicContentSize: CGSize {
    let inset = UIEdgeInsets(insets: [layoutMargins, backgroundView.layoutMargins])
    let height = inset.vertical + searchTextField.intrinsicContentSize.height
    return .init(width: UIView.noIntrinsicMetric, height: height)
  }

  // MARK: - Lifecycle
  override func awakeFromNib() {
    super.awakeFromNib()
    setupView()
  }

  // MARK: - Setup
  private func setupView() {
    backgroundView.layer.cornerRadius = 10
  }

  // MARK: - Actions
  @IBAction func onSearchBarTap(_ sender: Any) {
    searchTextField.becomeFirstResponder()
  }
}

Thanks again for your help!

There seems to be a fair amount of missing setup and some missing extensions – I cut back what I could to get this to compile but couldn't reproduce the issue. Given what is happening in your keyframe animation, my immediate suspicion would be something about UIButton, but I don't have a ton of context to provide there.

It would help to file a feedback request with a complete project (post the FB number here and I can track it earlier) just so that we can both be sure we are looking at the same thing.

Thanks!

My bad, didn't think you wanted to run the code.
I could throw an app update while changing the button type from system to custom and see if it helps.

I have filed a feedback request at https://bugreport.apple.com, the number is FB13593988
I have also uploaded a project on Github at https://github.com/Reqven/iOS-Crash-UIView.animateKeyframes

Let me know if there is anything else I can do.
Thanks!

Crash when using UIView.animateKeyframes - NSInternalInconsistencyException: You must provide a value (key=contents)
 
 
Q