38

I am getting a strange error with iOS13 when performing a Segue and I can't figure out what it means, nor can I find any documentation for this error. The problem is that this seems to cause a lot of lag (a few seconds) until the segue is performed.

2019-09-11 22:45:38.861982+0100 Thrive[2324:414597] [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, 0}; contentSize: {315, 118}; adjustedContentInset: {0, 0, 0, 0}; dataSource: >

I am using Hero but I tried disabling it and using a regular Segue and this hasn't stopped the lag.

The code to initiate the segue is didSelectRowAt

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        if indexPath.section == 0 {
            selectedCell = realIndexFor(activeGoalAt: indexPath)
            performSegue(withIdentifier: "toGoalDetails", sender: nil)
        } else if indexPath.section == 1 {
            selectedCell = indexPath.row
            performSegue(withIdentifier: "toIdeaDetails", sender: nil)
        } else {
            selectedDecision = indexPath.row
            hero(destination: "DecisionDetails", type: .zoom)
        }
    }

And then none of the code in viewDidLoad or viewWillAppear from the destination VC affects this in any way (I tried commenting it all out with no difference.

Any idea what's causing this? I can share whatever other details are needed.

Thank you.

CristianMoisei
  • 2,071
  • 2
  • 22
  • 28
  • 1
    I see this too. It seems that you can ignore this. – Paulw11 Sep 11 '19 at 23:15
  • The problem is that I have a delay of several seconds between tapping on a cell and when it opens. Maybe it's not related.. Would you know where to look for what's causing the delay? I tried the Profiler but I couldn't find much. – CristianMoisei Sep 11 '19 at 23:16
  • 1
    Normally delays are caused by updating UI elements when you are no running in the main queue. Do you perform any network operations or other asynchronous tasks? Make sure you dispatch back onto the main queue from any completion handlers. – Paulw11 Sep 11 '19 at 23:18
  • Thanks. I am calling a function that syncs with iCloud in the main thread but I tried moving it to global queue and it doesn't seem to make any difference. I've tried removing the call to the function and anything else to do with iCloud and just using local storage and it still makes no difference. What's also strange is that the sync only happens once on launch, but maybe I did something wrong. Can I ask what queue you think is best for syncing small iCloud files? – CristianMoisei Sep 11 '19 at 23:24
  • What I noticed is that it registers my tap (prints a statement) but it doesn't start the animation for a few seconds. – CristianMoisei Sep 11 '19 at 23:26
  • Getting this in SwiftUI. Anyone else? – George Feb 15 '20 at 22:20

18 Answers18

20

It happened to me because I registered the device for change orientation notification in the viewWillAppear(:) method. I moved the registration in the viewDidAppear(:) and Xcode it's not stopping at the breakpoint anymore.

What I can say is that layout changes might be run when the view is already visible...

Miniapps
  • 292
  • 3
  • 3
  • 4
    As you say, when you do the update in viewDidAppear instead of viewWillAppear, then the layout changes will happen when the view is already visible, leading to a very jerky user experience. I would rather say that this warning is a bug in iOS. Post a bug on the Apple feedback assistant, and ignore the warning. – fishinear Aug 01 '20 at 14:48
  • I have to agree that this is a bug. I am getting this warning / alert triggered by calling `cellForRow(at:)` which involves no layout at all. That seems wrong. – matt Oct 23 '20 at 14:59
  • Pretty sure this is a bug. – Pedro Paulo Amorim Nov 19 '20 at 16:23
  • I was applying autolayout constraints from viewDidLayoutSubviews or viewWillAppear and for me the culprit was calling layoutIfNeeded after setting constraints. Removing that line of code removed the warning and also my tableview was laid out properly. – Shawn Frank Jun 11 '21 at 09:51
10

For people using DiffableDataSource, set animatingDifferences to false and warning will be gone.

dataSource.apply(snapshot, animatingDifferences: false)

Denis Kakačka
  • 697
  • 1
  • 8
  • 21
  • 1
    Thanks mate, that was my issue – Ricardo Aug 11 '21 at 19:39
  • 4
    Thank you so much, it saved my day. For those who want to have animation afterward it can be used like this: `dataSource.apply(snapshot, animatingDifferences: tableView.window != nil)` – Dimson Oct 26 '21 at 16:28
7

Like @joe-h, I was getting this error and was also surprised as the unwind approach he shows is one used by lots of developers + is in some significant Apple iOS sample code.

The triggering line in my code (@joe-h, I'm guessing likely in yours, too) is a tableView.reloadRows at the selectedIndexPath (which is an unwrapped tableView.indexPathForSelectedRow):

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

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 the Apple FoodTracker tutorial mentioned above, as well as one used in Apple's Everyone Can Code series). If you don't reload the row(s) then your change won't show in the tableView. 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.

Marcus Adams
  • 53,009
  • 9
  • 91
  • 143
Gallaugher
  • 1,593
  • 16
  • 27
  • When you do the reload in viewDidAppear, then it will happen when the view is already visible, leading to a jerky user experience. – fishinear Aug 01 '20 at 14:50
  • I'll second what @fishinear said. We are 'forced' in to this solution, as the 'only' solution, but this a is really bad outcome. It felt like a bug when it started, now it feels like neglect. Come to think of it, I live better with this warning than bad user experience. For the user, updating in `viewDidAppear` looks like a bug. – bauerMusic Jun 18 '21 at 05:42
6

This warning can happen du to updating table view or collection view while it is not visible, for example when it is on the parent view controller. To solve that, first, I created a property in the view controller, containing the table view to check if the view controller is visible or not, as bellow:

var isVisible: Bool = false

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    self.isVisible = true
}

override func viewDidDisappear(_ animated: Bool) {
    super.viewDidAppear(animated)
    self.isVisible = false
}

Then in the data source delegate, before reacting to changes, first check if the view controller is visible. If it was not, do not do any updates. For example

func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
    guard isVisible else { return }
    tableView.beginUpdates()
}

