UITableViewAlertForLayoutOutsideViewHierarchy

Hi i get an error


[TableView] Warning once only: UITableView was told to layout its visible cells and other contents without being in the view hierarchy (the table view or one of its superviews has not been added to a window). This may cause bugs by forcing views inside the table view to load and perform layout without accurate information (e.g. table view bounds, trait collection, layout margins, safe area insets, etc), and will also cause unnecessary performance overhead due to extra layout passes. Make a symbolic breakpoint at UITableViewAlertForLayoutOutsideViewHierarchy to catch this in the debugger and see what caused this to occur, so you can avoid this action altogether if possible, or defer it until the table view has been added to a window. Table view: <UITableView: 0x102094800; frame = (0 0; 320 568); clipsToBounds = YES; autoresize = W+H; gestureRecognizers = <NSArray: 0x282dac3f0>; layer = <CALayer: 0x2823fb9c0>; contentOffset: {0, -64}; contentSize: {320, 248}; adjustedContentInset: {64, 0, 49, 0}; dataSource: <IOUUOMe.UOMeViewController: 0x10140a340>>


And really the question is what is this error stating and how do i fix it. This happens when i tap an UIBarButton on an Navigation Bar. I have put an symbolic breakpoint which is how i found the button that was causing the error but how do i get rid of this error. and fix it properly

Replies

Couuld you the code of the button action as well as the code related to tableView ?


Is the tableCiew created programmatically or in IB ?

I have the same problem. I think it has something to do with UISplitViewController in combination with UINavigationController.


I've created a very basic sample that fires this error/warning whe the device is rotated. The sample is like the Master-Dateil App in Xcode 10 and iOS 12. You can find the sample here: https://github.com/stsandro/TableViewSample


I have a more complex app, where the error is shown everytime I move back from detail to master. It complaints about the table view in the detail screen, although it is moving away from this view controller. The detail view controller's viewWillLayoutSubviews() method when the view is poped from the navigation controller.


The error also pops up rotating the device when the detail view is shown.

I'm also getting this error with SwiftUI Pickers in a Form. The error occurs when the user selects a choice in the presented choices table. However, the error only occurs on my physical device (iPhone 8) running iOS 13.1 beta: it doesn't occur with iOS 13.0 in Simulators. It therefore looks to be an iOS bug. Have you submitted a report?


Regards,


Michaela

This alert is to help you avoid updating the view when it isn't visible, but I have found a case where it happens unavoidably:


Easy to recreate this bug. On Xcode 11 create a Master/Detail template app, Objc with Core Data. Run on iPhone XR Simulator, rotate to landscape, it throws the error.


The table is in the window during the rotate and split separates so it shouldn't be happening, it's likely due to iOS 13's new panel based split implementation and/or the XR/XS Max/11 Pro Max split view overlay mode.


I submitted this to Feedback Assistant FB7306484

I understand what they are saying. Okay you're reloading the table view while it is offscreen so you may be wasting system resources. Say you have a tab bar controller and an offscreen tab bar with a table view that syncs with core data.


THe current selected tab could make a change that affects the offscreen table view controller, which would cause the nsfetchedresultscontroller to notify the offscreen vc about the change, and the table view will update its state. Do they really think it is worth the effort to check if you are on screen every time you respond to a change in the model? That sounds crazy to me. Think of the code duplication for every table view controller, for every developer, for every app.


They can defer the change in the UITableView itself if they get calls to insert or remove rows while it is offscreen. At least it's all done in one place and not duplicated across millions of apps.

I'm getting this warning but not as the result of selecting anything in the app. I don't have a UISplitViewController nor any UINavigationController up at the time.


I run my app in the iPad simulator, double-click home to go to settings, switch from dark to light or light to dark, then as I'm coming back into my app I get this warning several times.


I followed the instructions to set a symbolic breakpoint, but it's useless since the actual refresh of the table view happens after a delay so none of my code is in the call stack.


I then set a breakpoint on [UITableView reloadData] and after a lot of clicking on continue managed to find a few of my calls that were being done at suspicious times during the UITableViewController life cycle. I wrapped all those in a test to make sure that self.tableView.window was non-nil, and that eliminated the problem.


