AppleTV app crashes because of a UITableViewCell not providing a parentFocusEnvironment - uitableview

Since Xcode 14 (AppleTV SDK 16.0) and tvOS 16.0, my AppleTV app crashes when I try to call reloadData() on a UITableView after the user selects a cell (the goal is to update the table model and view). The crash states that a UITableViewCell does not have a parentFocusEnvironment:
*** Assertion failure in -[_UIFocusItemInfo _createFocusedRegion], _UIFocusItemInfo.m:187
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Focus item <UITableViewCell: 0x7f92d202fc00> does not provide a parentFocusEnvironment.'
The app creates regular UITableViewCell hence does not override this property so I don't understand why it is missing.
• On tvOS 15.4 (whatever Xcode version 13.4.1 or 14), the app does not crash and the behaviour is as expected. No system warnings or logs.
• On tvOS 16.0 with XCode 13.4.1 (AppleTV SDK 15.0), the app does not crash either but the unfocused view of the selected UITableViewCell stays in the view hierarchy after the reloading. Also a system log warns that the focus item (UITableViewCell) does not define a parentFocusEnvironment and that the app will crash in a future version -> my crash.
[Assert] Focus item <UITableViewCell: 0x7fe4290a7c00> does not provide a parentFocusEnvironment. This will become an assert in a future version.
[Assert] Failed to create FocusRegion for FocusItem: <UITableViewCell: 0x7ff129856600; frame = (0 800; 880 66); text = 'Show empty sections'; autoresize = W; layer = <CALayer: 0x600003a1f740>> with parentFocusEnvironment: (null) focusItemContainer: (null)
I've been able to create a sample project where the crash can be reproduced: https://github.com/aureliencolas/NavAndTableView
I've filed a feedback request to Apple a week ago with the sample project but I've got no response so far.
Any advice on how to refresh the tableView in a more appropriate way is welcome.

