cpimhoff cpimhoff - 3 months ago 26
Swift Question

UISplitViewController Resizing App Content when Keyboard Appears on iPhone

I have views nested inside of a

SplitViewController
, and the
SplitViewController.preferedDisplayMode
is
.primaryOverlay
.

When a user brings up the keyboard on a horizontally compact device, the entire content view of my application resizes to only include the area above the keyboard and any input accessory view.

This is opposed to the intended behavior of the keyboard appearing over the content (with content insets being applied to scroll views so users can see what is important).

By rotating the device, this resizing behavior goes away. In fact, after a single rotation, the behavior won't happen again until the app is restarted (even if you rotate back to the original rotation).

Debug Snapshots



These photos were taken on an iPhone with the split views collapses for clarity.

Here's a view debug snapshot of the content before rotation or keyboard:
before rotation without keyboard

Here's a debug snapshot of the content resized after the appearance of the keyboard (the further back view with the resize is a
UIPopoverView
which I did not create). Note that the keyboard is not in this snapshot because iOS puts the keyboard in a
RemoteKeyboardWindow
.

content resized by keyboard

And here's a debug snapshot and view debug snapshot of the content after a rotation to landscape and then back to portrait, and with the keyboard (the problem has gone away):
content with keyboard correct after rotations

The issue occurs on any text inputing view.

This is a bizarre issue to me, so if anybody has seen it before or knows any steps to take, it would be greatly appreciated.

Answer

I discovered the reason for this issue via this wonderful blog post about existing bugs in UISplitViewController.

Cause

The bug arose when horizontally compact environments requested the .primaryOverlay style. That style wraps the overlaying in a popover, and all popovers automatically resize to avoid the keyboard. Because a horizontally compact device was only showing one view, this wrapped all the app content in a resizing popover.

Turns out that UISplitViewController has an undocumented behavior in which rotating the device causes reevaluation of the preferedDisplayMode property. In the case of a horizontally compact device, the reevaluation would set the property back to .automatic (or .primaryHidden), which would stop wrapping the content in a resizing popover, fixing the issue.

Answer

The solution is to manually set the preferedDisplayMode property according to device traits both at initialization and whenever traits change.

I was able to fix the issue, while maintaining current behavior on iPad, by subclassing UISplitViewController as follows:

// Swift 3
class AdaptiveSplitViewController : UISplitViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        evaulateOverlayType(for: self.traitCollection)
    }

    override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) {
        super.willTransition(to: newCollection, with: coordinator)

        evaulateOverlayType(for: newCollection)
    }

    private func evaulateOverlayType(for traits: UITraitCollection) {
        if traits.horizontalSizeClass == .regular {
            self.preferredDisplayMode = .primaryOverlay
        } else {
            self.preferredDisplayMode = .automatic
        }
    }

}