... until I rotated the device and tried it in landscape. Now I'm getting the warning again. I have now wrapped ALL my reloadData calls with code that tests self.tableView.window and I've set breakpoints on ALL attempts to call reloadData when the tableView has no window. NONE of my breakpoints fire but I get the warning message when coming back from switching to/from dark mode.


So unless this warning can fire as a result of calls other than reloadData, I have to believe this is something iOS is doing internally with my table views. I'm tempted to ignore it at this point.

This whole thing feels like a premature optimization. This is a pretty absurd demand to put on developers to do this all over the place:


self.dataSource removeObjectsInArray:someArray];

if (self.tableView.window != nil)
{
   [self.tableView reloadData];
}
else
{
    self.reloadOnViewDidAppear = YES;
}


//somewhere else
if (tableView.window != nil)
{
    [tableView deleteSections:self.removedSections withRowAnimation:animateOption];
}
else
{
   self.reloadOnViewDidAppear = YES;
}



As I mentioned in my previous post, if you are syncing with some kind of dynamic datasource (syncing with Core data, iCloud, ect) where multiple places in your app make changes this really can get out of hand. If they want to defer table view layout until the table view is on screen they should do it in UITableView's implementation if they believe this will result in significant performance gains. I'm ignoring for now. If they decide to make this a crasher in the future I'll reconsider but I think they'll burn the entire house down if they do that.

Kind of busy right now, but if someone has the time can you test using a simple table view subclass like below and see if it gets rid of the warning?


@interface MyTableViewBiteMe()

@property (nonatomic) BOOL needsReloadWhenPutOnScreen;

@end

@implementation MyTableViewBiteMe

-(void)didMoveToWindow
{
    [super didMoveToWindow];
    if (self.window != nil)
    {
        if (self.needsReloadWhenPutOnScreen)
        {
            NSLog(@"Got dirtied while offscreen. reload.");
            self.needsReloadWhenPutOnScreen = NO;
            [self reloadData];
        }
       
    }
}

// Allows multiple insert/delete/reload/move calls to be animated simultaneously. Nestable.
-(void)performBatchUpdates:(void (NS_NOESCAPE ^ _Nullable)(void))updates
                completion:(void (^ _Nullable)(BOOL finished))completion
{
    if (self.window == nil)
    {
        self.needsReloadWhenPutOnScreen = YES;
    }
    else
    {
        [super performBatchUpdates:updates completion:completion];
    }
}

// Use -performBatchUpdates:completion: instead of these methods, which will be deprecated in a future release.
-(void)beginUpdates
{
    if (self.window == nil)
    {
        self.needsReloadWhenPutOnScreen = YES;
    }
    else
    {
        [super beginUpdates];
    }
    
    
}

-(void)endUpdates
{
    if (self.window == nil)
    {
        self.needsReloadWhenPutOnScreen = YES;
    }
    else
    {
        [super endUpdates];
    }
}

-(void)insertSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation
{
    if (self.window == nil)
    {
        self.needsReloadWhenPutOnScreen = YES;
    }
    else
    {
        [super insertSections:sections withRowAnimation:animation];
    }
    
   
}

- (void)deleteSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation
{
    if (self.window == nil)
    {
        self.needsReloadWhenPutOnScreen = YES;
    }
    else
    {
        [super deleteSections:sections withRowAnimation:animation];
    }
    
   
}

-(void)reloadSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation
{
    if (self.window == nil)
    {
        self.needsReloadWhenPutOnScreen = YES;
    }
    else
    {
         [super reloadSections:sections withRowAnimation:animation];
    }
    
   
}

-(void)moveSection:(NSInteger)section toSection:(NSInteger)newSection
{
    if (self.window == nil)
    {
        self.needsReloadWhenPutOnScreen = YES;
    }
    else
    {
        [super moveSection:section toSection:newSection];
    }
    
   
}

-(void)insertRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation
{
    if (self.window == nil)
    {
        self.needsReloadWhenPutOnScreen = YES;
    }
    else
    {
        [super insertRowsAtIndexPaths:indexPaths withRowAnimation:animation];
    }
}

-(void)deleteRowsAtIndexPaths:(NSArray<nsindexpath*>*)indexPaths withRowAnimation:(UITableViewRowAnimation)animation
{
    if (self.window == nil)
    {
        self.needsReloadWhenPutOnScreen = YES;
    }
    else
    {
        [super deleteRowsAtIndexPaths:indexPaths withRowAnimation:animation];
    }
}