After requesting a code-level support, an Apple engineer helped me find the fix.
Turns out I was not recycling UITableView cells, but creating new ones each time (which I don't usually do but, in this context, there were no point in recycling cells) and doing so, the cell's parentFocusEnvironment was not always defined.
As it was working fine before tvOS 16 and cells recycling not being mandatory, I still consider it a TVOS 16 regression but the issue is now fixed and it's what matters.

Update:
The fix below does only work if the focus has been changed using touch input. Once focus has been changed using the external keyboard arrow keys, it still crashes at (with the same stack trace).
Different approach for now is to store the old parentFocusEnvironment and to return that at all times. This satisfies the assertion within the focus update, but I am still unsure of any side effects this might have:
// Custom Cell
#interface Cell : UITableViewCell
// Weak(!) reference to the last focus environment
#property(nonatomic, weak) id<UIFocusEnvironment> lastParentFocusEnvironment;
#end
#implementation Cell
- (id<UIFocusEnvironment>)parentFocusEnvironment
{
// Always use original parent environment as long as it is available
id environment = [super parentFocusEnvironment];
if (environment)
self.lastParentFocusEnvironment = environment;
return self.lastParentFocusEnvironment;
}
#end
I see the same crash on iPadOS when contents of a focussed table view are cleared. The table view correctly removes the focus and informs the delegate in - (void)tableView:(UITableView *)tableView didUpdateFocusInContext:(UITableViewFocusUpdateContext *)context withAnimationCoordinator:(UIFocusAnimationCoordinator *)coordinator . However, the view crashes after that delegate method is invoked, stating that the old view, which had focus before, does not have a parentFocusEnvironment, since it had been removed from the view hierarchy.
The crash occurred in the next layout phase after the table view updated, since focus updates appear to happen during layout. This table already uses cell recycling, so that could not be the solution as mentioned in other answers.
The fix was to remove the selection from the table view before calling reloadData like so (assuming selectionFollowsFocus is set on the table view):
// Gather data..
if (!hasRowToFocus) {
// Remove selection
[self.tableView selectRowAtIndexPath:nil animated:NO scrollPosition:UITableViewScrollPositionNone];
// Update focus while the old focussed view is still visible to avoid the crash
[self.tableView setNeedsFocusUpdate];
[self.tableView updateFocusIfNeeded];
}
// Proceed with table view updates..
[tableView reloadData];
...
I'd also consider this a bug, since the SDKs focus validation ignores the fact that an old view might be removed from the view hierarchy and thus lacking a parentFocusEnvironment.

Related

Crash in UIPageViewController when adding pages

In the iOS 11 app I'm developing (using Swift 4), I need to dynamically add pages to a UIPageViewController. However, in some circumstances, I receive the following crash error (NSInternalInconsistencyException):
2017-11-27 14:55:54.787260+0000 MyApp[380:79434] *** Assertion failure in -[UIPageViewController _flushViewController:animated:], /BuildRoot/Library/Caches/com.apple.xbs/Sources/UIKit/UIKit-3698.21.8/UIPageViewController.m:2124
2017-11-27 14:55:54.791022+0000 MyApp[380:79434] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Don't know about flushed view <UIView: 0x12dd48ac0; frame = (0 0; 320 568); autoresize = W+H; layer = <CALayer: 0x1c003c280>>'
I am very new to iOS development, so I'm sorry if the answer is obvious, but I'm having a hard time finding the root cause for the crash. The logged message isn't being very helpful either...
This is the code I'm using to add pages:
//Inflate the view controller
let viewController = newViewController(param: someParam )
//Add it to the view controllers pool
viewControllersOrdered.insert(viewController, at: getInsertionIndex(param: someOtherParam) )
//Add it to the pageViewController
pageViewController.setViewControllers([viewController], direction: .forward, animated: false, completion: nil)
//Force cache to be cleared
pageViewController.dataSource = nil;
pageViewController.dataSource = self;
The getInsertionIndex function is working correctly, that is not the source of the crash.
In the iOS 11 app I'm developing (using Swift 4), I need to dynamically add pages to a UIPageViewController.
It sounds like what you mean is: you do not know what the next / previous view controller will be until the user actually tries to "turn the page". In that case, a scrolling UIPageViewController is not a good fit for your architecture (because, as you have already figured out, it precaches the next and previous pages). You have two choices:
Use a page-curl UIPageViewController. It doesn't precache its pages, so you are not called upon to make a decision until the user actually asks to turn the page.
Don't use a UIPageViewController at all; use a paging horizontal scroll view and manage the content view yourself.
Also, if you're using a UIPageViewController, in no case should you also be holding a retained list of child view controllers. That is the job of the UIPageViewController. Your job is merely to supply the next or previous view controller, which you should create on demand and at no other time, and release it into the UIPageViewController's sole control.
The main rules of crash-free using UIPageController with scroll transition style:
1) set dataSource before calling setViewControllers method
2) use setViewControllers method without animation (animated: false)
3) set dataSource to nil for single page mode
4) don't allow cycles for 2-page mode
One can find full Swift realisation of a stable subclass at my detailed answer
The initial code and usage examples one can find at GitHub project.

Autolayout issue on iOS6: "All dependent constraints should have been removed from the engine and also from the view's list of dependent constraints"

Has anyone encountered this warning message related with autolayout:
All dependent constraints should have been removed from the engine and also from the view's list of dependent constraints
Currently, we have some footerView with several buttons inside and they are hidden or showed depending on the need. We use full autolayout everywhere. Here's the method for hiding/showing this footerView:
- (void)hideFooterView:(BOOL)shouldHide {
self.containerViewBottomConstraint.constant = shouldHide ? 0 : 50; // expand containing view to fit to full screen OR make it smaller to fit with footerView
[UIView animateWithDuration:1 animations:^{
[self.view layoutIfNeeded]; // animate expanding screen height to full height
[self.footerView setAlpha:(shouldHide ? 0 : 1)];
} completion:nil];
}
So, when this method is called for the first time then there's no error message. But after the second time mentioned warning appears in console. We can't ignore this warning because in other screens we are encountering crashes with private Apple method calls without any clue how to solve that:
[UILayoutContainerView nsis_shouldIntegralizeVariable:]: message sent to deallocated instance
here's another crash message:
[UILayoutContainerView nsis_valueOfVariable:didChangeInEngine:]: message sent to deallocated instance
I was unable to find anything useful related to "nsis_valueOfVariable:didChangeInEngine:" or "Autolayout dependent constraints" keywords on web. Any ideas?
UPDATE Commenting out line "[self.view layoutIfNeeded]" seems fixed the issue but then there will be no animation...
In my project also this issue came. I was getting same warning "All dependent constraints should have been removed from the engine and also from the view's list of dependent constraints".
As you told, when I ignored it and in other screens I got crashes, showing
[UILayoutContainerView nsis_shouldIntegralizeVariable:]: message sent to deallocated instance
I found that, in my case I was calling [self.view layoutIfNeeded] inside -(void)viewWillDisappear:(BOOL)animated. When I removed this out, the issue was fixed.
No warnings and no crashes.
From this what I understood is, if we call layoutIfNeeded just before the view is being deallocated this warning will come. And in next screens it will cause crashing.
It took so much time to figure out this crash. That's why I am sharing my thoughts.
This may help someone.

