Anomaly where previous view shows up briefly before new view is shown

115 Views Asked by At

I encountered this bug where my previous view in the NavigationController hierarchy incorrectly shows up briefly before my newly pushed view is loaded. I couldn't diagnose or reproduce reliably. Here are some details.

The Complete NavigationController sequence is this:

A(ViewController w/ SearchController)

-> B(ViewController as SearchResultController, only shows up for search result)

-> C1(ViewController for the specific record returned from search, pushed via a delegate method of B that uses the NavigationController in A to push to C, so note that B does not have navigation controller, but B stays in the view under the first C and will show when returned from C)

-> C2(pushed a new C by itself)

Expectation is, when C1 pushes the C2, only C2 is shown

But the actual behavior seen here is, when C1 pushes C2, B shows up for a split second before abruptly becoming C2. But nowhere is this specified.

I did the following things to pinpoint the issue as this only occurs once in about 40-50 random searches and has no pattern to find (the same search results won't produce the behavior the second time), but once observed, this behavior will stick on that specific view of C1 and will reproduce indefinitely when C2 is pushed, it will however return to normal when view is popped back to B from C1 and and the same C1 would not produce this behavior anymore when clicked again.

  1. I checked console output, no error or warning generated.
  2. I added breakpoint before the new C is pushed and checked if the data needed for initializing new C properly is there and it was there.
  3. I added breakpoints in ViewDidLoad, ViewWillAppear and ViewDidAppear, and found that B showed up after ViewWillAppear and stayed til ViewDidLoad, only after ViewDidLoad was finished did new C show up abruptly. I am still confused as apparently the new C is not "loaded" when the viewdidload executed.

I can't find any related information to debug this. The platform is iOS16 and I am using UIKit

Below are the minimal representation of my codes

In View A:

class SearchViewController: UIViewController, UISearchResultsUpdating,{
let searchController : UISearchController = {
        let vc = UISearchController(searchResultsController: SearchResultsViewController())
        vc.definesPresentationContext = true
        return vc
    
}()
override func viewDidLoad(){
searchController.searchResultsUpdater = self
}

func updateSearchResults(for searchController: UISearchController){
        let result = [] //basically any array of results object
        guard let resultsController = self.searchController.searchResultsController as? SearchResultsViewController,
              let query = searchController.searchBar.text, !query.isEmpty else{
            return
        }
        resultsController.delegate = self
        resultsController.update(with:result)
        
    }
}

//extension for the delegate of view B
extension SearchViewController : SearchResultsViewControllerDelegate{
    func didTapResult(_ result :  Record){
        let vc = RecordDetailViewController(rec: result)
        navigationController?.pushViewController(vc, animated: true)
    }
}

In View B:

class SearchResultsViewController: UIViewController, UITableViewDelegate, UITableViewDataSource{
weak var delegate : SearchResultsViewControllerDelegate?
private let tableView : UITableView = {
        let tableView = UITableView()
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "myCell")
        return tableView
    }()
override func viewDidLoad() {
        super.viewDidLoad()
        //viewAllFonts()
        view.addSubview(tableView)
        tableView.delegate = self
        tableView.dataSource = self
        tableView.keyboardDismissMode = .onDrag
        
        
    } //viewDidLoad
override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        tableView.frame = view.bounds
    }

    func update(with results: [Record]){
        filteredData = results
        tableView.reloadData()
        
    }
//omitting othertableview methods
//this is the part that delegates the didtapresult to view A
//has to do this bc navigationcontroller in this class is nil
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        //print(filteredData[indexPath.row])
        tableView.deselectRow(at: indexPath, animated: true)
        delegate?.didTapResult(filteredData[indexPath.row])
        
    }

}

in View C:

class RecordDetailViewController: UIViewController{

//omitting tableview var closure, same as view B basically
init(rec : Record){
        
        self.rec = rec
        super.init(nibName: nil, bundle: nil)
        print("alloced")
    }
    required init?(coder: NSCoder) {
        fatalError()
    }
    deinit {
        print("dealloced")
    }
override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        self.navigationItem.largeTitleDisplayMode = .never
    }
//omiting configure method
override func viewDidLoad() {
        super.viewDidLoad()
       
        configure()
        
        self.view.addSubview(self.tableView)
        
        tableView.delegate = self
        tableView.dataSource = self
        tableView.frame = view.bounds
        
        
        
    }


}

extension RecordDetailViewController: UITableViewDelegate, UITableViewDataSource{
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
                let rec = Record()
                //when I added breakpoint here, rec is properly initialized before vc is pushed
//as you can see this is pushing another view of this class
//after briefly pushing this, view B table appears for brief second.
                let vc = RecordDetailViewController(rec: rec!)
                self.navigationController?.pushViewController(vc, animated: true)
}


}

Following @Jacob Sheldon advice, I added debug statement in view C's init and deinit method. It appears that all view controllers are allocated and deallocated normally. There are no retaining views. Basically, each alloc prints comes with a dealloc print.

I also tried to capture UIHierarchy in Xcode, I first try to produce the behavior by clicking random results(B) and click table row(C1), then when the behavior is produced in C1->C2, I set up the Xcode in C1 and clicked "View UI Hierarchy" when the transition between c1->c2 just showed view B. This however did not capture the UIHierarchy of view B but still that of view C2, which is rendered correctly according to UIViewHierarchy.

I checked profiler and observed the memory usage and allocation usage. Nothing is out of line, the memory did go up and down with database results searches but it always returned to normal (~30 MB)when returning to view A.

Edit

After weeks of trying different things, I still couldn't even replicate or eliminate this bug reliably. It always happens in random. I made sure the init value for the view C is not null. There is no memory leak, all navigation controllers are correctly set up. I also moved all my code out of storyboards so now every behavior is done programmatically. One thing I noticed is that this bug never appears on the iOS 16 simulator, but does appear on my real iPhone 12 running iOS16.

I suspected if this is because of dispatch queue issue where the data is not available when the view is pushed, but the data is there, and the weird thing is all subsequent pushes to view D (a completely different and static view controller) are bugged too, this couldn't possibly be the data issue.

This is behavior in question

Behavior in question

this is expected behavior

enter image description here

1

There are 1 best solutions below

3
jacob sheldon On

Due to no source code, I just can give you one clue. Maybe it can solve your issue, maybe not. The most possible reason is the vc isn't be released normally. you can add the dealloc (for Objc) or the deinit (for Swift) method in your b and c vc implementation. If you reallise the vc is not be released as expected, you just need to check is there retain cycle in vc code. Especially the callbacks of network or subviews. If you can give more about your code or pseudocode, the issue can be solved more quickly.