You should check that visibility before doing any changes in the tableView. For example, in case of NSFetchedResultsController, it must be done in all delegate callbacks which we have implemented.

UPDATE

I recently found that if you update the table view with animation false, even when it is not visible, there won't be any warnings.

Justin
  • 1,786
  • 20
  • 21
  • 2
    +1 "with animation false, even when it is not visible, there won't be any warnings." - you should make this comment bold and put it on top, it is gold. – de. Mar 01 '21 at 21:01
  • 1
    I still get this warning, using `UITableViewRowAnimationNone`... – bauerMusic Jul 19 '21 at 05:14
6

I had the same error on my Project; A tableView with a diffable datasource. Been bugging on it for hours. Problem lies in updating the snapshot, more specifically on a background thread (default). Forcing the update of the datasource on the main thread got rid of the problem! Hope this helps someone out there!

    func updateData(on annotations: [Annotation]) {
        var snapshot = NSDiffableDataSourceSnapshot<AnnotationType, Annotation>()
        //Append available sections
        AnnotationType.allCases.forEach { snapshot.appendSections([$0]) }

        //Append annotations to their corresponding sections
        annotations.forEach { (annotation) in
            snapshot.appendItems([annotation], toSection: annotation.type as AnnotationType)
        }

        //Force the update on the main thread to silence a warning about tableview not being in the hierarchy!
        DispatchQueue.main.async {
            self.dataSource.apply(snapshot, animatingDifferences: true)
        }
    }
DeveloperSammy
  • 167
  • 1
  • 11
3

I'm new to Xcode/Swift so this may or may not help anyone. I started getting this error after updating to iOS 13 and Xcode 11 within the app when going back to a list from a detail view.

I found that I was doing a tableView.reloadRows and tableView.insertRows in the unwind(as suggested by Apple in one of their tutorials)

@IBAction func unwindToMealList(sender: UIStoryboardSegue) {
    if let sourceViewController = sender.source as? MealViewController, let meal = sourceViewController.meal {

        if let selectedIndexPath = tableView.indexPathForSelectedRow {
            // Update an existing meal.
            meals[selectedIndexPath.row] = meal
            tableView.reloadRows(at: [selectedIndexPath], with: .none)
        }
        else {
            // Add a new meal.
            let newIndexPath = IndexPath(row: meals.count, section: 0)

            meals.append(meal)
            tableView.insertRows(at: [newIndexPath], with: .automatic)
        }
    }
}

)

I commented out that section of code and it went away.

Oddly enough leaving the sort and self.tableView.reloadData() didn't give me the error.

alxlives
  • 5,084
  • 4
  • 28
  • 50
