Compare commits
87 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c0c479cdba | |||
| d76389e430 | |||
| c7563abb4e | |||
| 68d1d645e4 | |||
| 125b276319 | |||
| bb542c0ef9 | |||
| 59ac63c3f1 | |||
| 3620387efc | |||
| e9b9dc7e0e | |||
| 5dc40100bf | |||
| 061acc97cf | |||
| 5c8a4252d3 | |||
| e6ef615083 | |||
| 8275176f21 | |||
| 4e028ee4e9 | |||
| e925415429 | |||
| 36a19cba83 | |||
| 17da6c00e4 | |||
| 46880f6631 | |||
| 9809badff2 | |||
| 5a08bff7a5 | |||
| 6d50f761ad | |||
| b4c2d666cf | |||
| 82a0d85308 | |||
| 368383e16b | |||
| a42b0346f9 | |||
| 27ded10c91 | |||
| 534580ee41 | |||
| d453927276 | |||
| 9358ba3e5c | |||
| 8e6c9b5a23 | |||
| 56ed1aff1d | |||
| 13a0f3ffe3 | |||
| 2ece9b58bc | |||
| 04b4f8eed5 | |||
| 0f21ade764 | |||
| 3b99264884 | |||
| 654b4949b2 | |||
| 0a4796a643 | |||
| c3062e345a | |||
| ba11e04deb | |||
| 8c97f5d83a | |||
| 4f85c08a54 | |||
| ec97d4b936 | |||
| 846852a67f | |||
| 809c0dabc5 | |||
| 1febd4694b | |||
| 551d94f52c | |||
| 65c738f3e4 | |||
| 6b5119bb08 | |||
| 3c739780bb | |||
| 11f0492d37 | |||
| 53836f94c7 | |||
| 6fc4f08afa | |||
| 81bd105e4d | |||
| 89a84daf3c | |||
| 80fcb1ae32 | |||
| 8fb074ff46 | |||
| 6f67d2c205 | |||
| 361b31c298 | |||
| 982da685e3 | |||
| eb28ffde42 | |||
| 61161b7d64 | |||
| 1f7dde75e2 | |||
| 65b8467384 | |||
| a19e0ffab2 | |||
| 57744804bb | |||
| adb9a189b5 | |||
| c6a10a275f | |||
| f8b4cc19c6 | |||
| 970041fb4f | |||
| 6b225bdb05 | |||
| 08e547dcb0 | |||
| 758e78dc13 | |||
| d9365d80aa | |||
| d94c2e622a | |||
| 917865fe5b | |||
| cb3b318869 | |||
| 6f819a1eea | |||
| fc42d7020c | |||
| 7a7a3c6be1 | |||
| 88043ca615 | |||
| fde366e946 | |||
| 9244d52a4a | |||
| 68233ec332 | |||
| 343aa9b8c4 | |||
| cfcfa3fd18 |
@@ -0,0 +1,8 @@
|
||||
disabled_rules:
|
||||
- line_length
|
||||
- unused_closure_parameter
|
||||
- identifier_name
|
||||
|
||||
file_length:
|
||||
warning: 850
|
||||
error: 1200
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
language: swift
|
||||
osx_image: xcode10.1
|
||||
osx_image: xcode10.2
|
||||
xcode_project: ScrollingContentViewController.xcodeproj
|
||||
xcode_scheme: ScrollingContentViewController
|
||||
xcode_destination: platform=iOS Simulator,OS=12.1,name=iPhone X
|
||||
|
||||
@@ -41,5 +41,7 @@
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIUserInterfaceStyle</key>
|
||||
<string>Light</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -73,30 +73,34 @@ class SignUpViewController: ScrollingContentViewController {
|
||||
configureTextFields()
|
||||
|
||||
signUpButton.setTitle("Sign Up", for: .normal)
|
||||
signUpButton.titleLabel?.font = UIFont.systemFont(ofSize: 17, weight: .medium)
|
||||
|
||||
addConstraints()
|
||||
}
|
||||
|
||||
private func configureTextFields() {
|
||||
configureTextField(nameTextField, placeholder: "Name", textContentType: .name, autocapitalizationType: .words, keyboardType: .default, isSecureTextEntry: false)
|
||||
configureTextField(emailTextField, placeholder: "Email", textContentType: .emailAddress, autocapitalizationType: .none, keyboardType: .emailAddress, isSecureTextEntry: false)
|
||||
configureTextField(passwordTextField, placeholder: "Password", textContentType: nil, autocapitalizationType: .none, keyboardType: .default, isSecureTextEntry: true)
|
||||
configureTextField(nameTextField, placeholder: "Name", textContentType: .name, autocapitalizationType: .words, returnKeyType: .next, keyboardType: .default, isSecureTextEntry: false)
|
||||
configureTextField(emailTextField, placeholder: "Email", textContentType: .emailAddress, autocapitalizationType: .none, returnKeyType: .next, keyboardType: .emailAddress, isSecureTextEntry: false)
|
||||
configureTextField(passwordTextField, placeholder: "Password", textContentType: nil, autocapitalizationType: .none, returnKeyType: .done, keyboardType: .default, isSecureTextEntry: true)
|
||||
}
|
||||
|
||||
private func configureTextField(_ textField: UITextField, placeholder: String?, textContentType: UITextContentType?, autocapitalizationType: UITextAutocapitalizationType, keyboardType: UIKeyboardType, isSecureTextEntry: Bool) {
|
||||
// swiftlint:disable:next function_parameter_count
|
||||
private func configureTextField(_ textField: UITextField, placeholder: String?, textContentType: UITextContentType?, autocapitalizationType: UITextAutocapitalizationType, returnKeyType: UIReturnKeyType, keyboardType: UIKeyboardType, isSecureTextEntry: Bool) {
|
||||
textField.placeholder = placeholder
|
||||
textField.textContentType = textContentType
|
||||
textField.autocapitalizationType = autocapitalizationType
|
||||
textField.autocorrectionType = .no
|
||||
textField.smartDashesType = .no
|
||||
textField.smartInsertDeleteType = .no
|
||||
textField.smartQuotesType = .no
|
||||
textField.spellCheckingType = .no
|
||||
textField.returnKeyType = .next
|
||||
textField.returnKeyType = returnKeyType
|
||||
textField.keyboardType = keyboardType
|
||||
textField.enablesReturnKeyAutomatically = true
|
||||
textField.isSecureTextEntry = isSecureTextEntry
|
||||
}
|
||||
|
||||
// swiftlint:disable:next function_body_length
|
||||
private func addConstraints() {
|
||||
logoImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
nameTextField.translatesAutoresizingMaskIntoConstraints = false
|
||||
@@ -153,13 +157,13 @@ class SignUpViewController: ScrollingContentViewController {
|
||||
|
||||
logoImageTopLayoutGuide.heightAnchor.constraint(equalTo: logoImageBottomLayoutGuide.heightAnchor),
|
||||
logoImageBottomLayoutGuide.heightAnchor.constraint(equalTo: signUpButtonBottomLayoutGuide.heightAnchor, multiplier: 2, constant: 0),
|
||||
signUpButtonBottomLayoutGuide.heightAnchor.constraint(greaterThanOrEqualToConstant: 16),
|
||||
signUpButtonBottomLayoutGuide.heightAnchor.constraint(greaterThanOrEqualToConstant: 16)
|
||||
]
|
||||
|
||||
logoImageView.setContentHuggingPriority(.required, for: .vertical)
|
||||
logoImageView.setContentHuggingPriority(.required, for: .horizontal)
|
||||
|
||||
contentView.addConstraints(constraints)
|
||||
NSLayoutConstraint.activate(constraints)
|
||||
}
|
||||
|
||||
private func addPillViewConstraints(to pillView: UIView) {
|
||||
|
||||
@@ -20,4 +20,3 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -43,5 +43,7 @@
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIUserInterfaceStyle</key>
|
||||
<string>Light</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -71,6 +71,14 @@ class SignUpViewController: UIViewController {
|
||||
signUpController = SignUpController(logoImageView: logoImageView, nameTextField: nameTextField, emailTextField: emailTextField, passwordTextField: passwordTextField, signUpButton: signUpButton, signInButton: signInButton, delegate: self)
|
||||
}
|
||||
|
||||
// Note: This method is not strictly required, but logs a warning if the content
|
||||
// view's size is undefined.
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
scrollingContentViewManager.viewWillAppear(animated)
|
||||
}
|
||||
|
||||
// Note: This is only required in apps that support device orientation changes.
|
||||
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
|
||||
super.viewWillTransition(to: size, with: coordinator)
|
||||
@@ -78,9 +86,9 @@ class SignUpViewController: UIViewController {
|
||||
scrollingContentViewManager.viewWillTransition(to: size, with: coordinator)
|
||||
}
|
||||
|
||||
/// Note: This is only required in apps with navigation controllers that are used to
|
||||
/// push sequences of view controllers with text fields that become the first
|
||||
/// responder in `viewWillAppear`.
|
||||
// Note: This is only required in apps with navigation controllers that are used to
|
||||
// push sequences of view controllers with text fields that become the first
|
||||
// responder in `viewWillAppear`.
|
||||
override func viewSafeAreaInsetsDidChange() {
|
||||
super.viewSafeAreaInsetsDidChange()
|
||||
|
||||
|
||||
@@ -41,5 +41,7 @@
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIUserInterfaceStyle</key>
|
||||
<string>Light</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" borderStyle="roundedRect" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="8XX-iU-ry4">
|
||||
<rect key="frame" x="47.5" y="128" width="280" height="30"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" priority="260" constant="280" id="C3T-U2-c38"/>
|
||||
<constraint firstAttribute="width" priority="750" constant="280" id="C3T-U2-c38"/>
|
||||
</constraints>
|
||||
<nil key="textColor"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
@@ -135,7 +135,7 @@
|
||||
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" borderStyle="roundedRect" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="mEM-VS-KHt">
|
||||
<rect key="frame" x="47.5" y="128" width="280" height="30"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" priority="260" constant="280" id="65G-Kd-aIC"/>
|
||||
<constraint firstAttribute="width" priority="750" constant="280" id="65G-Kd-aIC"/>
|
||||
</constraints>
|
||||
<nil key="textColor"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
@@ -233,7 +233,7 @@
|
||||
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" borderStyle="roundedRect" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="smT-RC-FvD">
|
||||
<rect key="frame" x="47.5" y="128" width="280" height="30"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" priority="260" constant="280" id="suP-3m-O8o"/>
|
||||
<constraint firstAttribute="width" priority="750" constant="280" id="suP-3m-O8o"/>
|
||||
</constraints>
|
||||
<nil key="textColor"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
|
||||
@@ -41,5 +41,7 @@
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIUserInterfaceStyle</key>
|
||||
<string>Light</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -64,7 +64,7 @@
|
||||
</constraints>
|
||||
<nil key="textColor"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
<textInputTraits key="textInputTraits" autocorrectionType="no" spellCheckingType="no" returnKeyType="next" enablesReturnKeyAutomatically="YES" secureTextEntry="YES" smartDashesType="no" smartInsertDeleteType="no" smartQuotesType="no"/>
|
||||
<textInputTraits key="textInputTraits" autocorrectionType="no" spellCheckingType="no" returnKeyType="done" enablesReturnKeyAutomatically="YES" secureTextEntry="YES" smartDashesType="no" smartInsertDeleteType="no" smartQuotesType="no"/>
|
||||
</textField>
|
||||
</subviews>
|
||||
</stackView>
|
||||
|
||||
@@ -35,7 +35,7 @@ class GradientBackgroundView: UIView {
|
||||
|
||||
gradientLayer.colors = [
|
||||
UIColor(red: 37/255, green: 176/255, blue: 176/255, alpha: 1).cgColor,
|
||||
UIColor(red: 72/255, green: 72/255, blue: 171/255, alpha: 1).cgColor,
|
||||
UIColor(red: 72/255, green: 72/255, blue: 171/255, alpha: 1).cgColor
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ class PillTextField: UITextField {
|
||||
private func updateAttributedPlaceholder() {
|
||||
let placeholderAttributes: [NSAttributedString.Key: Any] = [
|
||||
.foregroundColor: placeholderColor,
|
||||
.font: UIFont.systemFont(ofSize: 17, weight: .medium),
|
||||
.font: UIFont.systemFont(ofSize: 17, weight: .medium)
|
||||
]
|
||||
if let placeholder = placeholder {
|
||||
attributedPlaceholder = NSAttributedString(string: placeholder, attributes: placeholderAttributes)
|
||||
|
||||
@@ -44,6 +44,7 @@ class SignUpController: NSObject {
|
||||
passwordTextField.addTarget(self, action: #selector(updateSignUpButtonIsEnabledState), for: .editingChanged)
|
||||
|
||||
signUpButton.isEnabled = false
|
||||
signUpButton.addTarget(self, action: #selector(signUp), for: .touchUpInside)
|
||||
|
||||
configureSignInButton(signInButton)
|
||||
}
|
||||
@@ -62,6 +63,13 @@ class SignUpController: NSObject {
|
||||
signUpButton?.isEnabled = isEnabled
|
||||
}
|
||||
|
||||
@objc func signUp() {
|
||||
// Dismiss the keyboard.
|
||||
UIApplication.shared.keyWindow?.endEditing(true)
|
||||
|
||||
// In a real app, the sign up flow would continue here.
|
||||
}
|
||||
|
||||
/// If `true`, the text field contains the empty string, after trimming leading and
|
||||
/// trailing whitespace.
|
||||
private func textFieldIsEmpty(_ textField: UITextField) -> Bool {
|
||||
@@ -84,11 +92,11 @@ class SignUpController: NSObject {
|
||||
|
||||
let signInButtonTitleRegularFontAttributes: [NSAttributedString.Key: Any] = [
|
||||
.foregroundColor: signInButtonTitleColor,
|
||||
.font: UIFont.systemFont(ofSize: signInButtonTitleFontSize, weight: .regular),
|
||||
.font: UIFont.systemFont(ofSize: signInButtonTitleFontSize, weight: .regular)
|
||||
]
|
||||
let signInButtonTitleMediumFontAttributes: [NSAttributedString.Key: Any] = [
|
||||
.foregroundColor: signInButtonTitleColor,
|
||||
.font: UIFont.systemFont(ofSize: signInButtonTitleFontSize, weight: .medium),
|
||||
.font: UIFont.systemFont(ofSize: signInButtonTitleFontSize, weight: .medium)
|
||||
]
|
||||
|
||||
signInButtonTitle.append(NSAttributedString(string: "Already have an account? ", attributes: signInButtonTitleRegularFontAttributes))
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
import UIKit
|
||||
|
||||
/// Delegate for SignUpController.
|
||||
protocol SignUpControllerDelegate: class {
|
||||
protocol SignUpControllerDelegate: AnyObject {
|
||||
|
||||
/// Tells the delegate to scroll the scroll view so that the first responder becomes
|
||||
/// visible.
|
||||
|
||||
@@ -43,5 +43,7 @@
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIUserInterfaceStyle</key>
|
||||
<string>Light</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 88 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 139 KiB |
@@ -0,0 +1,31 @@
|
||||
// swift-tools-version:5.3
|
||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "ScrollingContentViewController",
|
||||
platforms: [
|
||||
.iOS(.v12)
|
||||
],
|
||||
products: [
|
||||
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
||||
.library(
|
||||
name: "ScrollingContentViewController",
|
||||
targets: ["ScrollingContentViewController"])
|
||||
],
|
||||
dependencies: [
|
||||
// Dependencies declare other packages that this package depends on.
|
||||
// .package(url: /* package url */, from: "1.0.0"),
|
||||
],
|
||||
targets: [
|
||||
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
|
||||
// Targets can depend on other targets in this package, and on products in packages this package depends on.
|
||||
.target(
|
||||
name: "ScrollingContentViewController",
|
||||
dependencies: []),
|
||||
.testTarget(
|
||||
name: "ScrollingContentViewControllerTests",
|
||||
dependencies: ["ScrollingContentViewController"])
|
||||
]
|
||||
)
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[](https://travis-ci.org/drewolbrich/ScrollingContentViewController)
|
||||
[](http://developer.apple.com/ios)
|
||||
[](https://developer.apple.com/swift)
|
||||
[](https://developer.apple.com/swift)
|
||||
[](LICENSE)
|
||||
[](http://twitter.com/drewolbrich)
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
* [Caveats](#caveats)
|
||||
* [Usage Without Subclassing](#usage-without-subclassing)
|
||||
* [Examples](#examples)
|
||||
* [Properties](#properties)
|
||||
* [View Controller Properties](#view-controller-properties)
|
||||
* [Scroll View Properties and Methods](#scroll-view-properties-and-methods)
|
||||
* [How It Works](#how-it-works)
|
||||
* [Special Cases Handled](#special-cases-handled)
|
||||
@@ -21,19 +21,21 @@
|
||||
|
||||
## Overview
|
||||
|
||||
ScrollingContentViewController makes it easy to create a view controller with a scrolling content view, or to convert an existing static view controller into one that scrolls. Most importantly, it takes care of several tricky undocumented edge cases involving the keyboard, navigation controllers, and device rotations.
|
||||
ScrollingContentViewController makes it easy to create a view controller with a single scrolling content view, or to convert an existing static view controller into one that scrolls. Most importantly, it takes care of several tricky undocumented edge cases involving the keyboard, navigation controllers, and device rotations.
|
||||
|
||||
## Background
|
||||
|
||||
A common UIKit Auto Layout task involves creating a view controller with a fixed layout that is too large to fit older, smaller devices, or devices in landscape orientation, or the area of the screen that remains visible when the keyboard is presented.
|
||||
A common UIKit Auto Layout task involves creating a view controller with a fixed layout that is too large to fit older, smaller devices, or devices in landscape orientation, or the area of the screen that remains visible when the keyboard is presented. The problem is compounded when [Dynamic Type](https://developer.apple.com/documentation/uikit/uifont/scaling_fonts_automatically) is used to support large font sizes.
|
||||
|
||||
For example, consider this sign up screen, which fits iPhone XS, but not iPhone SE with a keyboard:
|
||||
For example, consider this sign up screen, which fits iPhone Xs, but not iPhone SE with a keyboard:
|
||||
|
||||
<img src="https://github.com/drewolbrich/ScrollingContentViewController/raw/master/Images/Overview-Comparison.png" width="888px">
|
||||
|
||||
This case can be handled by nesting the view inside a scroll view. You can do this manually in Interface Builder, as described by Apple's [Working with Scroll Views](https://developer.apple.com/library/archive/documentation/UserExperience/Conceptual/AutolayoutPG/WorkingwithScrollViews.html) documentation, but many steps are required. If your view contains text fields, you'll have to write code to compensate for the keyboard when it's presented, as in [Managing the Keyboard](https://developer.apple.com/library/archive/documentation/StringsTextFonts/Conceptual/TextAndWebiPhoneOS/KeyboardManagement/KeyboardManagement.html#//apple_ref/doc/uid/TP40009542-CH5-SW3). However, handling the keyboard robustly is [surprisingly complicated](#keyboard-resize-filtering), especially if your app presents a sequence of screens with keyboards in the context of a navigation controller, or when device orientation support is required.
|
||||
This case can be handled by nesting the view inside a scroll view. You could do this manually in Interface Builder, as described by Apple's [Working with Scroll Views](https://developer.apple.com/library/archive/documentation/UserExperience/Conceptual/AutolayoutPG/WorkingwithScrollViews.html) documentation, but many steps are required. If your view contains text fields, you'll have to write code to adjust the view to compensate for the presented keyboard, as described in [Managing the Keyboard](https://developer.apple.com/library/archive/documentation/StringsTextFonts/Conceptual/TextAndWebiPhoneOS/KeyboardManagement/KeyboardManagement.html#//apple_ref/doc/uid/TP40009542-CH5-SW3). However, handling the keyboard robustly is [surprisingly complicated](#keyboard-resize-filtering), especially if your app presents a sequence of screens with keyboards in the context of a navigation controller, or when device orientation support is required.
|
||||
|
||||
To simplify this task, ScrollingContentViewController inserts the scroll view into the view hierarchy for you, along with all necessary Auto Layout constraints. When used in a storyboard, ScrollingContentViewController exposes a [`contentView`](#contentView) outlet that you connect to the view that you'd like make scrollable. Everything else is taken care of automatically.
|
||||
To simplify this task, ScrollingContentViewController inserts the scroll view into the view hierarchy for you at run time, along with all necessary Auto Layout constraints.
|
||||
|
||||
When used in a storyboard, ScrollingContentViewController exposes an outlet called [`contentView`](#contentView) that you connect to the view that you'd like to make scrollable. This may be the view controller's root view or an arbitrary subview. Everything else is taken care of automatically, including responding to keyboard presentation and device orientation changes.
|
||||
|
||||
ScrollingContentViewController can be configured using storyboards or entirely in code. The easiest way to use it is by subclassing the `ScrollingContentViewController` class instead of [`UIViewController`](https://developer.apple.com/documentation/uikit/uiviewcontroller). However, when this is not an option, a helper class called `ScrollingContentViewManager` can be composed with your existing view controller class instead.
|
||||
|
||||
@@ -47,11 +49,17 @@ To install ScrollingContentViewController using CocoaPods, add this line to your
|
||||
pod 'ScrollingContentViewController'
|
||||
```
|
||||
|
||||
To install using Carthage, add this to your Cartfile:
|
||||
|
||||
```
|
||||
github "drewolbrich/ScrollingContentViewController"
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Subclasses of `ScrollingContentViewController` may be configured using [storyboards](#storyboards) or in [code](#code).
|
||||
Subclasses of `ScrollingContentViewController` may be configured using [storyboards](#storyboards) or in [code](#code).
|
||||
|
||||
This library can also be used without subclassing, by composing the helper class `ScrollingContentViewManager` instead. See [Usage Without Subclassing](#usage-without-subclassing).
|
||||
This library may also be used without subclassing, by composing the helper class `ScrollingContentViewManager` instead. Refer to [Usage Without Subclassing](#usage-without-subclassing).
|
||||
|
||||
### Storyboards
|
||||
|
||||
@@ -63,13 +71,13 @@ To configure `ScrollingContentViewController` in a storyboard:
|
||||
import ScrollingContentViewController
|
||||
|
||||
class MyViewController: ScrollingContentViewController {
|
||||
|
||||
|
||||
// ...
|
||||
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
2. In Interface Builder's outline view, control-click your view controller and connect its [`contentView`](#contentView) outlet to your view controller's root view.
|
||||
2. In Interface Builder's outline view, control-click your view controller and connect its [`contentView`](#contentView) outlet to your view controller's root view or any other subview that you want to make scrollable.
|
||||
|
||||
<img src="https://github.com/drewolbrich/ScrollingContentViewController/raw/master/Images/Usage-Storyboards.png" width="471px">
|
||||
|
||||
@@ -78,14 +86,14 @@ To configure `ScrollingContentViewController` in a storyboard:
|
||||
```swift
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
4. At runtime, the `ScrollingContentViewController` property [`contentView`](#contentView) will now reference the superview of the controls that you laid out in Interface Builder. This superview will no longer be referenced by the [`view`](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621460-view) property, which will instead reference an empty root view behind the scrolling content view. If necessary, revise your code to reflect this change.
|
||||
4. At run time, the `ScrollingContentViewController` property [`contentView`](#contentView) will now reference the superview of the controls that you laid out in Interface Builder. This superview will no longer be referenced by the [`view`](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621460-view) property, which will instead reference an empty root view behind the scrolling content view. If necessary, revise your code to reflect this change.
|
||||
|
||||
Your content view will now scroll, provided that you ensure that the content view's Auto Layout constraints [sufficiently define its size](#auto-layout-considerations).
|
||||
Your content view will now scroll, provided that you ensure that the content view's Auto Layout constraints [sufficiently define its size](#auto-layout-considerations), and that this size is larger than the safe area.
|
||||
|
||||
### Code
|
||||
|
||||
@@ -97,24 +105,27 @@ To integrate `ScrollingContentViewController` programmatically:
|
||||
import ScrollingContentViewController
|
||||
|
||||
class MyViewController: ScrollingContentViewController {
|
||||
|
||||
|
||||
// ...
|
||||
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
2. In your view controller's [`viewDidLoad`](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621495-viewdidload) method, assign a new view to the [`contentView`](#contentView) property. Add all of your controls to this view instead of referencing the [`view`](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621460-view) property so they can scroll freely. The view controller's root view referenced by its [`view`](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621460-view) property now acts as a background view behind the scrolling content view.
|
||||
2. In your view controller's [`viewDidLoad`](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621495-viewdidload) method, assign a new view to the [`contentView`](#contentView) property. Add all of your controls to this view instead of referencing the [`view`](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621460-view) property so they can scroll freely. The view controller's root view referenced by its [`view`](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621460-view) property now acts as a background view behind the scrolling content view.
|
||||
|
||||
```swift
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
contentView = UIView()
|
||||
|
||||
|
||||
// Add all controls to contentView instead of view.
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
You may also assign [`contentView`](#contentView) to a subview of your view controller's root view, in which case only that subview will be made scrollable.
|
||||
|
||||
## Caveats
|
||||
|
||||
### Auto Layout Considerations
|
||||
@@ -127,7 +138,7 @@ If you'd like your content view to stretch to take advantage of the full visible
|
||||
|
||||
To determine the size of the scroll view's content size, ScrollingContentViewController creates width and height constraints with a relation greater than or equal to the width and height of the scroll view's safe area. The priority of these constraints is 500. Consequently, if you create an unbroken chain of constraints with priority [`defaultHigh`](https://developer.apple.com/documentation/uikit/uilayoutpriority/1622249-defaulthigh) (750) or [`required`](https://developer.apple.com/documentation/uikit/uilayoutpriority/1622241-required) (1000), they will take precedence over ScrollingContentViewController's internal minimum width and height constraints, and your content view will not stretch to fill the scroll view's safe area.
|
||||
|
||||
If the size of your view controller is intentionally highly constrained (e.g. consisting exclusively of constraints with `required` priority and lacking [`greaterThanOrEqual`](https://developer.apple.com/documentation/uikit/nslayoutconstraint/relation/greaterthanorequal) relation constraints), you may see Auto Layout constraint errors in Interface Builder if the constraints don't match the simulated size of the view, for example, when you switch between simulated device sizes. The easiest way to resolve this issue is to reduce the priority of one of your constraints. The value 240 is a good choice because it is lower than the default content hugging priority (250) and consequently, it will help avoid the undesirable behavior where text fields and labels without height constraints stretch vertically.
|
||||
If the size of your view controller is intentionally highly constrained (e.g. consisting exclusively of constraints with [`required`](https://developer.apple.com/documentation/uikit/uilayoutpriority/1622241-required) priority and lacking [`greaterThanOrEqual`](https://developer.apple.com/documentation/uikit/nslayoutconstraint/relation/greaterthanorequal) relation constraints), you may see Auto Layout constraint errors in Interface Builder if the constraints don't match the simulated size of the view, for example, when you switch between simulated device sizes. The easiest way to resolve this issue is to reduce the priority of one of your constraints. The value 240 is a good choice because it is lower than the default content hugging priority (250) and consequently, it will help avoid the undesirable behavior where text fields and labels without height constraints stretch vertically.
|
||||
|
||||
<img src="https://github.com/drewolbrich/ScrollingContentViewController/raw/master/Images/Usage-Auto-Layout-Considerations.png" width="663px">
|
||||
|
||||
@@ -135,7 +146,7 @@ If the size of your view controller is intentionally highly constrained (e.g. co
|
||||
|
||||
If you'd prefer not to use Auto Layout, the content view's size may be specified using [`intrinsicContentSize`](https://developer.apple.com/documentation/uikit/uiview/1622600-intrinsiccontentsize) instead of constraints.
|
||||
|
||||
The default `UIView` content hugging priority is [`defaultLow`](https://developer.apple.com/documentation/uikit/uilayoutpriority/1622250-defaultlow), and consequently, the content view's intrinisic content size will normally be overridden by the minimum size constraints that ScrollingContentViewController assigns. If you'd like `intrinsicContentSize` to take precedence over these constraints, set the content view's content hugging priority to [`required`](https://developer.apple.com/documentation/uikit/uilayoutpriority/1622241-required).
|
||||
The default `UIView` content hugging priority is [`defaultLow`](https://developer.apple.com/documentation/uikit/uilayoutpriority/1622250-defaultlow), and consequently, the content view's intrinisic content size will normally be overridden by the minimum size constraints that ScrollingContentViewController assigns. If you'd like [`intrinsicContentSize`](https://developer.apple.com/documentation/uikit/uiview/1622600-intrinsiccontentsize) to take precedence over these constraints, set the content view's content hugging priority to [`required`](https://developer.apple.com/documentation/uikit/uilayoutpriority/1622241-required).
|
||||
|
||||
### Changing the Background Color
|
||||
|
||||
@@ -163,7 +174,7 @@ If you make changes to your content view that modify its size, you must call the
|
||||
For example, after updating the view's [`NSLayoutConstraint.constant`](https://developer.apple.com/documentation/uikit/nslayoutconstraint/1526928-constant) properties, you may animate the changes like this:
|
||||
|
||||
```swift
|
||||
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 0,
|
||||
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 0,
|
||||
options: [], animations: {
|
||||
self.scrollView.setNeedsLayout()
|
||||
self.scrollView.layoutIfNeeded()
|
||||
@@ -174,7 +185,7 @@ UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSp
|
||||
|
||||
In Interface Builder, it's possible to design a view controller that is intentionally larger than the height of the screen. To do this, change the view controller's simulated size to Freeform and adjust its height. When used with ScrollingContentViewController, the view controller's oversized content view will scroll freely, assuming its constraints require it to be larger than the screen.
|
||||
|
||||
<img src="https://github.com/drewolbrich/ScrollingContentViewController/raw/master/Images/Usage-Oversized-View-Controllers.png" width="610px">
|
||||
<img src="https://github.com/drewolbrich/ScrollingContentViewController/raw/master/Images/Usage-Oversized-View-Controllers.png" width="609px">
|
||||
|
||||
## Usage Without Subclassing
|
||||
|
||||
@@ -209,6 +220,14 @@ class MyViewController: UIViewController {
|
||||
contentView.backgroundColor = nil
|
||||
}
|
||||
|
||||
// Note: This method is not strictly required, but logs a warning if the content
|
||||
// view's size is undefined.
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
scrollingContentViewManager.viewWillAppear(animated)
|
||||
}
|
||||
|
||||
// Note: This is only required in apps that support device orientation changes.
|
||||
override func viewWillTransition(to size: CGSize,
|
||||
with coordinator: UIViewControllerTransitionCoordinator) {
|
||||
@@ -217,9 +236,9 @@ class MyViewController: UIViewController {
|
||||
scrollingContentViewManager.viewWillTransition(to: size, with: coordinator)
|
||||
}
|
||||
|
||||
/// Note: This is only required in apps with navigation controllers that are used to
|
||||
/// push sequences of view controllers with text fields that become the first
|
||||
/// responder in `viewWillAppear`.
|
||||
// Note: This is only required in apps with navigation controllers that are used to
|
||||
// push sequences of view controllers with text fields that become the first
|
||||
// responder in `viewWillAppear`.
|
||||
override func viewSafeAreaInsetsDidChange() {
|
||||
super.viewSafeAreaInsetsDidChange()
|
||||
|
||||
@@ -229,7 +248,7 @@ class MyViewController: UIViewController {
|
||||
}
|
||||
```
|
||||
|
||||
The `ScrollingContentViewManager` class supports all of the same [properties](#properties) and [methods](#methods) as `ScrollingContentViewController`.
|
||||
The `ScrollingContentViewManager` class supports all of the same [properties](#view-controller-properties) and [methods](#scroll-view-properties-and-methods) as `ScrollingContentViewController`.
|
||||
|
||||
`ScrollingContentViewManager` can also be used to create a scrolling view controller programatically:
|
||||
|
||||
@@ -240,11 +259,11 @@ class MyViewController: UIViewController {
|
||||
|
||||
lazy var scrollingContentViewManager = ScrollingContentViewManager(hostViewController: self)
|
||||
|
||||
var contentView = UIView()
|
||||
let contentView = UIView()
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
|
||||
// Populate your content view here.
|
||||
// ...
|
||||
|
||||
@@ -254,6 +273,14 @@ class MyViewController: UIViewController {
|
||||
scrollingContentViewManager.contentView = contentView
|
||||
}
|
||||
|
||||
// Note: This method is not strictly required, but logs a warning if the content
|
||||
// view's size is undefined.
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
scrollingContentViewManager.viewWillAppear(animated)
|
||||
}
|
||||
|
||||
// Note: This is only required in apps that support device orientation changes.
|
||||
override func viewWillTransition(to size: CGSize,
|
||||
with coordinator: UIViewControllerTransitionCoordinator) {
|
||||
@@ -262,9 +289,9 @@ class MyViewController: UIViewController {
|
||||
scrollingContentViewManager.viewWillTransition(to: size, with: coordinator)
|
||||
}
|
||||
|
||||
/// Note: This is only required in apps with navigation controllers that are used to
|
||||
/// push sequences of view controllers with text fields that become the first
|
||||
/// responder in `viewWillAppear`.
|
||||
// Note: This is only required in apps with navigation controllers that are used to
|
||||
// push sequences of view controllers with text fields that become the first
|
||||
// responder in `viewWillAppear`.
|
||||
override func viewSafeAreaInsetsDidChange() {
|
||||
super.viewSafeAreaInsetsDidChange()
|
||||
|
||||
@@ -282,17 +309,17 @@ class MyViewController: UIViewController {
|
||||
|
||||
* [ManagerExample](Examples/ManagerExample) - Example using `ScrollingContentViewManager` and class composition instead of subclassing `ScrollingContentViewController`.
|
||||
|
||||
* [SequenceExample](Examples/SequenceExample) - Example of a sequence of pushed scrolling view controllers with keyboards in the context of a navigation controller.
|
||||
* [SequenceExample](Examples/SequenceExample) - Example of a sequence of pushed scrolling view controllers with keyboards in the context of a navigation controller.
|
||||
|
||||
* [ReassignExample](Examples/ReassignExample) - Example of dynamically reassigning `contentView`.
|
||||
|
||||
## Properties
|
||||
## View Controller Properties
|
||||
|
||||
The `ScrollingContentViewController` and `ScrollingContentViewManager` classes share the following properties:
|
||||
|
||||
### contentView
|
||||
|
||||
The scrolling content view parented to the scroll view.
|
||||
The scrolling content view parented to the scroll view.
|
||||
|
||||
When this property is first assigned, the view that it references is parented to [`scrollView`](#scrollView), which is then added to the view controller's view hierarchy.
|
||||
|
||||
@@ -300,7 +327,7 @@ If the content view already has a superview, the scroll view replaces it in the
|
||||
|
||||
If the content view has no superview, the scroll view is parented to the view controller's root view and its frame and autoresizing mask are defined to track the root view's bounds.
|
||||
|
||||
If the [`contentView`](#contentView) property is later reassigned, the new content view replaces the old one as the subview of the scroll view, and the scroll view is left otherwise unmodified.
|
||||
If the [`contentView`](#contentView) property is later reassigned, the new content view replaces the old one as the subview of the scroll view, and the scroll view is left otherwise unmodified.
|
||||
|
||||
### scrollView
|
||||
|
||||
@@ -364,7 +391,7 @@ When the content view is first assigned, if it has a superview, the scroll view
|
||||
|
||||
If the content view has no superview, the scroll view is parented to the view controller's root view and its frame and autoresizing mask are defined to track the root view's bounds.
|
||||
|
||||
If the ScrollingContentViewController's `contentView` property references its root view, a new `UIView` is allocated and replaces it as the root view so that the scroll view will have an appropriate view to be parented view.
|
||||
If the ScrollingContentViewController's `contentView` property references its root view, a new `UIView` is allocated and replaces it as the root view so that the scroll view will have an appropriate view to be parented to.
|
||||
|
||||
The content view's superview does not necessarily have to be the view controller's root view, and does not have to match the root view's size.
|
||||
|
||||
@@ -376,13 +403,13 @@ When the keyboard is presented, ScrollingContentViewController modifies the cont
|
||||
|
||||
Although ScrollingContentViewController modifies [`additionalSafeAreaInsets`](https://developer.apple.com/documentation/uikit/uiviewcontroller/2902284-additionalsafeareainsets) when the keyboard is presented, it restores it to its original value when the keyboard is dismissed. This allows [`additionalSafeAreaInsets`](https://developer.apple.com/documentation/uikit/uiviewcontroller/2902284-additionalsafeareainsets) to be used for other purposes, such as custom tool palettes.
|
||||
|
||||
During development, an alternate approach suggested by Apple, modifying the scroll view's content size, was also tried. This requires adjusting the scroll view's [`scrollIndicatorInsets`](https://developer.apple.com/documentation/uikit/uiscrollview/1619427-scrollindicatorinsets) property to compensate for the content size change. On iPhone XS in landscape orientation, doing so has the unfortunate side effect of awkwardly shifting the scroll indicator away from the edge of the screen.
|
||||
During development, an alternate approach suggested by Apple, modifying the scroll view's content size, was also tried. This requires adjusting the scroll view's [`scrollIndicatorInsets`](https://developer.apple.com/documentation/uikit/uiscrollview/1619427-scrollindicatorinsets) property to compensate for the content size change. Unfortunately, on iPhone Xs in landscape orientation, doing so has the side effect of awkwardly shifting the scroll indicator away from the edge of the screen.
|
||||
|
||||
### Keyboard Resize Filtering
|
||||
|
||||
When a text field becomes the first responder, UIKit presents the keyboard. If the user taps another text field, changing the first responder, UIKit may adjust the keyboard's height if an input accessory view is specified. These changes may generate a sequence of [`keyboardWillShow`](https://developer.apple.com/documentation/uikit/uiresponder/1621576-keyboardwillshownotification) notifications, each with different keyboard heights.
|
||||
|
||||
As an extreme example, if the user populates a text field by tapping on an AutoFill input accessory view, and this action causes a password text field to automatically become the first responder, one [`keyboardWillHide`](https://developer.apple.com/documentation/uikit/uikeyboardwillhidenotification) notification and two [`keyboardWillShow`](https://developer.apple.com/documentation/uikit/uiresponder/1621576-keyboardwillshownotification) notifications will be posted within a span of 0.1 seconds.
|
||||
As an extreme example, if the user populates an email text field by tapping on an AutoFill input accessory view item, and this action has the side effect of causing a password text field to become the first responder, one [`keyboardWillHide`](https://developer.apple.com/documentation/uikit/uikeyboardwillhidenotification) notification and two [`keyboardWillShow`](https://developer.apple.com/documentation/uikit/uiresponder/1621576-keyboardwillshownotification) notifications will be posted within a span of 0.1 seconds.
|
||||
|
||||
If ScrollingContentViewController were to respond to each of these notifications individually, this would cause awkward discontinuities in the scroll view animation that accompanies changes to the keyboard's height.
|
||||
|
||||
@@ -390,7 +417,7 @@ To work around this issue, ScrollingContentViewController filters out sequences
|
||||
|
||||
During a device orientation transition, a [`keyboardWillHide`](https://developer.apple.com/documentation/uikit/uikeyboardwillhidenotification) notification is posted before the animation starts, followed by [`keyboardWillShow`](https://developer.apple.com/documentation/uikit/uiresponder/1621576-keyboardwillshownotification) after it ends, even though the keyboard remains visible during the transition. Because the duration of the animation exceeds the filtering time window, it is therefore necessary to temporarily suspend filtering during the transition. Otherwise, the content view would resize unnecessarily.
|
||||
|
||||
Finally, ScrollingContentViewController correctly handles the case where changes to the size or layout of the scroll view's content may occur in response to keyboard presentation or device orientation changes (in particular when [`shouldResizeContentViewForKeyboard`](#shouldResizeContentViewForKeyboard) is `true`), invaliding the coordinate space of the rectangle passed to `scrollRectToVisible` (most importantly, in the case when that method is called automatically by iOS after keyboard changes) which would otherwise result in the scroll view scrolling by an inappropriate amount or leaving the scroll view with a content offset that is outside of the legal scrolling range.
|
||||
Finally, ScrollingContentViewController correctly handles the case where changes to the size or layout of the scroll view's content may occur in response to keyboard presentation or device orientation changes (in particular when [`shouldResizeContentViewForKeyboard`](#shouldResizeContentViewForKeyboard) is `true`), invalidating the coordinate space of the rectangle passed to [`scrollRectToVisible`](https://developer.apple.com/documentation/uikit/uiscrollview/1619439-scrollrecttovisible) (most importantly, in the case when that method is called automatically by iOS after keyboard changes) which would otherwise result in the scroll view scrolling by an inappropriate amount or leaving the scroll view with a content offset that is outside of the legal scrolling range.
|
||||
|
||||
Refer to Apple's [Managing the Keyboard](https://developer.apple.com/library/archive/documentation/StringsTextFonts/Conceptual/TextAndWebiPhoneOS/KeyboardManagement/KeyboardManagement.html#//apple_ref/doc/uid/TP40009542-CH5-SW3) documentation for more information about responding to changes in keyboard visibility.
|
||||
|
||||
@@ -400,13 +427,13 @@ In addition to [keyboard resize filtering](#keyboard-resize-filtering), above, S
|
||||
|
||||
### Navigation Controllers
|
||||
|
||||
ScrollingContentViewController correctly handles sequences of pushed view controllers in the context of a navigation controller, in particular in the case when each view controller calls a text field's [`becomeFirstResponder`](https://developer.apple.com/documentation/uikit/uiresponder/1621113-becomefirstresponder) method in [`viewWillAppear`](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621510-viewwillappear), such that the keyboard remains visible across view controller transitions.
|
||||
ScrollingContentViewController correctly handles sequences of pushed view controllers in the context of a navigation controller, in particular in the case when each view controller calls a text field's [`becomeFirstResponder`](https://developer.apple.com/documentation/uikit/uiresponder/1621113-becomefirstresponder) method in [`viewWillAppear`](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621510-viewwillappear), such that the keyboard remains visible across view controller transitions.
|
||||
|
||||
### Device Orientation Changes
|
||||
|
||||
When device orientation changes occur, ScrollingContentViewController improves upon the default scroll view behavior by pinning the upper left corner of the scroll view in place, while at the same time preventing out of range content offsets. This matches the behavior of many of Apple's iOS apps.
|
||||
|
||||
### keyboardDismissMode Enhancement
|
||||
### keyboardDismissMode
|
||||
|
||||
ScrollingContentViewController automatically enables [`UIScrollView.alwaysBounceVertical`](https://developer.apple.com/documentation/uikit/uiscrollview/1619383-alwaysbouncevertical) while the keyboard is presented if [`UIScrollView.keyboardDismissMode`](https://developer.apple.com/documentation/uikit/uiscrollview/1619437-keyboarddismissmode) is set to anything other than [`none`](https://developer.apple.com/documentation/uikit/uiscrollview/keyboarddismissmode/none), so the keyboard can be dismissed even if the view is too short to normally allow scrolling.
|
||||
|
||||
@@ -414,7 +441,7 @@ ScrollingContentViewController automatically enables [`UIScrollView.alwaysBounce
|
||||
|
||||
ScrollingContentViewController correctly handles the case when the scroll view doesn't cover the full extent of the screen, in which case it may only partially intersect the keyboard.
|
||||
|
||||
### Text Field Animation Artifact Fix
|
||||
### Text Field Animation Artifacts
|
||||
|
||||
As of iOS 12, if the user taps a sequence of custom text fields, UIKit may awkwardly animate the text field's text. ScrollingContentViewController suppresses this animation.
|
||||
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'ScrollingContentViewController'
|
||||
s.version = '1.0.0'
|
||||
s.summary = 'A Swift class that simplifies making a view controller\'s view scrollable'
|
||||
s.version = '1.6.0'
|
||||
s.summary = 'A Swift library that simplifies making a view controller\'s view scrollable'
|
||||
|
||||
s.description = <<-DESC
|
||||
ScrollingContentViewController makes it easy to create a view controller with a
|
||||
scrolling content view, or to convert an existing static view controller into
|
||||
one that scrolls. Most importantly, it takes care of several tricky undocumented
|
||||
edge cases involving the keyboard, navigation controllers, and device rotations.
|
||||
single scrolling content view, or to convert an existing static view controller
|
||||
into one that scrolls. Most importantly, it takes care of several tricky
|
||||
undocumented edge cases involving the keyboard, navigation controllers, and
|
||||
device rotations.
|
||||
DESC
|
||||
|
||||
s.homepage = 'https://github.com/drewolbrich/ScrollingContentViewController'
|
||||
@@ -16,11 +17,11 @@ edge cases involving the keyboard, navigation controllers, and device rotations.
|
||||
s.source = { :git => 'https://github.com/drewolbrich/ScrollingContentViewController.git', :tag => s.version.to_s }
|
||||
s.social_media_url = 'https://twitter.com/drewolbrich'
|
||||
|
||||
s.ios.deployment_target = '11.0'
|
||||
s.ios.deployment_target = '12.0'
|
||||
|
||||
s.source_files = 'Source/**/*.swift'
|
||||
s.source_files = 'Sources/**/*.swift'
|
||||
|
||||
s.frameworks = 'UIKit'
|
||||
|
||||
s.swift_version = '4.2'
|
||||
s.swift_versions = ['4.2', '5.0']
|
||||
end
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
3A83273921E700A000E8D95C /* SignUpController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A83273721E700A000E8D95C /* SignUpController.swift */; };
|
||||
3A83273B21E703F600E8D95C /* SignUpControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A83273A21E703F600E8D95C /* SignUpControllerDelegate.swift */; };
|
||||
3A83273C21E703F600E8D95C /* SignUpControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A83273A21E703F600E8D95C /* SignUpControllerDelegate.swift */; };
|
||||
3AAC048C21E2D3FD00D94DA5 /* LICENSE in Resources */ = {isa = PBXBuildFile; fileRef = 3AAC048921E2D3FD00D94DA5 /* LICENSE */; };
|
||||
3A881DB9224525F500E21CA2 /* InsetContentViewKeyboardTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A881DB72245257E00E21CA2 /* InsetContentViewKeyboardTests.swift */; };
|
||||
3AAC048F21E2D4C500D94DA5 /* ScrollingContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AAC048E21E2D4C500D94DA5 /* ScrollingContentViewController.swift */; };
|
||||
3AAC049121E2D4F100D94DA5 /* ScrollingContentViewManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AAC049021E2D4F100D94DA5 /* ScrollingContentViewManager.swift */; };
|
||||
3AAC049821E2F01C00D94DA5 /* UIResponder+Current.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AAC049321E2F01C00D94DA5 /* UIResponder+Current.swift */; };
|
||||
@@ -43,7 +43,7 @@
|
||||
3AAC049B21E2F01C00D94DA5 /* AdditionalSafeAreaInsetsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AAC049621E2F01C00D94DA5 /* AdditionalSafeAreaInsetsController.swift */; };
|
||||
3AAC049E21E2F18E00D94DA5 /* AdditionalSafeAreaInsetsControlling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AAC049D21E2F18E00D94DA5 /* AdditionalSafeAreaInsetsControlling.swift */; };
|
||||
3AAC04A021E2F28A00D94DA5 /* ScrollViewBounceControlling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AAC049F21E2F28A00D94DA5 /* ScrollViewBounceControlling.swift */; };
|
||||
3AAC04A221E2F30700D94DA5 /* KeyboardObservering.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AAC04A121E2F30700D94DA5 /* KeyboardObservering.swift */; };
|
||||
3AAC04A221E2F30700D94DA5 /* KeyboardObserving.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AAC04A121E2F30700D94DA5 /* KeyboardObserving.swift */; };
|
||||
3AAC04A421E301C400D94DA5 /* ScrollViewFilterKeyboardDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AAC04A321E301C400D94DA5 /* ScrollViewFilterKeyboardDelegate.swift */; };
|
||||
3AAC04AD21E3A01900D94DA5 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AAC04AC21E3A01900D94DA5 /* AppDelegate.swift */; };
|
||||
3AAC04BC21E3A07500D94DA5 /* ScrollingContentViewController.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3A5702CE21E2CBB600E4CC55 /* ScrollingContentViewController.framework */; };
|
||||
@@ -207,6 +207,7 @@
|
||||
3A5702D721E2CBB600E4CC55 /* ScrollingContentViewControllerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ScrollingContentViewControllerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
3A5702DC21E2CBB600E4CC55 /* StoryboardTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryboardTests.swift; sourceTree = "<group>"; };
|
||||
3A5702DE21E2CBB600E4CC55 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
3A5B3C98265D3D2100E26100 /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = "<group>"; };
|
||||
3A61C80621EB9028001F76A8 /* SecondViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecondViewController.swift; sourceTree = "<group>"; };
|
||||
3A61C80821EB9032001F76A8 /* ThirdViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThirdViewController.swift; sourceTree = "<group>"; };
|
||||
3A6273DF21E79757008EA567 /* ManagerExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ManagerExample.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
@@ -216,9 +217,10 @@
|
||||
3A7014BB21EBD723002C6740 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
3A83273721E700A000E8D95C /* SignUpController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignUpController.swift; sourceTree = "<group>"; };
|
||||
3A83273A21E703F600E8D95C /* SignUpControllerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignUpControllerDelegate.swift; sourceTree = "<group>"; };
|
||||
3A881DB72245257E00E21CA2 /* InsetContentViewKeyboardTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InsetContentViewKeyboardTests.swift; sourceTree = "<group>"; };
|
||||
3AAC048821E2D3FD00D94DA5 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
|
||||
3AAC048921E2D3FD00D94DA5 /* LICENSE */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE; sourceTree = "<group>"; };
|
||||
3AAC048A21E2D3FD00D94DA5 /* ScrollingContentViewController.podspec */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = ScrollingContentViewController.podspec; sourceTree = "<group>"; };
|
||||
3AAC048A21E2D3FD00D94DA5 /* ScrollingContentViewController.podspec */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; fileEncoding = 4; path = ScrollingContentViewController.podspec; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.ruby; };
|
||||
3AAC048E21E2D4C500D94DA5 /* ScrollingContentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollingContentViewController.swift; sourceTree = "<group>"; };
|
||||
3AAC049021E2D4F100D94DA5 /* ScrollingContentViewManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollingContentViewManager.swift; sourceTree = "<group>"; };
|
||||
3AAC049321E2F01C00D94DA5 /* UIResponder+Current.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIResponder+Current.swift"; sourceTree = "<group>"; };
|
||||
@@ -227,7 +229,7 @@
|
||||
3AAC049621E2F01C00D94DA5 /* AdditionalSafeAreaInsetsController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdditionalSafeAreaInsetsController.swift; sourceTree = "<group>"; };
|
||||
3AAC049D21E2F18E00D94DA5 /* AdditionalSafeAreaInsetsControlling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdditionalSafeAreaInsetsControlling.swift; sourceTree = "<group>"; };
|
||||
3AAC049F21E2F28A00D94DA5 /* ScrollViewBounceControlling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollViewBounceControlling.swift; sourceTree = "<group>"; };
|
||||
3AAC04A121E2F30700D94DA5 /* KeyboardObservering.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardObservering.swift; sourceTree = "<group>"; };
|
||||
3AAC04A121E2F30700D94DA5 /* KeyboardObserving.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardObserving.swift; sourceTree = "<group>"; };
|
||||
3AAC04A321E301C400D94DA5 /* ScrollViewFilterKeyboardDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollViewFilterKeyboardDelegate.swift; sourceTree = "<group>"; };
|
||||
3AAC04AA21E3A01900D94DA5 /* StoryboardExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = StoryboardExample.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
3AAC04AC21E3A01900D94DA5 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
@@ -244,6 +246,8 @@
|
||||
3AAC04F321E4518700D94DA5 /* SignUpViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SignUpViewController.swift; sourceTree = "<group>"; };
|
||||
3AC88F4F21E5B7F500EED460 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
3AC88F5021E5B7F600EED460 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
3ACE0D7B220B34BE0093FE5A /* .swiftlint.yml */ = {isa = PBXFileReference; lastKnownFileType = text; path = .swiftlint.yml; sourceTree = "<group>"; };
|
||||
3ACE0D7C220B34BE0093FE5A /* .travis.yml */ = {isa = PBXFileReference; lastKnownFileType = text; path = .travis.yml; sourceTree = "<group>"; };
|
||||
3AD597C821F3964000F220A0 /* CodeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodeTests.swift; sourceTree = "<group>"; };
|
||||
3AD597CA21F3995A00F220A0 /* ManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManagerTests.swift; sourceTree = "<group>"; };
|
||||
3AD597CC21F3AC2000F220A0 /* IntrinsicSizeContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntrinsicSizeContentView.swift; sourceTree = "<group>"; };
|
||||
@@ -335,8 +339,11 @@
|
||||
children = (
|
||||
3AAC048921E2D3FD00D94DA5 /* LICENSE */,
|
||||
3AAC048821E2D3FD00D94DA5 /* README.md */,
|
||||
3A5B3C98265D3D2100E26100 /* Package.swift */,
|
||||
3AAC048A21E2D3FD00D94DA5 /* ScrollingContentViewController.podspec */,
|
||||
3A5702D021E2CBB600E4CC55 /* Source */,
|
||||
3ACE0D7B220B34BE0093FE5A /* .swiftlint.yml */,
|
||||
3ACE0D7C220B34BE0093FE5A /* .travis.yml */,
|
||||
3A5702D021E2CBB600E4CC55 /* Sources */,
|
||||
3AAC04A521E39FBF00D94DA5 /* Examples */,
|
||||
3A5702DB21E2CBB600E4CC55 /* Tests */,
|
||||
3A5702CF21E2CBB600E4CC55 /* Products */,
|
||||
@@ -357,14 +364,40 @@
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
3A5702D021E2CBB600E4CC55 /* Source */ = {
|
||||
3A5702D021E2CBB600E4CC55 /* Sources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3A5B3CB9265D481400E26100 /* ScrollingContentViewController */,
|
||||
);
|
||||
path = Sources;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
3A5702DB21E2CBB600E4CC55 /* Tests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3A5702DC21E2CBB600E4CC55 /* StoryboardTests.swift */,
|
||||
3AD597C821F3964000F220A0 /* CodeTests.swift */,
|
||||
3AD597CA21F3995A00F220A0 /* ManagerTests.swift */,
|
||||
3AD597CE21F3AD8000F220A0 /* IntrinsicSizeTests.swift */,
|
||||
3AD597D021F3B70400F220A0 /* KeyboardTests.swift */,
|
||||
3A881DB72245257E00E21CA2 /* InsetContentViewKeyboardTests.swift */,
|
||||
3A3652F521F38E750010CE55 /* StoryboardTests.storyboard */,
|
||||
3A3652F721F390110010CE55 /* ContentView.swift */,
|
||||
3AD597CC21F3AC2000F220A0 /* IntrinsicSizeContentView.swift */,
|
||||
3A5702DE21E2CBB600E4CC55 /* Info.plist */,
|
||||
);
|
||||
path = Tests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
3A5B3CB9265D481400E26100 /* ScrollingContentViewController */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3A5702D221E2CBB600E4CC55 /* Info.plist */,
|
||||
3AAC048E21E2D4C500D94DA5 /* ScrollingContentViewController.swift */,
|
||||
3AAC049021E2D4F100D94DA5 /* ScrollingContentViewManager.swift */,
|
||||
3ADD380B21EBFE8D00396B7A /* ScrollingContentScrollView.swift */,
|
||||
3AAC049421E2F01C00D94DA5 /* KeyboardObserver.swift */,
|
||||
3AAC04A121E2F30700D94DA5 /* KeyboardObservering.swift */,
|
||||
3AAC04A121E2F30700D94DA5 /* KeyboardObserving.swift */,
|
||||
3AD597D221F420ED00F220A0 /* KeyboardNotificationManager.swift */,
|
||||
3AD597D421F4221B00F220A0 /* KeyboardNotificationObserving.swift */,
|
||||
3AF3AA3621FACCAF008AF677 /* ScrollViewFilter.swift */,
|
||||
@@ -379,25 +412,8 @@
|
||||
3AAC049321E2F01C00D94DA5 /* UIResponder+Current.swift */,
|
||||
3A06577B2200A552005BE8CC /* IsUnitTest.swift */,
|
||||
3A5702D121E2CBB600E4CC55 /* ScrollingContentViewController.h */,
|
||||
3A5702D221E2CBB600E4CC55 /* Info.plist */,
|
||||
);
|
||||
path = Source;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
3A5702DB21E2CBB600E4CC55 /* Tests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3A5702DC21E2CBB600E4CC55 /* StoryboardTests.swift */,
|
||||
3AD597C821F3964000F220A0 /* CodeTests.swift */,
|
||||
3AD597CA21F3995A00F220A0 /* ManagerTests.swift */,
|
||||
3AD597CE21F3AD8000F220A0 /* IntrinsicSizeTests.swift */,
|
||||
3AD597D021F3B70400F220A0 /* KeyboardTests.swift */,
|
||||
3A3652F521F38E750010CE55 /* StoryboardTests.storyboard */,
|
||||
3A3652F721F390110010CE55 /* ContentView.swift */,
|
||||
3AD597CC21F3AC2000F220A0 /* IntrinsicSizeContentView.swift */,
|
||||
3A5702DE21E2CBB600E4CC55 /* Info.plist */,
|
||||
);
|
||||
path = Tests;
|
||||
path = ScrollingContentViewController;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
3A6273E021E79757008EA567 /* ManagerExample */ = {
|
||||
@@ -509,6 +525,7 @@
|
||||
3A5702CA21E2CBB600E4CC55 /* Sources */,
|
||||
3A5702CB21E2CBB600E4CC55 /* Frameworks */,
|
||||
3A5702CC21E2CBB600E4CC55 /* Resources */,
|
||||
3ACE0D7A220B2D790093FE5A /* Run SwiftLint */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
@@ -639,30 +656,36 @@
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastSwiftUpdateCheck = 1010;
|
||||
LastUpgradeCheck = 1010;
|
||||
LastUpgradeCheck = 1250;
|
||||
ORGANIZATIONNAME = "Oath Inc.";
|
||||
TargetAttributes = {
|
||||
3A5702CD21E2CBB600E4CC55 = {
|
||||
CreatedOnToolsVersion = 10.1;
|
||||
LastSwiftMigration = 1010;
|
||||
LastSwiftMigration = 1020;
|
||||
};
|
||||
3A5702D621E2CBB600E4CC55 = {
|
||||
CreatedOnToolsVersion = 10.1;
|
||||
LastSwiftMigration = 1020;
|
||||
};
|
||||
3A6273DE21E79757008EA567 = {
|
||||
CreatedOnToolsVersion = 10.1;
|
||||
LastSwiftMigration = 1020;
|
||||
};
|
||||
3AAC04A921E3A01900D94DA5 = {
|
||||
CreatedOnToolsVersion = 10.1;
|
||||
LastSwiftMigration = 1020;
|
||||
};
|
||||
3AAC04D921E4514B00D94DA5 = {
|
||||
CreatedOnToolsVersion = 10.1;
|
||||
LastSwiftMigration = 1020;
|
||||
};
|
||||
3AE460E721EA952000B3E547 = {
|
||||
CreatedOnToolsVersion = 10.1;
|
||||
LastSwiftMigration = 1020;
|
||||
};
|
||||
3AE989822206443B006254A4 = {
|
||||
CreatedOnToolsVersion = 10.1;
|
||||
LastSwiftMigration = 1020;
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -695,7 +718,6 @@
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
3AAC048C21E2D3FD00D94DA5 /* LICENSE in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -758,6 +780,27 @@
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
3ACE0D7A220B2D790093FE5A /* Run SwiftLint */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Run SwiftLint";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "if which swiftlint >/dev/null; then\nswiftlint\nelse\necho \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n";
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
3A5702CA21E2CBB600E4CC55 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
@@ -775,7 +818,7 @@
|
||||
3AAC04A021E2F28A00D94DA5 /* ScrollViewBounceControlling.swift in Sources */,
|
||||
3AAC049E21E2F18E00D94DA5 /* AdditionalSafeAreaInsetsControlling.swift in Sources */,
|
||||
3AD597D521F4221B00F220A0 /* KeyboardNotificationObserving.swift in Sources */,
|
||||
3AAC04A221E2F30700D94DA5 /* KeyboardObservering.swift in Sources */,
|
||||
3AAC04A221E2F30700D94DA5 /* KeyboardObserving.swift in Sources */,
|
||||
3AD597D321F420ED00F220A0 /* KeyboardNotificationManager.swift in Sources */,
|
||||
3AF3AA3B21FCB76E008AF677 /* ScrollRectEvent.swift in Sources */,
|
||||
3A06577C2200A552005BE8CC /* IsUnitTest.swift in Sources */,
|
||||
@@ -791,6 +834,7 @@
|
||||
3A3652F821F390110010CE55 /* ContentView.swift in Sources */,
|
||||
3A5702DD21E2CBB600E4CC55 /* StoryboardTests.swift in Sources */,
|
||||
3AD597CF21F3AD8000F220A0 /* IntrinsicSizeTests.swift in Sources */,
|
||||
3A881DB9224525F500E21CA2 /* InsetContentViewKeyboardTests.swift in Sources */,
|
||||
3AD597D121F3B70400F220A0 /* KeyboardTests.swift in Sources */,
|
||||
3AD597C921F3964000F220A0 /* CodeTests.swift in Sources */,
|
||||
3AD597CD21F3AC2000F220A0 /* IntrinsicSizeContentView.swift in Sources */,
|
||||
@@ -970,6 +1014,7 @@
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
@@ -996,7 +1041,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
@@ -1034,6 +1079,7 @@
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
@@ -1054,7 +1100,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
@@ -1077,7 +1123,7 @@
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
DYLIB_CURRENT_VERSION = 1;
|
||||
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||
INFOPLIST_FILE = "$(SRCROOT)/Source/Info.plist";
|
||||
INFOPLIST_FILE = "$(SRCROOT)/Sources/ScrollingContentViewController/Info.plist";
|
||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
@@ -1088,7 +1134,7 @@
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 4.2;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
@@ -1104,7 +1150,7 @@
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
DYLIB_CURRENT_VERSION = 1;
|
||||
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||
INFOPLIST_FILE = "$(SRCROOT)/Source/Info.plist";
|
||||
INFOPLIST_FILE = "$(SRCROOT)/Sources/ScrollingContentViewController/Info.plist";
|
||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
@@ -1114,7 +1160,7 @@
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.retroactivefiasco.ScrollingContentViewController;
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_VERSION = 4.2;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
@@ -1133,7 +1179,7 @@
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.retroactivefiasco.ScrollingContentViewControllerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 4.2;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
@@ -1152,7 +1198,7 @@
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.retroactivefiasco.ScrollingContentViewControllerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 4.2;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
@@ -1171,7 +1217,7 @@
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.retroactivefiasco.ManagerExample;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 4.2;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
@@ -1190,7 +1236,7 @@
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.retroactivefiasco.ManagerExample;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 4.2;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
@@ -1209,7 +1255,7 @@
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.retroactivefiasco.StoryboardExample;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 4.2;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
@@ -1228,7 +1274,7 @@
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.retroactivefiasco.StoryboardExample;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 4.2;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
@@ -1247,7 +1293,7 @@
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.retroactivefiasco.CodeExample;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 4.2;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
@@ -1266,7 +1312,7 @@
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.retroactivefiasco.CodeExample;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 4.2;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
@@ -1285,7 +1331,7 @@
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.retroactivefiasco.SequenceExample;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 4.2;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
@@ -1304,7 +1350,7 @@
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.retroactivefiasco.SequenceExample;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 4.2;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
@@ -1323,7 +1369,7 @@
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.retroactivefiasco.ReassignExample;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 4.2;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
@@ -1342,7 +1388,7 @@
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.retroactivefiasco.ReassignExample;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 4.2;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
|
||||
+3
-7
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1010"
|
||||
LastUpgradeVersion = "1250"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
@@ -27,8 +27,6 @@
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
@@ -38,8 +36,8 @@
|
||||
ReferencedContainer = "container:ScrollingContentViewController.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
<Testables>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
@@ -61,8 +59,6 @@
|
||||
ReferencedContainer = "container:ScrollingContentViewController.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
|
||||
+3
-7
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1010"
|
||||
LastUpgradeVersion = "1250"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
@@ -27,8 +27,6 @@
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
@@ -38,8 +36,8 @@
|
||||
ReferencedContainer = "container:ScrollingContentViewController.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
<Testables>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
@@ -61,8 +59,6 @@
|
||||
ReferencedContainer = "container:ScrollingContentViewController.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
|
||||
+12
-16
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1010"
|
||||
LastUpgradeVersion = "1250"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
@@ -26,8 +26,17 @@
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
codeCoverageEnabled = "YES"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
codeCoverageEnabled = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "3A5702CD21E2CBB600E4CC55"
|
||||
BuildableName = "ScrollingContentViewController.framework"
|
||||
BlueprintName = "ScrollingContentViewController"
|
||||
ReferencedContainer = "container:ScrollingContentViewController.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
@@ -40,17 +49,6 @@
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "3A5702CD21E2CBB600E4CC55"
|
||||
BuildableName = "ScrollingContentViewController.framework"
|
||||
BlueprintName = "ScrollingContentViewController"
|
||||
ReferencedContainer = "container:ScrollingContentViewController.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
@@ -71,8 +69,6 @@
|
||||
ReferencedContainer = "container:ScrollingContentViewController.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
|
||||
+3
-7
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1010"
|
||||
LastUpgradeVersion = "1250"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
@@ -27,8 +27,6 @@
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
@@ -38,8 +36,8 @@
|
||||
ReferencedContainer = "container:ScrollingContentViewController.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
<Testables>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
@@ -61,8 +59,6 @@
|
||||
ReferencedContainer = "container:ScrollingContentViewController.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
|
||||
+3
-7
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1010"
|
||||
LastUpgradeVersion = "1250"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
@@ -27,8 +27,6 @@
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
@@ -38,8 +36,8 @@
|
||||
ReferencedContainer = "container:ScrollingContentViewController.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
<Testables>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
@@ -61,8 +59,6 @@
|
||||
ReferencedContainer = "container:ScrollingContentViewController.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
//
|
||||
// AdditionalSafeAreaInsetsControlling.swift
|
||||
// ScrollingContentViewController
|
||||
//
|
||||
// Created by Drew Olbrich on 1/6/19.
|
||||
// Copyright 2019 Oath Inc.
|
||||
//
|
||||
// Licensed under the terms of the MIT License. See the file LICENSE for the full terms.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
/// Delegate for `AdditionalSafeAreaInsetsController`.
|
||||
internal protocol AdditionalSafeAreaInsetsControlling: class {
|
||||
|
||||
/// 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 }
|
||||
|
||||
}
|
||||
+18
-17
@@ -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 hostViewController = delegate?.hostViewController else {
|
||||
return
|
||||
}
|
||||
|
||||
var adjustedBottomInset = bottomInset
|
||||
@@ -40,6 +40,8 @@ internal class AdditionalSafeAreaInsetsController {
|
||||
let initialAdditionalSafeAreaInsets = hostViewController.additionalSafeAreaInsets
|
||||
self.initialAdditionalSafeAreaInsets = initialAdditionalSafeAreaInsets
|
||||
adjustedBottomInset = max(adjustedBottomInset, initialAdditionalSafeAreaInsets.bottom)
|
||||
self.delegate?.additionalSafeAreaInsetsControllerWillUpdateAdditionalSafeAreaInsetsForPresentedKeyboard(self)
|
||||
setAdditionalSafeAreaBottomInset(adjustedBottomInset)
|
||||
} else if bottomInset == 0 && oldValue != 0 {
|
||||
// The keyboard was dismissed.
|
||||
guard let initialAdditionalSafeAreaInsets = initialAdditionalSafeAreaInsets else {
|
||||
@@ -48,6 +50,8 @@ internal class AdditionalSafeAreaInsetsController {
|
||||
}
|
||||
adjustedBottomInset = initialAdditionalSafeAreaInsets.bottom
|
||||
self.initialAdditionalSafeAreaInsets = nil
|
||||
setAdditionalSafeAreaBottomInset(adjustedBottomInset)
|
||||
self.delegate?.additionalSafeAreaInsetsControllerDidUpdateAdditionalSafeAreaInsetsForDismissedKeyboard(self)
|
||||
} else if bottomInset != oldValue {
|
||||
// The keyboard changed size.
|
||||
guard let initialAdditionalSafeAreaInset = initialAdditionalSafeAreaInsets else {
|
||||
@@ -55,23 +59,20 @@ internal class AdditionalSafeAreaInsetsController {
|
||||
return
|
||||
}
|
||||
adjustedBottomInset = max(adjustedBottomInset, initialAdditionalSafeAreaInset.bottom)
|
||||
setAdditionalSafeAreaBottomInset(adjustedBottomInset)
|
||||
|
||||
} else {
|
||||
// The size of the keyboard is unchanged.
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func setAdditionalSafeAreaBottomInset(_ additionalSafeAreaBottomInset: CGFloat) {
|
||||
guard let hostViewController = delegate?.hostViewController else {
|
||||
return
|
||||
}
|
||||
hostViewController.additionalSafeAreaInsets.bottom = additionalSafeAreaBottomInset
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
//
|
||||
// AdditionalSafeAreaInsetsControlling.swift
|
||||
// ScrollingContentViewController
|
||||
//
|
||||
// Created by Drew Olbrich on 1/6/19.
|
||||
// Copyright 2019 Oath Inc.
|
||||
//
|
||||
// Licensed under the terms of the MIT License. See the file LICENSE for the full terms.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
/// Delegate for `AdditionalSafeAreaInsetsController`.
|
||||
internal protocol AdditionalSafeAreaInsetsControlling: AnyObject {
|
||||
|
||||
/// The view controller whose `additionalSafeAreaInsets` property is manipulated.
|
||||
var hostViewController: UIViewController? { get }
|
||||
|
||||
/// Tells the delegate that the host view controller's additional safe area insets
|
||||
/// are about to be updated because the keyboard has been presented.
|
||||
func additionalSafeAreaInsetsControllerWillUpdateAdditionalSafeAreaInsetsForPresentedKeyboard(_ additionalSafeAreaInsetsController: AdditionalSafeAreaInsetsController)
|
||||
|
||||
/// Tells the delegate that the host view controller's additional safe area insets
|
||||
/// have been restored to their original values after the keyboard was dismissed.
|
||||
func additionalSafeAreaInsetsControllerDidUpdateAdditionalSafeAreaInsetsForDismissedKeyboard(_ additionalSafeAreaInsetsController: AdditionalSafeAreaInsetsController)
|
||||
|
||||
}
|
||||
@@ -15,7 +15,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0.0</string>
|
||||
<string>1.4.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
</dict>
|
||||
+1
-1
@@ -8,7 +8,7 @@
|
||||
// Licensed under the terms of the MIT License. See the file LICENSE for the full terms.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
/// An event encapsulating both the keyboard's frame and the duration of the
|
||||
/// animation accompanying the change in the keyboard's frame, as reported by the
|
||||
+1
-1
@@ -12,7 +12,7 @@ import Foundation
|
||||
|
||||
/// A protocol for objects that should be notified by `KeyboardNotificationManager`
|
||||
/// when keyboard show or hide notifications are received.
|
||||
internal protocol KeyboardNotificationObserving: class {
|
||||
internal protocol KeyboardNotificationObserving: AnyObject {
|
||||
|
||||
/// Tells the observer that a keyboard notification has been received.
|
||||
func didReceiveKeyboardNotification(_ notification: Notification)
|
||||
+32
-31
@@ -20,7 +20,7 @@ internal class KeyboardObserver: NSObject {
|
||||
|
||||
// See https://developer.apple.com/library/archive/documentation/StringsTextFonts/Conceptual/TextAndWebiPhoneOS/KeyboardManagement/KeyboardManagement.html#//apple_ref/doc/uid/TP40009542-CH5-SW3
|
||||
|
||||
private weak var delegate: KeyboardObservering?
|
||||
private weak var delegate: KeyboardObserving?
|
||||
|
||||
private weak var scrollViewFilter: ScrollViewFilter?
|
||||
|
||||
@@ -31,7 +31,7 @@ internal class KeyboardObserver: NSObject {
|
||||
/// calls to `updateForCurrentKeyboardVisibility`.
|
||||
private var isAdjustingViewForKeyboardFrameEvent = false
|
||||
|
||||
init(scrollViewFilter: ScrollViewFilter, delegate: KeyboardObservering) {
|
||||
init(scrollViewFilter: ScrollViewFilter, delegate: KeyboardObserving) {
|
||||
super.init()
|
||||
|
||||
self.scrollViewFilter = scrollViewFilter
|
||||
@@ -120,7 +120,7 @@ internal class KeyboardObserver: NSObject {
|
||||
}
|
||||
}
|
||||
|
||||
// Continues in adjustViewForKeyboard(withKeyboardFrameEvent:)...
|
||||
// Continues in scrollViewFilter(_:adjustViewForKeyboardFrameEvent:)...
|
||||
}
|
||||
|
||||
/// Updates the view controller to compensate for the current state of the keyboard.
|
||||
@@ -142,11 +142,11 @@ internal class KeyboardObserver: NSObject {
|
||||
scrollViewFilter.flush()
|
||||
}
|
||||
|
||||
// Continues in adjustViewForKeyboard(withKeyboardFrameEvent:)...
|
||||
// Continues in scrollViewFilter(_:adjustViewForKeyboardFrameEvent:)...
|
||||
}
|
||||
}
|
||||
|
||||
/// Tests submitting a keyboard frame event.
|
||||
/// Tests submitting a keyboard frame event. This method is used by unit tests only.
|
||||
internal func testKeyboardFrameEvent(_ keyboardFrameEvent: KeyboardFrameEvent) {
|
||||
// This method is intended for use in unit tests only.
|
||||
assert(isUnitTest)
|
||||
@@ -158,7 +158,7 @@ internal class KeyboardObserver: NSObject {
|
||||
scrollViewFilter.submitKeyboardFrameEvent(keyboardFrameEvent)
|
||||
scrollViewFilter.flush()
|
||||
|
||||
// Continues in adjustViewForKeyboard(withKeyboardFrameEvent:)...
|
||||
// Continues in scrollViewFilter(_:adjustViewForKeyboardFrameEvent:)...
|
||||
}
|
||||
|
||||
/// Suppresses unwanted text field text animation.
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
+5
-5
@@ -1,5 +1,5 @@
|
||||
//
|
||||
// KeyboardObservering.swift
|
||||
// KeyboardObserving.swift
|
||||
// ScrollingContentViewController
|
||||
//
|
||||
// Created by Drew Olbrich on 1/6/19.
|
||||
@@ -11,7 +11,7 @@
|
||||
import UIKit
|
||||
|
||||
/// Delegate for `KeyboardObserver`.
|
||||
internal protocol KeyboardObservering: class {
|
||||
internal protocol KeyboardObserving: AnyObject {
|
||||
|
||||
/// View controller over top of which the keyboard is presented.
|
||||
var hostViewController: UIViewController? { get }
|
||||
@@ -26,11 +26,11 @@ internal protocol KeyboardObservering: class {
|
||||
/// the scroll view obscured by the presented keyboard, if possible.
|
||||
var shouldResizeContentViewForKeyboard: Bool { get }
|
||||
|
||||
/// Adjusts the view to compensate for the portion of the keyboard that overlaps the
|
||||
/// scroll view.
|
||||
/// Adjusts the view controller to compensate for the portion of the keyboard that
|
||||
/// overlaps the view controller's root view.
|
||||
///
|
||||
/// - Parameter bottomInset: The height of the vertical extent of the keyboard that
|
||||
/// overlaps the scroll view.
|
||||
/// overlaps the view controller's root view.
|
||||
func adjustViewForKeyboard(withBottomInset bottomInset: CGFloat)
|
||||
|
||||
}
|
||||
+3
-2
@@ -8,7 +8,7 @@
|
||||
// Licensed under the terms of the MIT License. See the file LICENSE for the full terms.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
/// An event encapsulating a deferred call to `scrollRectToVisible(_:animated:)`.
|
||||
internal struct ScrollRectEvent {
|
||||
@@ -20,7 +20,8 @@ internal struct ScrollRectEvent {
|
||||
|
||||
/// The content view should be scrolled to make visible a rectangle in the
|
||||
/// coordinate space of the bounds of a descendant view of the content view.
|
||||
case descendantViewRect(_ rect: CGRect, descendantView: UIView)
|
||||
/// If `rect` is nil, the bounds of the descendant view is made visible.
|
||||
case descendantViewRect(_ rect: CGRect?, descendantView: UIView)
|
||||
}
|
||||
|
||||
/// The area of the scroll view's content to make visible.
|
||||
+1
-1
@@ -11,7 +11,7 @@
|
||||
import UIKit
|
||||
|
||||
/// Delegate for `ScrollViewBounceController`.
|
||||
internal protocol ScrollViewBounceControlling: class {
|
||||
internal protocol ScrollViewBounceControlling: AnyObject {
|
||||
|
||||
/// Scroll view whose `alwaysBounceVertical` property is manipulated.
|
||||
var scrollView: ScrollingContentScrollView { get }
|
||||
+1
-1
@@ -241,7 +241,7 @@ internal class ScrollViewFilter {
|
||||
self.keyboardFrameEvent = nil
|
||||
keyboardDelegate?.scrollViewFilter(self, adjustViewForKeyboardFrameEvent: keyboardFrameEvent)
|
||||
}
|
||||
|
||||
|
||||
if let scrollRectEvent = scrollRectEvent {
|
||||
// Note: It's possible that the call to adjustViewForKeyboardFrameEvent, above,
|
||||
// results in a new call to submitScrollRectEvent which will be immediately handled
|
||||
+1
-1
@@ -12,7 +12,7 @@ import UIKit
|
||||
|
||||
/// A protocol that an object implements to be notified by `ScrollViewFilter` about
|
||||
/// keyboard frame changes.
|
||||
internal protocol ScrollViewFilterKeyboardDelegate: class {
|
||||
internal protocol ScrollViewFilterKeyboardDelegate: AnyObject {
|
||||
|
||||
/// Adjusts the view to compensate for the portion of the keyboard that overlaps the
|
||||
/// scroll view.
|
||||
+1
-1
@@ -13,7 +13,7 @@ import UIKit
|
||||
/// A protocol that an object implements to be notified by `ScrollViewFilter` about
|
||||
/// requests to scroll a specific area of the content so that it is visible in the
|
||||
/// scroll view.
|
||||
internal protocol ScrollViewFilterScrollDelegate: class {
|
||||
internal protocol ScrollViewFilterScrollDelegate: AnyObject {
|
||||
|
||||
/// Scrolls a specific area of the content so that it is visible in the scroll view.
|
||||
func scrollViewFilter(_ scrollViewFilter: ScrollViewFilter, adjustViewForScrollRectEvent scrollRectEvent: ScrollRectEvent)
|
||||
+21
-9
@@ -49,7 +49,7 @@ public class ScrollingContentScrollView: UIScrollView {
|
||||
// controller, changes to the size of the content view will result in the content
|
||||
// view's safe area insets changing unpredictably. The always behavior is chosen
|
||||
// here instead of never because unlike never, the always behavior adjusts the
|
||||
// scroll indicator insets, which is desirable, in particular on iPhone XS in
|
||||
// scroll indicator insets, which is desirable, in particular on iPhone Xs in
|
||||
// landscape orientation with the keyboard presented.
|
||||
contentInsetAdjustmentBehavior = .always
|
||||
}
|
||||
@@ -80,8 +80,16 @@ public class ScrollingContentScrollView: UIScrollView {
|
||||
/// - margin: An optional margin around `rect` that should also be made visible.
|
||||
/// If `nil`, the value of `visibilityScrollMargin` is used.
|
||||
public func scrollRectToVisible(_ rect: CGRect, animated: Bool, margin: CGFloat? = nil) {
|
||||
if let descendantView = self.descendentView(of: self, containing: rect, in: self) {
|
||||
let rect = descendantView.convert(rect, from: self)
|
||||
if let descendantView = self.descendantView(of: self, containing: rect, in: self) {
|
||||
// If the rect matches the bounds of the descendant view, we'll substitute it with
|
||||
// nil, which will be replaced with the bounds of the descendant view when it is
|
||||
// processed later. The handles the case where the descendant view is resized
|
||||
// between the time when self.scrollRectToVisible and super.scrollRectToVisible are
|
||||
// called.
|
||||
// Note: This does not handle the case where the rect is smaller than the
|
||||
// descendant view's bounds and the size of the descendant view changes.
|
||||
let boundsRect = descendantView.convert(rect, from: self)
|
||||
let rect: CGRect? = boundsRect == descendantView.bounds ? nil : boundsRect
|
||||
scrollViewFilter?.submitScrollRectEvent(ScrollRectEvent(contentArea: .descendantViewRect(rect, descendantView: descendantView), animated: animated, margin: margin ?? visibilityScrollMargin))
|
||||
|
||||
/// Continues in scrollViewFilter(_:adjustViewForScrollRectEvent:)...
|
||||
@@ -90,6 +98,8 @@ public class ScrollingContentScrollView: UIScrollView {
|
||||
|
||||
// No appropriate descendant view could be found, so `rect` is assumed to be defined
|
||||
// in the space of the scroll view's content area.
|
||||
// Note: This does not handle the case where the size of the scroll view content
|
||||
// area changes.
|
||||
scrollViewFilter?.submitScrollRectEvent(ScrollRectEvent(contentArea: .scrollViewRect(rect), animated: animated, margin: margin ?? visibilityScrollMargin))
|
||||
|
||||
/// Continues in scrollViewFilter(_:adjustViewForScrollRectEvent:)...
|
||||
@@ -123,7 +133,7 @@ public class ScrollingContentScrollView: UIScrollView {
|
||||
scrollViewToVisible(view, animated: animated, margin: margin)
|
||||
}
|
||||
|
||||
/// Returns the descedant view with the greatest depth whose bounds contains the
|
||||
/// Returns the descendant view with the greatest depth whose bounds contains the
|
||||
/// specified rectangle.
|
||||
///
|
||||
/// - Parameters:
|
||||
@@ -131,13 +141,13 @@ public class ScrollingContentScrollView: UIScrollView {
|
||||
/// - rect: The rectangle to search for, defined in the coordinate space of `rectView`.
|
||||
/// - rectView: The view that defines the coordinate space in which `rect` is defined.
|
||||
/// - Returns: The descendant view that contains `rect`.
|
||||
private func descendentView(of view: UIView, containing rect: CGRect, in rectView: UIView) -> UIView? {
|
||||
private func descendantView(of view: UIView, containing rect: CGRect, in rectView: UIView) -> UIView? {
|
||||
let frame = rectView.convert(rect, to: view)
|
||||
for subview in view.subviews {
|
||||
// Perform a depth first search so the descendant view with the greatest depth that
|
||||
// contains the rectangle will be found first.
|
||||
if let descendentView = descendentView(of: subview, containing: rect, in: rectView) {
|
||||
return descendentView
|
||||
if let descendantView = descendantView(of: subview, containing: rect, in: rectView) {
|
||||
return descendantView
|
||||
}
|
||||
if subview.frame.contains(frame) {
|
||||
return subview
|
||||
@@ -156,10 +166,12 @@ extension ScrollingContentScrollView: ScrollViewFilterScrollDelegate {
|
||||
switch scrollRectEvent.contentArea {
|
||||
case .scrollViewRect(let rect):
|
||||
scrollViewRect = rect
|
||||
break
|
||||
case .descendantViewRect(let rect, let descendantView):
|
||||
// If rect is nil, make the entire descendant view visible.
|
||||
// This handles the case where the descendant view has changed
|
||||
// size since scrollRectToVisible was called.
|
||||
let rect = rect ?? descendantView.bounds
|
||||
scrollViewRect = convert(rect, from: descendantView)
|
||||
break
|
||||
}
|
||||
|
||||
scrollViewRect = scrollViewRect.insetBy(dx: 0, dy: -scrollRectEvent.margin)
|
||||
+2
@@ -111,6 +111,8 @@ open class ScrollingContentViewController: UIViewController {
|
||||
assert(contentView != nil, "Either contentView must be assigned in viewDidLoad, or the contentView outlet must be connected in Interface Builder")
|
||||
|
||||
assert(scrollingContentViewManager.contentView != nil, "The content view was not added to the view hierarchy. Did you forget to call super in viewDidLoad?")
|
||||
|
||||
scrollingContentViewManager.viewWillAppear(animated)
|
||||
}
|
||||
|
||||
/// If you override this method, you must call `super` at some point in your
|
||||
+91
-39
@@ -19,7 +19,7 @@ import UIKit
|
||||
/// `ScrollingContentViewManager` may be used instead.
|
||||
///
|
||||
/// See [https://github.com/drewolbrich/ScrollingContentViewController](https://github.com/drewolbrich/ScrollingContentViewController/blob/master/README.md) for full documentation.
|
||||
public class ScrollingContentViewManager: KeyboardObservering, ScrollViewBounceControlling, AdditionalSafeAreaInsetsControlling {
|
||||
public class ScrollingContentViewManager: KeyboardObserving, ScrollViewBounceControlling, AdditionalSafeAreaInsetsControlling {
|
||||
|
||||
/// The view controller that hosts the scroll view.
|
||||
public private(set) weak var hostViewController: UIViewController?
|
||||
@@ -82,23 +82,26 @@ public class ScrollingContentViewManager: KeyboardObservering, ScrollViewBounceC
|
||||
/// The default value is `true`.
|
||||
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?
|
||||
/// A constraint that enforces a minimum width for the content view equal to the
|
||||
/// scroll view's safe area width.
|
||||
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 safe area height.
|
||||
private var contentViewMinimumHeightConstraint: NSLayoutConstraint?
|
||||
|
||||
/// When the keyboard is presented, if `shouldResizeContentViewForKeyboard` is
|
||||
/// false, this constraint is assigned to the current height of the content view. It
|
||||
/// is deactivated when the keyboard is dismissed. This prevents the content view
|
||||
/// from shrinking in response to the presented keyboard.
|
||||
private var contentViewMinimumHeightForPresentedKeyboardConstraint: 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.
|
||||
///
|
||||
/// This property's access control level is `internal` so it can be accessed by unit
|
||||
/// tests.
|
||||
internal(set) var keyboardObserver: KeyboardObserver?
|
||||
internal var keyboardObserver: KeyboardObserver?
|
||||
|
||||
/// An object that modifies the scroll view's `alwaysBounceVertical` property to
|
||||
/// reflect the state of the presented keyboard.
|
||||
@@ -191,22 +194,29 @@ public class ScrollingContentViewManager: KeyboardObservering, ScrollViewBounceC
|
||||
}
|
||||
|
||||
if hostViewController.view == contentView {
|
||||
hostViewController.view = defaultRootView
|
||||
hostViewController.view = substitutionRootView(for: contentView)
|
||||
}
|
||||
}
|
||||
|
||||
/// A root view that is substituted for the content view in the case that the
|
||||
/// content view and the root view are the same.
|
||||
private lazy var defaultRootView: UIView = {
|
||||
/// Creates a root view that is substituted for the content view in the case that
|
||||
/// the content view and the root view are the same.
|
||||
///
|
||||
/// - Parameter contentView: The content view to base the root view on
|
||||
/// - Returns: A root view
|
||||
private func substitutionRootView(for contentView: UIView?) -> UIView {
|
||||
let rootView = UIView()
|
||||
|
||||
if let contentView = contentView {
|
||||
rootView.frame = contentView.frame
|
||||
}
|
||||
|
||||
// By default, UIView.backgroundColor is nil, which in the general case would allow
|
||||
// black pixels to be seen behind the view, so here it is changed to white, which
|
||||
// is the default for UIViewController root views created by Interface Builder.
|
||||
rootView.backgroundColor = .white
|
||||
|
||||
return rootView
|
||||
}()
|
||||
}
|
||||
|
||||
/// Adds an initial content view as a subview of the scroll view.
|
||||
///
|
||||
@@ -231,17 +241,6 @@ public class ScrollingContentViewManager: KeyboardObservering, ScrollViewBounceC
|
||||
return
|
||||
}
|
||||
|
||||
let contentViewSystemLayoutSize = contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
|
||||
let contentViewIntrinsicContentSize = contentView.intrinsicContentSize
|
||||
let widthIsDefined = contentViewSystemLayoutSize.width > 0 || contentViewIntrinsicContentSize.width != UIView.noIntrinsicMetric
|
||||
let heightIsDefined = contentViewSystemLayoutSize.height > 0 || contentViewIntrinsicContentSize.height != UIView.noIntrinsicMetric
|
||||
// Warnings are reported only if both the width and height are undefined. When a
|
||||
// layout is intended to scroll along only one axis, it is convenient to leave the
|
||||
// size of the other axis undefined.
|
||||
if !widthIsDefined && !heightIsDefined {
|
||||
NSLog("Warning: The content view's size is undefined. You must have an unbroken chain of constraints and views stretching across at least one axis of the content view or the content view's intrinsic content size must be defined.")
|
||||
}
|
||||
|
||||
if contentView.superview == nil {
|
||||
addScrollViewToHostViewControllerRootView()
|
||||
} else {
|
||||
@@ -287,7 +286,7 @@ public class ScrollingContentViewManager: KeyboardObservering, ScrollViewBounceC
|
||||
// scroll view, leaving the content view without width and height constraints. The
|
||||
// content view will be assigned replacement minimum width and height constraints
|
||||
// later, in `addScrollViewAndContentViewConstraints`.
|
||||
transferWidthAndHeightConstraints(of: contentView, to: scrollView)
|
||||
moveWidthAndHeightConstraints(of: contentView, to: scrollView)
|
||||
}
|
||||
|
||||
/// Adds the content view as a subview of the scroll view.
|
||||
@@ -308,6 +307,30 @@ public class ScrollingContentViewManager: KeyboardObservering, ScrollViewBounceC
|
||||
addScrollViewAndContentViewConstraints()
|
||||
}
|
||||
|
||||
/// Logs a warning if the content view's size is undefined.
|
||||
public func viewWillAppear(_ animated: Bool) {
|
||||
guard let contentView = contentView else {
|
||||
assertionFailure("The content view is undefined")
|
||||
return
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
let contentViewSystemLayoutSize = contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
|
||||
let contentViewIntrinsicContentSize = contentView.intrinsicContentSize
|
||||
let widthIsDefined = contentViewSystemLayoutSize.width > 0 || contentViewIntrinsicContentSize.width != UIView.noIntrinsicMetric
|
||||
let heightIsDefined = contentViewSystemLayoutSize.height > 0 || contentViewIntrinsicContentSize.height != UIView.noIntrinsicMetric
|
||||
// Warnings are reported only if both the width and height are undefined. When a
|
||||
// layout is intended to scroll along only one axis, it is convenient to leave the
|
||||
// size of the other axis undefined.
|
||||
// Note: If a root view has no constraints, systemLayoutSizeFitting will return the
|
||||
// default size of the view, usually matching the size of the screen, so the
|
||||
// warning will not be displayed displayed in that case.
|
||||
if !widthIsDefined && !heightIsDefined {
|
||||
NSLog("Warning: The content view's size is undefined. You must have an unbroken chain of constraints and views stretching across at least one axis of the content view or the content view's intrinsic content size must be defined.")
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Responds to changes in the view controller's safe area insets. If this method is
|
||||
/// not called, then, in the context of a navigation controller, if a sequence of
|
||||
/// view controllers with text fields that become the first responder in
|
||||
@@ -380,12 +403,12 @@ public class ScrollingContentViewManager: KeyboardObservering, ScrollViewBounceC
|
||||
}
|
||||
}
|
||||
|
||||
/// Transfers width and height constraints from one view to another view.
|
||||
/// Moves width and height constraints from one view to another view.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - fromView: The view to transfer width and height constraints from.
|
||||
/// - toView: The view to transfer width and height constraints to.
|
||||
private func transferWidthAndHeightConstraints(of fromView: UIView, to toView: UIView) {
|
||||
private func moveWidthAndHeightConstraints(of fromView: UIView, to toView: UIView) {
|
||||
var constraintsToRemove: [NSLayoutConstraint] = []
|
||||
for constraint in fromView.constraints {
|
||||
if constraint.firstAttribute == .width || constraint.firstAttribute == .height {
|
||||
@@ -442,8 +465,12 @@ public class ScrollingContentViewManager: KeyboardObservering, ScrollViewBounceC
|
||||
let contentViewMinimumHeightConstraint = contentView.heightAnchor.constraint(greaterThanOrEqualTo: scrollView.safeAreaLayoutGuide.heightAnchor, multiplier: 1)
|
||||
self.contentViewMinimumHeightConstraint = contentViewMinimumHeightConstraint
|
||||
|
||||
let contentViewMinimumHeightForPresentedKeyboardConstraint = contentView.heightAnchor.constraint(greaterThanOrEqualToConstant: 0)
|
||||
self.contentViewMinimumHeightForPresentedKeyboardConstraint = contentViewMinimumHeightForPresentedKeyboardConstraint
|
||||
|
||||
contentViewMinimumWidthConstraint.priority = minimumSizeConstraintPriority
|
||||
contentViewMinimumHeightConstraint.priority = minimumSizeConstraintPriority
|
||||
contentViewMinimumHeightForPresentedKeyboardConstraint.priority = minimumSizeConstraintPriority
|
||||
|
||||
contentView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
@@ -454,9 +481,14 @@ public class ScrollingContentViewManager: KeyboardObservering, ScrollViewBounceC
|
||||
scrollView.contentLayoutGuide.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
|
||||
contentViewMinimumWidthConstraint,
|
||||
contentViewMinimumHeightConstraint,
|
||||
contentViewMinimumHeightForPresentedKeyboardConstraint
|
||||
]
|
||||
|
||||
scrollView.addConstraints(constraints)
|
||||
NSLayoutConstraint.activate(constraints)
|
||||
|
||||
// This constraint is activated only when the keyboard is presented
|
||||
// and shouldResizeContentViewForKeyboard is false.
|
||||
contentViewMinimumHeightForPresentedKeyboardConstraint.isActive = false
|
||||
}
|
||||
|
||||
/// Constrains a scroll view content offset so that it lies within the legal range
|
||||
@@ -496,20 +528,21 @@ public class ScrollingContentViewManager: KeyboardObservering, ScrollViewBounceC
|
||||
return scrollView.bounds.inset(by: scrollView.adjustedContentInset).size
|
||||
}
|
||||
|
||||
/// Adjusts the view to compensate for the portion of the keyboard that overlaps the
|
||||
/// scroll view.
|
||||
/// Adjusts the view controller to compensate for the portion of the keyboard that
|
||||
/// overlaps the view controller's root view.
|
||||
///
|
||||
/// This method is called by `KeyboardObserver` when the keyboard is presented,
|
||||
/// dismissed, or changes size.
|
||||
///
|
||||
/// - Parameter bottomInset: The height of the area of keyboard's frame that
|
||||
/// overlaps the view.
|
||||
/// overlaps the view controller's root view.
|
||||
func adjustViewForKeyboard(withBottomInset bottomInset: CGFloat) {
|
||||
self.bottomInset = bottomInset
|
||||
}
|
||||
|
||||
/// The bottom inset to assign to the view controller's additional safe area
|
||||
/// to compensate for the area of the keyboard that overlaps the scroll view.
|
||||
/// The bottom inset to assign to the view controller's additional safe area to
|
||||
/// compensate for the area of the keyboard that overlaps the view controller's root
|
||||
/// view.
|
||||
private var bottomInset: CGFloat = 0 {
|
||||
didSet {
|
||||
if bottomInset == oldValue {
|
||||
@@ -520,11 +553,11 @@ public class ScrollingContentViewManager: KeyboardObservering, ScrollViewBounceC
|
||||
|
||||
if shouldAdjustAdditionalSafeAreaInsetsForKeyboard {
|
||||
// When the keyboard is presented, the view controller's
|
||||
// additionalSafeAreaInsets.bottom property us adjusted to compensate.
|
||||
// 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
|
||||
// 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
|
||||
// screen.
|
||||
//
|
||||
@@ -535,4 +568,23 @@ public class ScrollingContentViewManager: KeyboardObservering, ScrollViewBounceC
|
||||
}
|
||||
}
|
||||
|
||||
func additionalSafeAreaInsetsControllerWillUpdateAdditionalSafeAreaInsetsForPresentedKeyboard(_ additionalSafeAreaInsetsController: AdditionalSafeAreaInsetsController) {
|
||||
guard !shouldResizeContentViewForKeyboard else {
|
||||
// Don't constrain the height of the keyboard.
|
||||
return
|
||||
}
|
||||
guard let contentViewMinimumHeightForPresentedKeyboardConstraint = contentViewMinimumHeightForPresentedKeyboardConstraint, let contentView = contentView else {
|
||||
return
|
||||
}
|
||||
|
||||
// When the keyboard is presented, just before AdditionalSafeAreaInsetsController
|
||||
|
||||
contentViewMinimumHeightForPresentedKeyboardConstraint.constant = contentView.frame.height
|
||||
contentViewMinimumHeightForPresentedKeyboardConstraint.isActive = true
|
||||
}
|
||||
|
||||
func additionalSafeAreaInsetsControllerDidUpdateAdditionalSafeAreaInsetsForDismissedKeyboard(_ additionalSafeAreaInsetsController: AdditionalSafeAreaInsetsController) {
|
||||
contentViewMinimumHeightForPresentedKeyboardConstraint?.isActive = false
|
||||
}
|
||||
|
||||
}
|
||||
+1
-1
@@ -15,7 +15,7 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
private var foundFirstResponder: UIResponder? = nil
|
||||
private var foundFirstResponder: UIResponder?
|
||||
|
||||
internal extension UIResponder {
|
||||
|
||||
@@ -43,7 +43,7 @@ class ContentView: UIView {
|
||||
widthConstraint.priority = .defaultLow
|
||||
heightConstraint.priority = .defaultLow
|
||||
|
||||
addConstraints([widthConstraint, heightConstraint])
|
||||
NSLayoutConstraint.activate([widthConstraint, heightConstraint])
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
//
|
||||
// InsetContentViewKeyboardTests.swift
|
||||
// ScrollingContentViewController
|
||||
//
|
||||
// Created by Drew Olbrich on 3/22/19.
|
||||
// Copyright 2019 Oath Inc.
|
||||
//
|
||||
// Licensed under the terms of the MIT License. See the file LICENSE for the full terms.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import ScrollingContentViewController
|
||||
|
||||
/// Test case of presenting the keyboard over a content view that is inset
|
||||
/// within the host view controller's root view.
|
||||
class InsetContentViewKeyboardTests: XCTestCase {
|
||||
|
||||
var window: UIWindow!
|
||||
|
||||
var scrollingContentViewManager: ScrollingContentViewManager!
|
||||
var hostViewController: UIViewController!
|
||||
|
||||
var contentView: UIView!
|
||||
var scrollView: UIScrollView!
|
||||
var rootView: UIView!
|
||||
|
||||
let navigationBarHeight: CGFloat = 64
|
||||
let tabBarHeight: CGFloat = 49
|
||||
let keyboardHeight: CGFloat = 258
|
||||
|
||||
let contentViewInset = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20)
|
||||
|
||||
override func setUp() {
|
||||
window = UIWindow(frame: UIScreen.main.bounds)
|
||||
window.isHidden = false
|
||||
|
||||
hostViewController = UIViewController()
|
||||
|
||||
hostViewController.additionalSafeAreaInsets.top = navigationBarHeight
|
||||
hostViewController.additionalSafeAreaInsets.bottom = tabBarHeight
|
||||
|
||||
scrollingContentViewManager = ScrollingContentViewManager(hostViewController: hostViewController)
|
||||
|
||||
contentView = UIView()
|
||||
|
||||
hostViewController.view.addSubview(contentView)
|
||||
|
||||
// Inset the content view within the initial safe area defined by the navigation
|
||||
// bar and tab bar, but not so much that it won't be overlapped by the keyboard
|
||||
// when it is presented.
|
||||
contentView.frame = hostViewController.view.bounds.inset(by: contentViewInset)
|
||||
contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
|
||||
scrollingContentViewManager.contentView = contentView
|
||||
|
||||
hostViewController.beginAppearanceTransition(true, animated: false)
|
||||
window.rootViewController = hostViewController
|
||||
hostViewController.view.layoutIfNeeded()
|
||||
hostViewController.endAppearanceTransition()
|
||||
|
||||
scrollView = scrollingContentViewManager.scrollView
|
||||
rootView = hostViewController.view
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
hostViewController.beginAppearanceTransition(false, animated: false)
|
||||
window.rootViewController = nil
|
||||
hostViewController.endAppearanceTransition()
|
||||
window.isHidden = true
|
||||
window = nil
|
||||
|
||||
hostViewController = nil
|
||||
|
||||
scrollView = nil
|
||||
rootView = nil
|
||||
contentView = nil
|
||||
}
|
||||
|
||||
/// Tests that the view hierarchy has the expected topology.
|
||||
func testViewHierarchy() {
|
||||
// The content view's superview should be the scroll view.
|
||||
XCTAssertEqual(contentView.superview, scrollView)
|
||||
|
||||
// The scroll view's superview should be the view controller's root view.
|
||||
XCTAssertEqual(scrollView.superview, rootView)
|
||||
}
|
||||
|
||||
/// Tests that the content view and the scroll view have the expected size.
|
||||
func testDefaultLayout() {
|
||||
let scrollViewSafeAreaFrame = scrollView.safeAreaLayoutGuide.layoutFrame
|
||||
|
||||
// The content view's frame should match the size of the root view's safe area that
|
||||
// intersects the scroll view frame.
|
||||
XCTAssertEqual(contentView.frame.size, scrollViewSafeAreaFrame.size)
|
||||
|
||||
// The scroll view's content size should match that of the root view's safe area
|
||||
// that intersects the scroll view frame.
|
||||
XCTAssertEqual(scrollView.contentSize, scrollViewSafeAreaFrame.size)
|
||||
}
|
||||
|
||||
/// Tests that presenting the keyboard does not affect the size of the content
|
||||
/// view when `shouldResizeContentViewForKeyboard` is `false`.
|
||||
func testPresentedKeyboardWithFixedContentView() {
|
||||
scrollingContentViewManager.shouldResizeContentViewForKeyboard = false
|
||||
|
||||
let initialContentViewSize = scrollView.safeAreaLayoutGuide.layoutFrame.size
|
||||
|
||||
presentKeyboard()
|
||||
|
||||
let expectedContentViewSize = initialContentViewSize
|
||||
|
||||
XCTAssertEqual(contentView.frame.size, expectedContentViewSize)
|
||||
}
|
||||
|
||||
/// Tests that presenting the keyboard affects the size of the content view
|
||||
/// when `shouldResizeContentViewForKeyboard` is `true`.
|
||||
func testPresentedKeyboardWithResizedContentView() {
|
||||
scrollingContentViewManager.shouldResizeContentViewForKeyboard = true
|
||||
|
||||
let initialContentViewSize = scrollView.safeAreaLayoutGuide.layoutFrame.size
|
||||
let initialBottomSafeAreaInset = rootView.safeAreaInsets.bottom - tabBarHeight
|
||||
|
||||
presentKeyboard()
|
||||
|
||||
let expectedContentViewSize = CGSize(width: initialContentViewSize.width, height: initialContentViewSize.height - (keyboardHeight - tabBarHeight) + initialBottomSafeAreaInset)
|
||||
|
||||
XCTAssertEqual(contentView.frame.size, expectedContentViewSize)
|
||||
}
|
||||
|
||||
private func presentKeyboard() {
|
||||
let keyboardFrame = CGRect(x: 0, y: window.bounds.height - keyboardHeight, width: window.bounds.width, height: keyboardHeight)
|
||||
|
||||
// A test keyboard frame must be injected here because keyboard notifications will
|
||||
// not be generated when a first responder is assigned within a test.
|
||||
let keyboardFrameEvent = KeyboardFrameEvent(keyboardFrame: keyboardFrame, duration: 0.35)
|
||||
scrollingContentViewManager.keyboardObserver?.testKeyboardFrameEvent(keyboardFrameEvent)
|
||||
}
|
||||
|
||||
}
|
||||
+41
-15
@@ -19,24 +19,29 @@ class KeyboardTests: XCTestCase {
|
||||
var scrollingContentViewManager: ScrollingContentViewManager!
|
||||
var hostViewController: UIViewController!
|
||||
|
||||
var contentView: ContentView!
|
||||
var contentView: UIView!
|
||||
var scrollView: UIScrollView!
|
||||
var rootView: UIView!
|
||||
|
||||
let navigationBarHeight: CGFloat = 64
|
||||
let tabBarHeight: CGFloat = 49
|
||||
let keyboardHeight: CGFloat = 258
|
||||
|
||||
override func setUp() {
|
||||
window = UIWindow(frame: UIScreen.main.bounds)
|
||||
window.isHidden = false
|
||||
|
||||
hostViewController = UIViewController()
|
||||
|
||||
hostViewController.additionalSafeAreaInsets.top = navigationBarHeight
|
||||
hostViewController.additionalSafeAreaInsets.bottom = tabBarHeight
|
||||
|
||||
scrollingContentViewManager = ScrollingContentViewManager(hostViewController: hostViewController)
|
||||
|
||||
contentView = ContentView()
|
||||
contentView = UIView()
|
||||
|
||||
scrollingContentViewManager.contentView = contentView
|
||||
|
||||
scrollingContentViewManager.shouldResizeContentViewForKeyboard = true
|
||||
|
||||
hostViewController.beginAppearanceTransition(true, animated: false)
|
||||
window.rootViewController = hostViewController
|
||||
hostViewController.view.layoutIfNeeded()
|
||||
@@ -80,23 +85,44 @@ class KeyboardTests: XCTestCase {
|
||||
XCTAssertEqual(scrollView.contentSize, rootViewSafeAreaSize)
|
||||
}
|
||||
|
||||
/// Tests that presenting the keyboard affects the size of the content view.
|
||||
func testPresentedKeyboard() {
|
||||
let keyboardHeight: CGFloat = 258
|
||||
/// Tests that presenting the keyboard does not affect the size of the content
|
||||
/// view when `shouldResizeContentViewForKeyboard` is `false`.
|
||||
func testPresentedKeyboardWithFixedContentView() {
|
||||
scrollingContentViewManager.shouldResizeContentViewForKeyboard = false
|
||||
|
||||
let initialContentViewSize = rootView.bounds.inset(by: rootView.safeAreaInsets).size
|
||||
|
||||
presentKeyboard()
|
||||
|
||||
let expectedContentViewSize = CGSize(width: initialContentViewSize.width, height: initialContentViewSize.height)
|
||||
|
||||
XCTAssertEqual(contentView.frame.size, expectedContentViewSize)
|
||||
}
|
||||
|
||||
/// Tests that presenting the keyboard affects the size of the content view
|
||||
/// when `shouldResizeContentViewForKeyboard` is `true`.
|
||||
func testPresentedKeyboardWithResizedContentView() {
|
||||
scrollingContentViewManager.shouldResizeContentViewForKeyboard = true
|
||||
|
||||
let initialContentViewSize = rootView.bounds.inset(by: rootView.safeAreaInsets).size
|
||||
let initialBottomSafeAreaInset = rootView.safeAreaInsets.bottom - tabBarHeight
|
||||
|
||||
presentKeyboard()
|
||||
|
||||
// The size of the expected safe area of the view controller's root view after the
|
||||
// keyboard is presented.
|
||||
let expectedContentViewSize = CGSize(width: initialContentViewSize.width, height: initialContentViewSize.height - (keyboardHeight - tabBarHeight) + initialBottomSafeAreaInset)
|
||||
|
||||
XCTAssertEqual(contentView.frame.size, expectedContentViewSize)
|
||||
}
|
||||
|
||||
private func presentKeyboard() {
|
||||
let keyboardFrame = CGRect(x: 0, y: window.bounds.height - keyboardHeight, width: window.bounds.width, height: keyboardHeight)
|
||||
|
||||
let initialSafeAreaSize = rootView.bounds.inset(by: rootView.safeAreaInsets).size
|
||||
|
||||
let initialBottomInset = scrollView.adjustedContentInset.bottom
|
||||
|
||||
// A test keyboard frame must be injected here because keyboard notifications will
|
||||
// not be generated when a first responder is assigned within a test.
|
||||
let keyboardFrameEvent = KeyboardFrameEvent(keyboardFrame: keyboardFrame, duration: 0.35)
|
||||
scrollingContentViewManager.keyboardObserver?.testKeyboardFrameEvent(keyboardFrameEvent)
|
||||
|
||||
let finalSafeAreaSize = CGSize(width: initialSafeAreaSize.width, height: initialSafeAreaSize.height - (keyboardHeight - initialBottomInset))
|
||||
|
||||
XCTAssertEqual(contentView.frame.size, finalSafeAreaSize)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user