38 Commits

Author SHA1 Message Date
Drew Olbrich 6b5119bb08 Set version to 1.1.0 2019-02-09 17:03:41 -08:00
Drew Olbrich 3c739780bb Set textContentType correctly for text fields 2019-02-09 16:50:07 -08:00
Drew Olbrich 11f0492d37 Change password field return key type to done 2019-02-09 16:47:17 -08:00
Drew Olbrich 53836f94c7 Warn about undefined content view size in viewWillAppear 2019-02-09 16:29:40 -08:00
Drew Olbrich 6fc4f08afa Rename to moveWidthAndHeightConstraints 2019-02-09 16:09:36 -08:00
Drew Olbrich 81bd105e4d Update code usage 2019-02-09 15:10:55 -08:00
Drew Olbrich 89a84daf3c Set version to 1.0.1 2019-02-09 15:05:22 -08:00
Drew Olbrich 80fcb1ae32 To avoid a warning, assign self.contentView after constraints are added 2019-02-09 15:04:42 -08:00
Drew Olbrich 8fb074ff46 Fix sign up button font size and weight 2019-02-09 14:28:58 -08:00
Drew Olbrich 6f67d2c205 Handle the case where scrollRectToVisible is used on a subview that later changes size 2019-02-09 14:05:06 -08:00
Drew Olbrich 361b31c298 Revert "Explicitly specify image tag heights"
This reverts commit 57744804bb.
2019-02-09 11:43:59 -08:00
Drew Olbrich 982da685e3 Revert "Fix image sizes"
This reverts commit a19e0ffab2.
2019-02-09 11:43:51 -08:00
Drew Olbrich eb28ffde42 Revert "Wrap overview comparison image in div tag"
This reverts commit 65b8467384.
2019-02-09 11:43:46 -08:00
Drew Olbrich 61161b7d64 Revert "Wrap all images in div tags so aspect ratios are correct on mobile"
This reverts commit 1f7dde75e2.
2019-02-09 11:43:37 -08:00
Drew Olbrich 1f7dde75e2 Wrap all images in div tags so aspect ratios are correct on mobile 2019-02-09 11:40:22 -08:00
Drew Olbrich 65b8467384 Wrap overview comparison image in div tag 2019-02-09 11:33:31 -08:00
Drew Olbrich a19e0ffab2 Fix image sizes 2019-02-09 11:10:47 -08:00
Drew Olbrich 57744804bb Explicitly specify image tag heights 2019-02-09 11:01:54 -08:00
Drew Olbrich adb9a189b5 Highlight height text field 2019-02-09 10:56:00 -08:00
Drew Olbrich c6a10a275f Don't let text fields stretch to fit long strings 2019-02-09 10:29:39 -08:00
Drew Olbrich f8b4cc19c6 Don't copy license as bundle resource 2019-02-09 10:20:06 -08:00
Drew Olbrich 970041fb4f Update oversized view controller image 2019-02-09 09:18:22 -08:00
Drew Olbrich 6b225bdb05 Add more text fields to oversized view controller image 2019-02-09 09:08:51 -08:00
Drew Olbrich 08e547dcb0 Dismiss the keyboard when the Sign Up button is tapped 2019-02-09 07:46:09 -08:00
Drew Olbrich 758e78dc13 Change Password return key to Done 2019-02-06 20:21:12 -08:00
Drew Olbrich d9365d80aa Rename SwiftLint build phase 2019-02-06 07:39:51 -08:00
Drew Olbrich d94c2e622a Add SwiftLint and Travis configuration files to project 2019-02-06 07:30:07 -08:00
Drew Olbrich 917865fe5b Integrate SwiftLint 2019-02-06 07:18:08 -08:00
Drew Olbrich cb3b318869 Rename section to View Controller Properties 2019-02-06 06:51:58 -08:00
Drew Olbrich 6f819a1eea Update comments 2019-02-05 19:29:27 -08:00
Drew Olbrich fc42d7020c When a substitution root view is created, match the original root view's frame 2019-02-05 19:01:19 -08:00
Drew Olbrich 7a7a3c6be1 Add comment about content view size warning with no constraints 2019-02-05 18:00:45 -08:00
Drew Olbrich 88043ca615 Revise Carthage installation instructions 2019-02-05 08:55:46 -08:00
Drew Olbrich fde366e946 Add Carthage installation instructions 2019-02-05 08:54:29 -08:00
Drew Olbrich 9244d52a4a Log content view size warning only for debug builds 2019-02-05 08:32:26 -08:00
Drew Olbrich 68233ec332 Update usage 2019-02-04 08:20:39 -08:00
Drew Olbrich 343aa9b8c4 Change iPhone XS to iPhone Xs 2019-02-04 07:50:35 -08:00
Drew Olbrich cfcfa3fd18 Update summary 2019-02-03 14:34:04 -08:00
20 changed files with 173 additions and 64 deletions
+8
View File
@@ -0,0 +1,8 @@
disabled_rules:
- line_length
- unused_closure_parameter
- identifier_name
file_length:
warning: 850
error: 1200
@@ -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,7 +157,7 @@ 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)
@@ -20,4 +20,3 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
}
}
@@ -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()
@@ -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"/>
+1 -1
View File
@@ -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>
+1 -1
View File
@@ -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
]
}
+1 -1
View File
@@ -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)
+10 -2
View File
@@ -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))
Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 88 KiB