Monotouch App Crashes when a tableView is removed from it's superview

I have a tableview where each cell contains a textview.
Steps to reproduce the crash.
Write something in the textview of first row (keyboard is open)
Scroll the tableview downwards where the first row is not visible anymore . however, the keyboard is still open
I choose another tab where in code I remove the view which contains the tableview from its superview then the app crashes
Here is the stack trace
at (wrapper managed-to-native) MonoTouch.ObjCRuntime.Messaging.void_objc_msgSendSuper (intptr,intptr) <IL 0x00024, 0xffffffff>
at MonoTouch.UIKit.UIView.RemoveFromSuperview () [0x00031] in /Developer/MonoTouch/Source/monotouch/src/UIKit/UIView.g.cs:848`
i tried to perform a ResignFirstResponder with the same results.
Moreover, if the keyboard is not opened there is no crash
It is only reproduced on IOS 7
The problem is that the UITableViewCells i am creating in my UITableViewSource are being freed by the managed garbage collector, because they're not referenced from managed code when that method exits.
There are two ways to solve this problem:
Keep a list of cells you've created in a class variable in the UITableViewSource class. The problem with this approach is that you have extra bookkeeping to do in order to remove cells from the list when they're no longer used.
Create a custom subclass of UITableViewCell. Custom subclasses are treated differently at runtime in Xamarin.iOS, and we make sure they're not freed until their corresponding native object is freed:
class CustomCell : UITableViewCell
{
public CustomCell(UITableViewCellStyle Style, string Identifier) : base(Style, Identifier) { }
}

Add a UITableView as a subview crash only on simulator iOS

Is there a problem with adding a UITableView as a subview of aUIView? I had a line of code that was working for months, and recently broke after I updated XCode to 4.6.3. Long story short, I was returning TableViewTwo as a subview of a UIView as a footer of TableViewOne. Please don't ask me why this is necessary, it is just convenient for what I am doing. Well, the app would always crash on the simulator, but not on the device. It would give me a bizarre error and an opaque call stack. The error said unrecognized selector(numberOfSections)sent to instance. Yes, the selector was numberOfSections and not the numberOfSectionsInTableView in the UITableViewDataSource. Well, when I returned the UITableViewTwo as UITableViewOne's footer..everything started working. Does anyone know if there is a problem with adding a UITableView as a subview of a UIView? For more information - see this SO Post(link)

Strange calls to modelIdentifierForElementAtIndexPath:inView: (UIDataSourceModelAssociation)

I'm currently implementing automatic state preservation/restoration in an iOS6-only app.
For a restoration of a table view, I added the UIDataSourceModelAssociation protocol to my table view controllers and implemented
- (NSString *)modelIdentifierForElementAtIndexPath:(NSIndexPath *)idx inView:(UIView *)view
and
- (NSIndexPath *)indexPathForElementWithModelIdentifier:(NSString *)identifier inView:(UIView *)view
When pressing the home button, the state preservation methods, including modelIdentifierForElementAtIndexPath:iView:, are getting called as expected and return valid identifier strings for the given index paths.
When killing the app and relaunching it, state restoration works more or less. I.e. the app re-opens the correct table view. However, the table view is always scrolled to the top, even when it was scrolled to another position before.
Here's the implementation of the UIDataSourceModelAssociation methods in my table view controller. Nothing fancy going on in there (the NdlFriend::accountUidproperty returns a unique identifier string for a given NdlFriend record):
#pragma mark - UIDataSourceModelAssociation
- (NSString *)modelIdentifierForElementAtIndexPath:(NSIndexPath *)idx inView:(UIView *)view
{
NSString* identifier = nil;
NSArray* content = self.contentArray;
// Sometimes idx might be nil...
if(idx && idx.row<content.count)
{
NdlFriend* friend = content[idx.row];
identifier=friend.accountUid;
}
return identifier;
}
- (NSIndexPath *)indexPathForElementWithModelIdentifier:(NSString *)identifier inView:(UIView *)view
{
NSIndexPath * indexPath=nil;
NSArray* content = self.contentArray;
NSInteger count = content.count;
for(NSInteger i=0;i<count;i++)
{
NdlFriend* friend = content[i];
if([identifier isEqualToString:friend.accountUid])
{
indexPath = [NSIndexPath indexPathForRow:i inSection:0];
break;
}
}
return indexPath;
}
I set break points in both methods.
To test the methods, I opened the table view and scrolled down a little bit. Then, when pressing the home button:
modelIdentifierForElementAtIndexPath:inView: gets called once, with the index path of the top most visible row. The method returns the correct uid for this row.
So far so good.
Then I stop and relaunch the app. Here's what happens (I'm especially puzzled by the first hit break point):
modelIdentifierForElementAtIndexPath:inView: gets called, with nil as index path (the view argument contains the correct pointer of the table view).
indexPathForElementWithModelIdentifier:inView: gets called with a valid identifier string (and a valid index path is returned by the method).
indexPathForElementWithModelIdentifier:inView: gets called again (with the same identifier string).
The table view is refreshed, but scrolled to the very top.
Does someone know, why the restoration of the scroll position fails? Does maybe the call of modelIdentifierForElementAtIndexPath:inView: with nil as indexPath has something to do with it (or is this normal behavior).
There is a bug in iOS 6 regarding state restoration of Table Views in Navigation Controllers.
You can see the open radar here: rdar://13438788
As you can see, it's a duplicate, so Apple are aware of this.
Also, see this next link, the guy who posted that open radar also did this blog post, it has the suggested workarounds that the Apple Engineers told him.
It makes the state preservation/restoration for me not such an enjoyable feature to implement, although remember, this is for your users ! So you should just do the workaround anyway.
Note there are 2 workarounds, one for the table view's view information to restore (scroll offset for example), and another workaround for the use when implementing UIDataSourceModelAssociation which is your case.
http://useyourloaf.com/blog/2013/04/07/bug-table-view-state-not-restored-when-embedded-in-navigation-controller.html
I don't think the problem you're having of the table view's scroll position resetting has to do with the UIDataSourceModelAssociation methods.
I believe there's a bug with a table view controller embedded in a nav controller, that causes it to reset its scroll position after restoration. As I understand it, if the cells in your table view don't reorder, you don't need to implement the UIDataSourceModelAssociation methods and you should get the scroll position "for free" (i.e. as long as you've opted into state preservation and set the restorations IDs). I can't really confirm this explicitly from the documentation, except to point out that UITableView descends from UIScrollView, which saves its scroll position. I've tested that if you set the table view controller to be the root controller, or embed a table view controller in a tab bar controller, then the scroll position is restored.
I've filed a bug report, you should too, if you haven't already.
I agree with Aky and this might indeed be a bug in iOS 6. See my answer on a related question: https://stackoverflow.com/a/14567551/322548
Make sure that you're not performing an asynchronous fetch on your data. If you are fetching your data from viewDidLoad, make sure that you use a [myManagedObjectContext performBlockAndWait:^{}] call instead of a [myManagedObjectContext performBlock:^{}].
It may be that you have a race condition where self.contentArray is empty when indexPathForElementWithModelIdentifier gets called.

Resources