Search code examples
iosswiftxcodeuiviewcontrollersafearealayoutguide

How to avoid viewSafeAreaInsetsDidChange() being called in UIViewController


I have a child-view controller that do not want to have system safe areas or viewSafeAreaInsetsDidChange() called on rotation. So far this is not working:

In Superview:

let childVC = UIViewController()
self.addChild(childVC)
childVC.view.frame = CGRect(x:0, y: self.view.bounds.height - 300, width: self.view.bounds.width, height: 300)
self.view.addSubview(childVC.view)
childVC.didMove(toParent: self)

In Child View Controller:

class childVC: UIViewController {
    let picker = UIPickerView()
    
    override final func loadView() {
        super.loadView()
    
        self.viewRespectsSystemMinimumLayoutMargins = false
        self.view.insetsLayoutMarginsFromSafeArea   = false
        self.view.preservesSuperviewLayoutMargins.  = false
    }

    override final func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)

        //Some mention this to be inside viewDidAppear
        self.viewRespectsSystemMinimumLayoutMargins = false
        self.view.insetsLayoutMarginsFromSafeArea = false
        self.view.preservesSuperviewLayoutMargins = false

        //Add picker view here
        self.picker.delegate = self
        self.picker.dataSource = self
        self.picker.frame = self.view.bounds
        self.view.addSubview(self.picker)
    }

    override func viewSafeAreaInsetsDidChange() {
        super.viewSafeAreaInsetsDidChange()
        //This gets called everytime the device rotates, causing the picker view to redraw and reload all components. Trying to avoid this method being called.

        print ("viewSafeAreaInsetsDidChange")
        print (self.view.safeAreaInsets)
    }
}

Issue

Everytime the device rotates, viewSafeAreaInsetsDidChange() gets called causing the picker view to redraw and reload.

Goal

Trying to avoid viewSafeAreaInsetsDidChange() being called every time the device rotates.


Solution

  • viewSafeAreaInsetsDidChange()

    Called to notify the view controller that the safe area insets of its root view changed.

    This is a notification call. You ignore it, or respond to it... but you cannot prevent the safe area insets from changing.

    These two examples: https://pastebin.com/vLkNj6vy and https://pastebin.com/cZPTZ17C show that viewSafeAreaInsetsDidChange() is called on device rotation, but the picker view does NOT reload.

    The picker view will reload if its frame is outside the affected safe-area change.

    Here is a quick example:

    class MovingPickerVC: UIViewController, UIPickerViewDataSource, UIPickerViewDelegate {
        
        let picker = UIPickerView()
        
        var vConstraints: [NSLayoutConstraint] = []
        var hConstraints: [NSLayoutConstraint] = []
    
        override func viewDidLoad() {
            super.viewDidLoad()
            
            picker.translatesAutoresizingMaskIntoConstraints = false
            self.view.addSubview(picker)
    
            // keep picker INSIDE the safe-area
            //  this WILL NOT cause reload on device rotation
            let g = view.safeAreaLayoutGuide
            
            // extend picker frame OUTSIDE the safe-area
            //  this WILL cause reload on device rotation
            //let g = self.view!
            
            NSLayoutConstraint.activate([
                picker.widthAnchor.constraint(equalToConstant: 300.0),
                picker.heightAnchor.constraint(equalToConstant: 160.0),
            ])
    
            vConstraints = [
                picker.centerXAnchor.constraint(equalTo: g.centerXAnchor),
                picker.bottomAnchor.constraint(equalTo: g.bottomAnchor),
            ]
            hConstraints = [
                picker.centerYAnchor.constraint(equalTo: g.centerYAnchor),
                picker.trailingAnchor.constraint(equalTo: g.trailingAnchor),
            ]
    
            // so we can see its frame
            picker.backgroundColor = .yellow
            
            self.picker.delegate = self
            self.picker.dataSource = self
        }
        
        override func viewWillAppear(_ animated: Bool) {
            super.viewWillAppear(animated)
            if self.traitCollection.verticalSizeClass == .regular {
                NSLayoutConstraint.activate(vConstraints)
            } else {
                NSLayoutConstraint.activate(hConstraints)
            }
        }
        
        override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) {
            super.willTransition(to: newCollection, with: coordinator)
            
            coordinator.animate(alongsideTransition: { [unowned self] _ in
                if newCollection.verticalSizeClass == .regular {
                    NSLayoutConstraint.deactivate(self.hConstraints)
                    NSLayoutConstraint.activate(self.vConstraints)
                } else {
                    NSLayoutConstraint.deactivate(self.vConstraints)
                    NSLayoutConstraint.activate(self.hConstraints)
                }
            }) { [unowned self] _ in
                // if we want to do something on completion
            }
            
        }
        override func viewSafeAreaInsetsDidChange() {
            super.viewSafeAreaInsetsDidChange()
            //This gets called everytime the device rotates, causing the picker view to redraw and reload all components. Trying to avoid this method being called.
            
            print ("viewSafeAreaInsetsDidChange")
            print (self.view.safeAreaInsets)
        }
        
        func numberOfComponents(in pickerView: UIPickerView) -> Int {
            print("num components: 1")
            return 1
        }
        
        func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
            print("num rows: 30")
            return 30
        }
        
        func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
            print("Title for Row:", row)
            return "Row: \(row)"
        }
        
    }
    

    Looks like this:

    enter image description here

    and rotating the device:

    enter image description here

    When that is run, we will see the viewSafeAreaInsetsDidChange() being logged, but the picker view will NOT reload.

    However, in viewDidLoad(), if we change the constraints to the view instead of the safe area:

        // keep picker INSIDE the safe-area
        //  this WILL NOT cause reload on device rotation
        //let g = view.safeAreaLayoutGuide
        
        // extend picker frame OUTSIDE the safe-area
        //  this WILL cause reload on device rotation
        let g = self.view!
    

    enter image description here

    enter image description here

    we've placed the picker view frame outside the safe area, and it will reload on rotation.

    If you don't want the picker to reload (for some reason), you'll need to manage that via your dataSource / delegate funcs.