Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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))
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 88 KiB |
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
//
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
private var foundFirstResponder: UIResponder? = nil
|
||||
private var foundFirstResponder: UIResponder?
|
||||
|
||||
internal extension UIResponder {
|
||||
|
||||
|
||||
Reference in New Issue
Block a user