-(void)reloadRowsAtIndexPaths:(NSArray<nsindexpath*>*)indexPaths withRowAnimation:(UITableViewRowAnimation)animation
{
    if (self.window == nil)
    {
        self.needsReloadWhenPutOnScreen = YES;
    }
    else
    {
        [super reloadRowsAtIndexPaths:indexPaths withRowAnimation:animation];
    }
}

-(void)moveRowAtIndexPath:(NSIndexPath*)indexPath toIndexPath:(NSIndexPath*)newIndexPath
{
    if (self.window == nil)
    {
        self.needsReloadWhenPutOnScreen = YES;
    }
    else
    {
        [super moveRowAtIndexPath:indexPath toIndexPath:newIndexPath];
    }
}

-(void)reloadData
{
    if (self.window == nil)
    {
        self.needsReloadWhenPutOnScreen = YES;
    }
    else
    {
        [super reloadData];
    }
}


@end

I am getting the same warning while using fetchResultsController. I do update the data in DetailsViewController and as a result delegate method triggers in MainViewController. Interesting but I get this warning when I use reloadRowsAtIndexPaths method only. And if I use reloadData instead everything is ok.

I found the cause of the problem..


First

[self.tableView reloadData];
[self.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]

Second--Solved

[self.tableView reloadData];

dispatch_async(dispatch_get_main_queue(), ^{

     UITableViewCell*cell = [self.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]];
});

Since selected rows are lost during reload first you need to save the selected rows then select them again after the reload. Also because there might be a deselection animation you might need to animateAlongsideTransition and call setNeedsLayout on the cell if it is visible or setNeedsLayout on the tableView if not.


This all seems very messy.


Has anyone thought about using

NSBlockOperation and addExecutionBlock to collect all of the calls and then start it when regaining the window?

I was getting this too (while using a Navigation controller in a Split View).


While reviewing Apple's sample code for a table view using a DiffableDataSource (project: Conference, class: WiFiSettingsViewController), I noticed that the function updateUI:animated allowed a boolean to be passed, which was sent to self.dataSource.apply(currentSnapshot, animatingDifferences: animated).


When updateUI:animated is initially called, animated is set to false.


overridefunc viewDidLoad() {
    super.viewDidLoad()
    self.navigationItem.title = "Wi-Fi"
    configureTableView()
    configureDataSource()
    updateUI(animated: false)
}


func updateUI(animated: Bool = true) {
    ...

    self.dataSource.apply(currentSnapshot, animatingDifferences: animated)
}

By following this pattern in my code, the error message went away; apparently, the error was triggered during the animation process.

For me this error occurs when unwinding from another view controller, returning to an existing table view controller

The triggering line in my code is a tableView.reloadRows at the selectedIndexPath (which is an unwrapped tableView.indexPathForSelectedRow):

tableView.reloadRows(at: [selectedIndexPath], with: .automatic)


My guess is this code happens before the view has loaded and that seems to be what Xcode is warning about. Unfortunately commenting out the row isn't an option if you are unwinding after updating the value in an existing tableView row (which is an approach in several Apple Swift/iOS examples, including in Apple's Everyone Can Code series). After commenting out the reload in the unwind, I added a viewDidAppear with the following code and this seems to fix things:


override func viewDidAppear(_ animated: Bool) {

super.viewDidAppear(animated)

if let selectedIndexPath = tableView.indexPathForSelectedRow {

tableView.reloadRows(at: [selectedIndexPath], with: .automatic)

}

}


I'd welcome comments on whether this is a sound approach, but for now, this seems to be working. Also also - is it something we really need to be worrying about? I'm sure lots of developers have learned to use the unwind approach from Apple's examples, only to see that there are now warnings. Is this overzealousness in Xcode that we can plan on being fixed, or an appropriate warning that Apple-shared examples no longer work & an alternative should be found? Looking forward to an answer, as the above technique (that now earns an Xcode warning) is what I've been sharing with my students. Thanks! John

Hello,
to fix it add your code

override func viewWillLayoutSubviews() {
       super.viewWillLayoutSubviews()

       
       relatedArticle.layoutIfNeeded()
    }