Fix bug: Content view isn't sized correctly when it doesn't fill the screen

When a content view doesn't fill the screen and the keyboard is presented, the
content view wasn't sized correctly.
This commit is contained in:
Drew Olbrich
2019-03-19 21:18:25 -07:00
parent 0f21ade764
commit 04b4f8eed5
4 changed files with 68 additions and 59 deletions
@@ -12,7 +12,7 @@ import UIKit
/// An object that adjusts the host view controller's
/// `additionalSafeAreaInsets.bottom` property to compensate for the portion of the
/// keyboard that overlaps the scroll view.
/// keyboard that overlaps the host view controller's root view.
internal class AdditionalSafeAreaInsetsController {
private weak var delegate: AdditionalSafeAreaInsetsControlling?
@@ -26,12 +26,12 @@ internal class AdditionalSafeAreaInsetsController {
self.delegate = delegate
}
/// The height of the portion of the keyboard that overlaps the host view
/// controller's root view.
var bottomInset: CGFloat = 0 {
didSet {
guard let delegate = delegate,
let hostViewController = delegate.hostViewController,
let contentViewMinimumHeightConstraint = delegate.contentViewMinimumHeightConstraint else {
return
guard let delegate = delegate, let hostViewController = delegate.hostViewController else {
return
}
var adjustedBottomInset = bottomInset
@@ -60,17 +60,7 @@ internal class AdditionalSafeAreaInsetsController {
return
}
if delegate.shouldResizeContentViewForKeyboard {
// Adjust the additional safe area insets, possibly reducing the size
// of the content view.
hostViewController.additionalSafeAreaInsets.bottom = adjustedBottomInset
} else {
// Adjust the additional safe area insets, but also increase the minimum height of
// the content view to compensate. The size of the content view will remain
// unchanged.
hostViewController.additionalSafeAreaInsets.bottom = adjustedBottomInset
contentViewMinimumHeightConstraint.constant = adjustedBottomInset
}
hostViewController.additionalSafeAreaInsets.bottom = adjustedBottomInset
}
}
@@ -13,12 +13,9 @@ import UIKit
/// Delegate for `AdditionalSafeAreaInsetsController`.
internal protocol AdditionalSafeAreaInsetsControlling: class {
/// View controller whose `additionalSafeAreaInsets` property is manipulated.
/// The view controller whose `additionalSafeAreaInsets` property is manipulated.
var hostViewController: UIViewController? { get }
/// Manipulated content view minimum height constraint.
var contentViewMinimumHeightConstraint: NSLayoutConstraint? { get }
/// If `true`, the content view is allowed to shrink to compensate for the reduced
/// visible area of the screen when the keyboard is presented.
var shouldResizeContentViewForKeyboard: Bool { get }
+26 -25
View File
@@ -261,44 +261,45 @@ extension KeyboardObserver: ScrollViewFilterKeyboardDelegate {
assert(isAdjustingViewForKeyboardFrameEvent)
}
/// Returns the height of portion of the keyboard's frame that overlaps the scroll
/// view.
/// Returns the height of portion of the keyboard's frame that overlaps the host
/// view controller's root view.
///
/// This method correctly handles the case where the view doesn't cover the entire
/// screen.
/// This method correctly handles the case where the view controller's root view
/// does not cover the entire screen, for example if the view controller is composed
/// within another view controller.
///
/// - Parameter notification: The keyboard notification that provides the keyboard's
/// frame.
/// - Returns: The height of portion of the keyboard's frame that overlaps the view.
/// - Returns: The height of portion of the keyboard's frame that overlaps the view
/// controller's root view.
private func bottomInset(from keyboardFrame: CGRect?) -> CGFloat? {
guard let keyboardFrame = keyboardFrame,
let hostViewController = delegate?.hostViewController,
let view = hostViewController.view,
let rootView = hostViewController.view,
// UIApplication.shared.keyWindow is nil when unit tests are executing, and
// view.window is nil outside of unit tests in the case when a view is being
// pushed.
let window = isUnitTest ? view.window : UIApplication.shared.keyWindow else {
// rootView is nil outside of unit tests in the case when a view is being pushed.
let window = isUnitTest ? rootView.window : UIApplication.shared.keyWindow else {
return nil
}
// The frame of the view in the window's coordinate space.
let viewFrameInWindow = window.convert(view.frame, from: view.superview)
// The frame of the view controller's root view in the window's coordinate space.
let rootViewFrameInWindow = window.convert(rootView.frame, from: rootView.superview)
// The intersection of the keyboard's frame with the frame of the view in the
// window's coordinate space.
let keyboardViewIntersectionFrameInWindow = viewFrameInWindow.intersection(keyboardFrame)
// The intersection of the keyboard's frame with the frame of the root view in
// the window's coordinate space.
let keyboardViewIntersectionFrameInWindow = rootViewFrameInWindow.intersection(keyboardFrame)
// The intersection of the keyboard's frame with the frame of the view in the
// view's coordinate space.
let keyboardViewIntersectionFrameInView = window.convert(keyboardViewIntersectionFrameInWindow, to: view)
// The intersection of the keyboard's frame with the frame of the root view in
// the root view's coordinate space.
let keyboardViewIntersectionFrameInRootView = window.convert(keyboardViewIntersectionFrameInWindow, to: rootView)
// The height of the region of the keyboard that overlaps the view.
let overlappingKeyboardHeight = keyboardViewIntersectionFrameInView.height
// The height of the region of the keyboard that overlaps the root view.
let overlappingKeyboardHeight = keyboardViewIntersectionFrameInRootView.height
// The view's safe area bottom inset.
let safeAreaBottomInset = hostViewController.view.safeAreaInsets.bottom
// The root view safe area bottom inset.
let safeAreaBottomInset = rootView.safeAreaInsets.bottom
// The view's additional safe area bottom inset.
// The host view controller's additional safe area bottom inset.
let additionalSafeAreaBottomInset = hostViewController.additionalSafeAreaInsets.bottom
// The bottom safe area bottom inset, excluding the additional safe area inset.
@@ -308,10 +309,10 @@ extension KeyboardObserver: ScrollViewFilterKeyboardDelegate {
// into account, which would otherwise result in a negative number.
let baseSafeAreaBottomInset = max(0, safeAreaBottomInset - additionalSafeAreaBottomInset)
// The height of area of the keyboard's frame that overlaps the view.
let keyboardHeightOverlappingView = max(0, overlappingKeyboardHeight - baseSafeAreaBottomInset)
// The height of area of the keyboard's frame that overlaps the root view.
let keyboardHeightOverlappingRootView = max(0, overlappingKeyboardHeight - baseSafeAreaBottomInset)
return keyboardHeightOverlappingView
return keyboardHeightOverlappingRootView
}
}
+35 -14
View File
@@ -74,7 +74,11 @@ public class ScrollingContentViewManager: KeyboardObservering, ScrollViewBounceC
/// the scroll view obscured by the presented keyboard, if possible.
///
/// The default value is `false`.
public var shouldResizeContentViewForKeyboard = false
public var shouldResizeContentViewForKeyboard = false {
didSet {
updateContentViewMinimumHeightConstraints()
}
}
/// If `true`, the view controller's `additionalSafeAreaInsets` property is adjusted
/// when the keyboard is presented.
@@ -83,15 +87,17 @@ public class ScrollingContentViewManager: KeyboardObservering, ScrollViewBounceC
public var shouldAdjustAdditionalSafeAreaInsetsForKeyboard = true
/// A constraint that enforces a minimum width for the content view.
///
/// The priority of this constraint is `defaultHigh`.
internal private(set) var contentViewMinimumWidthConstraint: NSLayoutConstraint?
private var contentViewMinimumWidthConstraint: NSLayoutConstraint?
/// A constraint that enforces a minimum height for the content view.
///
/// The priority of this constraint is `defaultHigh`. This constraint's constant is
/// modified by `AdditionalSafeAreaInsetsController`.
internal private(set) var contentViewMinimumHeightConstraint: NSLayoutConstraint?
/// A constraint that enforces a minimum height for the content view equal to the
/// scroll view's frame height. This constraint is active if
/// `shouldResizeContentViewForKeyboard` is `false`.
private var contentViewMinimumHeightScrollViewFrameConstraint: NSLayoutConstraint?
/// A constraint that enforces a minimum height for the content view equal to the
/// scroll view's safe area height. This constraint is active if
/// `shouldResizeContentViewForKeyboard` is `true`.
private var contentViewMinimumHeightScrollViewSafeAreaConstraint: NSLayoutConstraint?
/// An object that responds to notifications posted by UIKit when the keyboard is
/// presented or dismissed, and which adjusts the scroll view to compensate.
@@ -459,11 +465,15 @@ public class ScrollingContentViewManager: KeyboardObservering, ScrollViewBounceC
let contentViewMinimumWidthConstraint = contentView.widthAnchor.constraint(greaterThanOrEqualTo: scrollView.safeAreaLayoutGuide.widthAnchor, multiplier: 1)
self.contentViewMinimumWidthConstraint = contentViewMinimumWidthConstraint
let contentViewMinimumHeightConstraint = contentView.heightAnchor.constraint(greaterThanOrEqualTo: scrollView.safeAreaLayoutGuide.heightAnchor, multiplier: 1)
self.contentViewMinimumHeightConstraint = contentViewMinimumHeightConstraint
let contentViewMinimumHeightScrollViewFrameConstraint = contentView.heightAnchor.constraint(greaterThanOrEqualTo: scrollView.heightAnchor, multiplier: 1)
self.contentViewMinimumHeightScrollViewFrameConstraint = contentViewMinimumHeightScrollViewFrameConstraint
let contentViewMinimumHeightScrollViewSafeAreaConstraint = contentView.heightAnchor.constraint(greaterThanOrEqualTo: scrollView.safeAreaLayoutGuide.heightAnchor, multiplier: 1)
self.contentViewMinimumHeightScrollViewSafeAreaConstraint = contentViewMinimumHeightScrollViewSafeAreaConstraint
contentViewMinimumWidthConstraint.priority = minimumSizeConstraintPriority
contentViewMinimumHeightConstraint.priority = minimumSizeConstraintPriority
contentViewMinimumHeightScrollViewFrameConstraint.priority = minimumSizeConstraintPriority
contentViewMinimumHeightScrollViewSafeAreaConstraint.priority = minimumSizeConstraintPriority
contentView.translatesAutoresizingMaskIntoConstraints = false
@@ -473,10 +483,21 @@ public class ScrollingContentViewManager: KeyboardObservering, ScrollViewBounceC
scrollView.contentLayoutGuide.topAnchor.constraint(equalTo: contentView.topAnchor),
scrollView.contentLayoutGuide.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
contentViewMinimumWidthConstraint,
contentViewMinimumHeightConstraint
contentViewMinimumHeightScrollViewFrameConstraint,
contentViewMinimumHeightScrollViewSafeAreaConstraint
]
scrollView.addConstraints(constraints)
updateContentViewMinimumHeightConstraints()
}
/// Activates either `contentViewMinimumHeightScrollViewFrameConstraint` or
/// `contentViewMinimumHeightScrollViewSafeAreaConstraint` given the value of
/// `shouldResizeContentViewForKeyboard`.
private func updateContentViewMinimumHeightConstraints() {
contentViewMinimumHeightScrollViewFrameConstraint?.isActive = !shouldResizeContentViewForKeyboard
contentViewMinimumHeightScrollViewSafeAreaConstraint?.isActive = shouldResizeContentViewForKeyboard
}
/// Constrains a scroll view content offset so that it lies within the legal range
@@ -542,7 +563,7 @@ public class ScrollingContentViewManager: KeyboardObservering, ScrollViewBounceC
// When the keyboard is presented, the view controller's
// additionalSafeAreaInsets.bottom property is adjusted to compensate.
//
// This approach is chosen instead of resizing the scroll view's content size,
// This approach was chosen instead of resizing the scroll view's content size,
// because doing so requires adjusting its scrollIndicatorInsets property to
// compensate, and on iPhone Xs in landscape orientation, this has the unfortunate
// side effect of awkwardly shifting the scroll indicator away from the edge of the