Joe H.
  • 31
  • 1
3

In viewDidDisappear method I declare tableView.setContentOffset(CGPoint(x: 0, y: 0), animated: false) function. Some of you says it's not important but it affected tableView delegate methods. For example viewForHeader function is not called when I get this warning.

Tahir Kizir
  • 83
  • 2
  • 10
3

I found the most robust and safe way is to wait for the didMoveToWindow of the table view / collection view

as even in viewWillAppear the view may NOT be attached to a window and puting your code in viewDidAppear may cause unwanted graphical glitches

class MyTableViewOrCollectionView: UITableView {

    var didMoveToWindowCallback: (()->())? = nil
    
    override func didMoveToWindow() {
        
        super.didMoveToWindow()
        
        didMoveToWindowCallback?()
        didMoveToWindowCallback = nil
        
    }

}

and than you can

override func viewDidLoad() {
    
    super.viewDidLoad()
    
    tableView.didMoveToWindowCallback = { [weak self] in
        self?.setupInitialContent()
    }
    
}
Peter Lapisu
  • 19,915
  • 16
  • 123
  • 179
  • 1
    This is a good solution, but this really looks like something Apple should have taken care of (instead of subclassing). How is that a good UX to present an 'un-synced' data and updated it 'live' only while it's visible. We already assert that in `viewDidLoad` all Storyboard properties are initialized, this is another nail in `UITableView`. Should just migrate everything to `UICollectionView`. – bauerMusic Jun 18 '21 at 05:37
  • UICollectionView is the same – Peter Lapisu Jun 18 '21 at 09:46
  • Thanks. As many said, I'd treat it as an Apple bug and either adopt your solution or live with the warning. – bauerMusic Jul 19 '21 at 04:58
2

iPadOS 13.2.3 swift 5.2 Xcode 11.2.1

Just ran into this issue only when starting the app while the device was landscape. I was calling the detail seque in the viewDidLoad func of the master controller to make sure the detail view was setup correctly.

     override func viewDidLoad() {
       super.viewDidLoad()
            ...
       self.performSegue(withIdentifier: "showDetail", sender: self)
     }

When I removed the performSeque the warning not longer appeared, however, the left bar buttons on the detail controller no longer worked properly, again only when starting the app while the device was landscape. The left most button would activate the next button to the right instead of what the first button was suppose to do.

The fix for the bar buttons was to add to the viewDidLoad

     override func viewDidLoad() {
       super.viewDidLoad()
            ...
       self.splitViewController?.preferredDisplayMode = UISplitViewController.DisplayMode.allVisible
     }

Then execute

     override func viewWillAppear(_ animated: Bool) {
       self.splitViewController?.preferredDisplayMode = UISplitViewController.DisplayMode.automatic
       super.viewWillAppear(animated)
     }

I have no explanation why this worked!

This app had worked flawlessly until iPados 13 was loaded.

Community
  • 1
  • 1
Daryl1109
  • 66
  • 4
2

I am getting similar breakpoint with SwiftUI, without even dealing with viewDidLoad or viewDidappear

    //
//  ContentView.swift
//  DD
//
//  Created by Roman Emperor on 3/29/20.
//  Copyright © 2020 Emperors. All rights reserved.
//
import Combine
import SwiftUI

// Defining a class Booking of type Bindable Object [changed to ObservableObject]
class Booking: ObservableObject {
    var didChange = PassthroughSubject<Void, Never>()

    // Array of types to work with
    static let types = ["Consultation", "Tooth Pain", "Cleaning", "Brases", "Dental Implant" ]
    // Setting instance varibale type
    var type = 0 { didSet { update() } }

    func update () {
        didChange.send(())
    }
}


struct ContentView: View {
    @ObservedObject var booking = Booking() //bindableObject in old swift version

    var body: some View {
        NavigationView {
            Form {
                Section {
                    Picker(selection: $booking.type, label: Text("Select a Booking Type")) {
                        ForEach(0 ..< Booking.types.count){
                            Text(Booking.types[$0]).tag($0)
                        }
                    }
                }
            }
        .navigationBarTitle(Text("Darpan Dental Home"))
        }
    }
}


struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

The Complete output Log is here:

