Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

UITableViewAlertForLayoutOutsideViewHierarchy when UITableView doesn't have window iOS13

I started to receive warning (below) on iOS13. I have noticed that this warning pops up because UITableView's window is null (another tab is selected, pushed detailed view controller on table selection...). I am trying to update UITableView from NSFetchedResultController delegate. What is the correct way to do this on iO13 to keep table updated?

Code below worked fine on previous releases.

PS: Any kind of beginUpdates , reloadRowsAtIndexPaths:withRowAnimation: , insertSections:withRowAnimation: , endUpdates will cause this warning.

PS: I tried reload table but if I navigate back I lose animation to deselect row (clear row selection).

2019-09-27 09:40:42.849128+0200 xxx[63595:9762090] [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: ; layer = ; contentOffset: {0, -64}; contentSize: {375, 3432}; adjustedContentInset: {64, 0, 0, 0}; dataSource: >

// ------------  ------------  ------------  ------------  ------------  ------------
#pragma mark - FetchedResultsController delegate

- (void) controllerWillChangeContent:(NSFetchedResultsController *)controller {
//    if (self.tableView.window) {
        [self.tableView beginUpdates];
//    }
}
- (void) controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath {

    if (type == NSFetchedResultsChangeInsert && newIndexPath != nil) {
        [self.tableView insertRowsAtIndexPaths:@[newIndexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
    }

    if (type == NSFetchedResultsChangeUpdate && indexPath != nil) {
        [self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
//        id<CellLoadable> cell = [self.tableView cellForRowAtIndexPath:indexPath];
//        [cell loadData:anObject];
    }

    if (type == NSFetchedResultsChangeMove && indexPath != nil && newIndexPath != nil) {
        // if cell is visible, update it
        id<CellLoadable> cell = [self.tableView cellForRowAtIndexPath:indexPath];
        [cell loadData:anObject];
        [self.tableView moveRowAtIndexPath:indexPath toIndexPath:newIndexPath];
    }
}

- (void) controller:(NSFetchedResultsController *)controller didChangeSection:(id<NSFetchedResultsSectionInfo>)sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type {

    if (type == NSFetchedResultsChangeInsert) {
        [self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
    }
    if (type == NSFetchedResultsChangeDelete) {
        [self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
    }
}

- (void) controllerDidChangeContent:(NSFetchedResultsController *)controller {
//    if (self.tableView.window) {
        [self.tableView endUpdates];
//    }
}
like image 636
Marek H Avatar asked Sep 27 '19 07:09

Marek H


2 Answers

PS: Any kind of beginUpdates , reloadRowsAtIndexPaths:withRowAnimation: , insertSections:withRowAnimation: , endUpdates will cause this warning.

I found that wrapping the table update where the breakpoint triggers in dispatch_async eliminates the issue:

dispatch_async(dispatch_get_main_queue(), ^(void){
    [self.table reloadSections:[NSIndexSet indexSetWithIndex:1] withRowAnimation:UITableViewRowAnimationFade];
});

(may have to walk up the call stack to find the call when it breaks)

like image 80
robm Avatar answered Sep 27 '22 20:09

robm


You could try the code below, i.e. by checking for the window and calling reloadData instead. FYI this doesn't actually reload all the cells it just calls the number of rows, sections etc. Then on next appear the new cells will be loaded. You would be better off disabling the fetch controller when the view disappears though and reloading the table on next appear.

- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
    if(!self.tableView.window){
        return;
    }
    [self.tableView beginUpdates];
}

- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo
           atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type {
    if(!self.tableView.window){
        return;
    }
    switch(type) {
        case NSFetchedResultsChangeInsert:
            [self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
            break;

        case NSFetchedResultsChangeDelete:
            [self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
            break;

        default:
            return;
    }
}

- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
       atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
      newIndexPath:(NSIndexPath *)newIndexPath {
    UITableView *tableView = self.tableView;
    if(!tableView.window){
        return;
    }
    switch(type) {
        case NSFetchedResultsChangeInsert:
            [tableView insertRowsAtIndexPaths:@[newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
            break;

        case NSFetchedResultsChangeDelete:
            [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
            break;
        case NSFetchedResultsChangeMove:
            [tableView moveRowAtIndexPath:indexPath toIndexPath:newIndexPath];
        case NSFetchedResultsChangeUpdate:
            [self configureCell:[tableView cellForRowAtIndexPath:indexPath] withEvent:anObject];
            break;
    }
}

- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
    if(!self.tableView.window){
        [self.tableView reloadData];
        return;
    }
    [self.tableView endUpdates];
}

Note, you also might like to reselect a previously selected cell. There are a few different ways to do that.

like image 24
malhal Avatar answered Sep 27 '22 22:09

malhal