+35 -12
View File
@@ -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)
@@ -27,7 +27,7 @@ ScrollingContentViewController makes it easy to create a view controller with a
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.
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">
@@ -47,6 +47,12 @@ 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).
@@ -85,7 +91,7 @@ To configure `ScrollingContentViewController` in a storyboard:
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.
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
@@ -112,6 +118,7 @@ To integrate `ScrollingContentViewController` programmatically:
contentView = UIView()
// Add all controls to contentView instead of view.
// ...
}
```
@@ -174,7 +181,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 +216,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 +232,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()
@@ -254,6 +269,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 +285,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()
@@ -286,7 +309,7 @@ class MyViewController: UIViewController {
* [ReassignExample](Examples/ReassignExample) - Example of dynamically reassigning `contentView`.
## Properties
## View Controller Properties
The `ScrollingContentViewController` and `ScrollingContentViewManager` classes share the following properties:
@@ -376,7 +399,7 @@ 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. 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.
### Keyboard Resize Filtering
+2 -2
View File
@@ -1,7 +1,7 @@
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.1.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
@@ -34,7 +34,6 @@
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 */; };
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 */; };
@@ -244,6 +243,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>"; };
@@ -336,6 +337,8 @@
3AAC048921E2D3FD00D94DA5 /* LICENSE */,
3AAC048821E2D3FD00D94DA5 /* README.md */,
3AAC048A21E2D3FD00D94DA5 /* ScrollingContentViewController.podspec */,
3ACE0D7B220B34BE0093FE5A /* .swiftlint.yml */,
3ACE0D7C220B34BE0093FE5A /* .travis.yml */,
3A5702D021E2CBB600E4CC55 /* Source */,
3AAC04A521E39FBF00D94DA5 /* Examples */,
3A5702DB21E2CBB600E4CC55 /* Tests */,
@@ -509,6 +512,7 @@
3A5702CA21E2CBB600E4CC55 /* Sources */,
3A5702CB21E2CBB600E4CC55 /* Frameworks */,
3A5702CC21E2CBB600E4CC55 /* Resources */,
3ACE0D7A220B2D790093FE5A /* Run SwiftLint */,
);
buildRules = (
);
@@ -695,7 +699,6 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
3AAC048C21E2D3FD00D94DA5 /* LICENSE in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -758,6 +761,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;
+1 -1
View File
@@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0.0</string>
<string>1.1.0</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
</dict>
+2 -1
View File
@@ -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
View File
@@ -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
+16 -4
View File
@@ -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
}
@@ -81,7 +81,15 @@ public class ScrollingContentScrollView: UIScrollView {
/// 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 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 descedant 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:)...
@@ -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)
@@ -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
+42 -22
View File
@@ -98,7 +98,7 @@ public class ScrollingContentViewManager: KeyboardObservering, ScrollViewBounceC
///
/// 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 +191,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 +238,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 +283,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 +304,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 +400,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 {
@@ -453,7 +473,7 @@ public class ScrollingContentViewManager: KeyboardObservering, ScrollViewBounceC
scrollView.contentLayoutGuide.topAnchor.constraint(equalTo: contentView.topAnchor),
scrollView.contentLayoutGuide.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
contentViewMinimumWidthConstraint,
contentViewMinimumHeightConstraint,
contentViewMinimumHeightConstraint
]
scrollView.addConstraints(constraints)
@@ -524,7 +544,7 @@ public class ScrollingContentViewManager: KeyboardObservering, ScrollViewBounceC
//
// This approach is 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.
//
+1 -1
View File
@@ -15,7 +15,7 @@
import UIKit
private var foundFirstResponder: UIResponder? = nil
private var foundFirstResponder: UIResponder?
internal extension UIResponder {