*> 2020-03-29 09:22:09.626082+0545 DD[1840:76404] [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.*

**where is this UITableViewAlertForLayoutOutsideViewHierarchy in SwiftUI ? **

Parajuli Roman
  • 551
  • 1
  • 6
  • 15
  • I had this waring on my `List` and `Form`, because of that I change them into `ScrollView`. If you found better solution, please let me know. – FRIDDAY May 08 '20 at 06:51
1
extension UIView {

  func rootView() -> UIView {
     var view = self
     while view.superview.isNotNil {
         view = view.superview!
     }
     return view
  }

  var isOnWindow: Bool {
     return self.rootView() is UIWindow
    }
  }

then you just need to check if your tableView isOnWindow like...

if self.tableView.isOnWindow {
/// do stuff
}

Disclaimer: as the documentation explains, you may need to defer the call which means that there is no warranty your method will be called again so it's your responsibility to perform your update when isOnWindow is true.

James Rochabrun
  • 4,137
  • 2
  • 20
  • 17
  • Actually we can just do this instead... extension UIView { var isOnWindow: Bool { return window != nil } } – James Rochabrun Nov 04 '19 at 23:06
  • 4
    ... or just don't use an extension at all. `if self.tableView.window != nil` or `guard self.tableView.window != nil` – Mark Mar 17 '20 at 12:54
1

Had the same issue, removing tableView.reloadSections fixed it. This was the line of code causing the warning:

iOS 13:

tableView.reloadSections(IndexSet(integer: 0), with: .automatic)

in iOS 14, removing tableView.reloadSections did not fix the warning.

spnkr
  • 952
  • 9
  • 18
1

Or maybe your code (like mine) has nothing wrong with it and this message just randomly starts popping up. In that case, do a clean on your project, restart Xcode and watch the message magically go away!

Carl Smith
  • 1,236
  • 15
  • 18
0

Please check following function

override func viewWillLayoutSubviews()
Chathurka
  • 605
  • 6
  • 11
0

For anyone that has this issue with a UISplitViewController and a UITableView inside the detail view controller, you can try subclassing and override layoutSubviews like this (From this thread):

class CustomTableView: UITableView {
    override func layoutSubviews() {
        if (self.window == nil) {
            return
        }
        super.layoutSubviews()
    }
 }
0

Instead of reloading the rows inside viewDidAppear, this is what it worked for me:

DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
    self.tableView.reloadRows(at: [indexPath], with: .none)
}

Also if you are using DiffableDataSource and you are selecting an indexPath manually for example, you need to do it on the completion block of the apply snapshot method:

dataSource.apply(snapshot, to: section, animatingDifferences: false, completion: {
    // select the indexPath programmatically or do UITableView UI stuff here.
})
pableiros
  • 14,932
  • 12
  • 99
  • 105
0

... the table view or one of its superviews has not been added to a window ...

To resolve the issue we need to check tableView.window property:

class MyViewController: UIViewController {
  override func viewDidLoad() {
    super.viewDidLoad()
    updateTableViewRows()
  }

  func dataChanged() {
    updateTableViewRows()
  }

  func updateTableViewRows() {
    if tableView.window == nil {
      // TODO: just update data source
    } else {
      tableView.performBatchUpdates {
        // TODO: update data source
        // TODO: update table view cells
      }
    }
  }
}

The idea is to not call performBatchUpdates and related functions while tableView.window is nil.

Roman Aliyev
  • 224
  • 2
  • 7
0

This happens if you do something with the view which is (or its superview) not fully loaded yet.
My issue was that, in one method, called before all views are loaded on screen, I had this:

guard let cell = tableView.cellForRow(at: IndexPath(row: index, section: 0)) as? MyCell else { return }

which means that I was trying to do something with table when it was not loaded.

What helped me was to make sure that I am on the main thread when doing this:

DispatchQueue.main.async {
    guard let cell = tableView.cellForRow(at: IndexPath(row: index, section: 0)) as? MyCell else { return }
}

For some of you, maybe you will need some more time, so you can do this:

DispatchQueue.main.asyncAfter(deadline: .now()) {
    guard let cell = tableView.cellForRow(at: IndexPath(row: index, section: 0)) as? MyCell else { return }
}

Notice that I did not put additional delay in deadline parameter, since it adds some slight delay itself, but you can put some if you need.

After any of this two solutions, the error in console was gone for me. Hope it helps.

stackich
  • 3,607
  • 3
  • 17
  • 41