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:
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user