├── Example
├── Sources
│ ├── Assets.xcassets
│ │ ├── Contents.json
│ │ └── AppIcon.appiconset
│ │ │ └── Contents.json
│ ├── AppDelegate.swift
│ ├── Base.lproj
│ │ └── LaunchScreen.storyboard
│ ├── SingleFieldAutoNavViewController.swift
│ ├── AutoNavigatorViewController.swift
│ ├── ViewController.swift
│ └── Main.storyboard
├── Example.xcodeproj
│ ├── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ └── IDEWorkspaceChecks.plist
│ ├── xcshareddata
│ │ └── xcschemes
│ │ │ └── iOS Example.xcscheme
│ └── project.pbxproj
└── Info.plist
├── .codebeatsettings
├── .swiftpm
└── xcode
│ └── package.xcworkspace
│ └── contents.xcworkspacedata
├── CODEOWNERS
├── KeyboardSupport.xcworkspace
├── xcshareddata
│ └── IDEWorkspaceChecks.plist
└── contents.xcworkspacedata
├── .swiftlint.yml
├── Sources
├── KeyboardRespondable.swift
├── KeyboardSupport.h
├── Extensions
│ ├── UIEdgeInsets+KeyboardSupport.swift
│ ├── UIScrollView+Inset.swift
│ ├── Array+KeyboardSupport.swift
│ ├── CGRect+Modifying.swift
│ └── UIView+KeyboardSupport.swift
├── KeyboardDismissable.swift
├── KeyboardAccessory.swift
├── KeyboardSafeAreaAdjustable.swift
├── KeyboardNavigator.swift
├── KeyboardToolbar.swift
├── KeyboardScrollable.swift
└── KeyboardAutoNavigator.swift
├── NOTICE
├── Dangerfile.swift
├── Package.swift
├── CONTRIBUTING.md
├── .github
├── ISSUE_TEMPLATE
│ └── release-template.md
└── workflows
│ └── main.yml
├── Tests
├── Helper
│ └── Mocks
│ │ ├── MockKeyboardNavigatorDelegate.swift
│ │ ├── MockKeyboardAccessoryDelegate.swift
│ │ └── MockKeyboardAutoNavigatorDelegate.swift
├── KeyboardAutoNavigatorTests.swift
├── KeyboardToolbarTests.swift
└── KeyboardNavigatorTests.swift
├── .gitignore
├── KeyboardSupport.podspec
├── Documentation
└── Migrations
│ └── KeyboardSupport 1.x-2.0 Migration Guide.md
├── KeyboardSupport.xcodeproj
├── xcshareddata
│ └── xcschemes
│ │ └── KeyboardSupport iOS.xcscheme
└── project.pbxproj
├── CHANGELOG.md
├── README.md
└── LICENSE
/Example/Sources/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/.codebeatsettings:
--------------------------------------------------------------------------------
1 | {
2 | "SWIFT": {
3 | "LOC": [30, 40, 60, 80],
4 | "ABC": [20, 30, 40, 60],
5 | "ARITY": [5, 6, 7, 8],
6 | "TOO_MANY_FUNCTIONS": [17, 20, 30, 50]
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Example/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/CODEOWNERS:
--------------------------------------------------------------------------------
1 | # These owners will be the default owners for everything in the repo.
2 | # Unless a later match takes precedence, these owners will be requested for review when someone opens a pull request.
3 | * @BottleRocketStudios/team-ios-open-source-w
4 |
--------------------------------------------------------------------------------
/KeyboardSupport.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/KeyboardSupport.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/Example/Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.swiftlint.yml:
--------------------------------------------------------------------------------
1 | # Set max line length before warning (default is 120)
2 | line_length: 240
3 |
4 | disabled_rules:
5 | - trailing_whitespace # Disables SwiftLint complaining about whitespace characters on empty lines
6 | - todo # Disables auto-warning of TODO statements
7 |
8 | identifier_name:
9 | excluded:
10 | - id
11 | - ok
12 |
--------------------------------------------------------------------------------
/Sources/KeyboardRespondable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeyboardRespondable.swift
3 | // KeyboardSupport
4 | //
5 | // Copyright © 2017 Bottle Rocket Studios. All rights reserved.
6 | //
7 |
8 | import UIKit
9 |
10 | /// Inherits from both KeyboardDismissable and KeyboardScrollable for convenience.
11 | public protocol KeyboardRespondable: KeyboardDismissable, KeyboardScrollable {}
12 |
--------------------------------------------------------------------------------
/NOTICE:
--------------------------------------------------------------------------------
1 | =============================================================================
2 | = NOTICE file corresponding to section 4d of the Apache License Version 2.0 =
3 | = This notice is optional, but appreciated. =
4 | =============================================================================
5 | This product includes software developed by
6 | Bottle Rocket LLC (http://www.bottlerocketstudios.com/).
--------------------------------------------------------------------------------
/Dangerfile.swift:
--------------------------------------------------------------------------------
1 | import Danger
2 |
3 | let danger = Danger()
4 |
5 | //
6 | // Ensure CHANGELOG.md was modified for edits to source files.
7 | //
8 | let allSourceFiles = danger.git.modifiedFiles + danger.git.createdFiles
9 |
10 | let changelogChanged = allSourceFiles.contains("CHANGELOG.md")
11 | let sourceChanges = allSourceFiles.first(where: { $0.hasPrefix("Sources") })
12 |
13 | if !changelogChanged && sourceChanges != nil {
14 | warn("No CHANGELOG entry added.")
15 | }
16 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.0
2 | import PackageDescription
3 |
4 | let package = Package(
5 | name: "KeyboardSupport",
6 | platforms: [
7 | .iOS(.v12)
8 | ],
9 | products: [
10 | .library(name: "KeyboardSupport", targets: ["KeyboardSupport"])
11 | ],
12 | targets: [
13 | .target(name: "KeyboardSupport", path: "Sources"),
14 | .testTarget(name: "KeyboardSupportTests", dependencies: ["KeyboardSupport"], path: "Tests")
15 | ]
16 | )
17 |
--------------------------------------------------------------------------------
/Example/Sources/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // Example
4 | //
5 | // Copyright © 2018 Bottle Rocket Studios. All rights reserved.
6 | //
7 |
8 | import UIKit
9 |
10 | @UIApplicationMain
11 | class AppDelegate: UIResponder, UIApplicationDelegate {
12 |
13 | var window: UIWindow?
14 |
15 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
16 | return true
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/Sources/KeyboardSupport.h:
--------------------------------------------------------------------------------
1 | //
2 | // KeyboardSupport.h
3 | // KeyboardSupport
4 | //
5 | //
6 |
7 | #import
8 |
9 | //! Project version number for KeyboardSupport.
10 | FOUNDATION_EXPORT double KeyboardSupportVersionNumber;
11 |
12 | //! Project version string for KeyboardSupport.
13 | FOUNDATION_EXPORT const unsigned char KeyboardSupportVersionString[];
14 |
15 | // In this header, you should import all the public headers of your framework using statements like #import
16 |
17 |
18 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | Contributing
2 | ============
3 |
4 | If you would like to contribute code to this project you can do so through GitHub by
5 | forking the repository and sending a pull request.
6 |
7 | When submitting code follow the existing conventions and code style. Ensure that your code changes build and unit tests pass.
8 |
9 | Before your code can be accepted into the project you must also sign the
10 | [Individual Contributor License Agreement (CLA)][1].
11 |
12 |
13 | [1]: https://cla-assistant.io/BottleRocketStudios/iOS-KeyboardSupport
14 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/release-template.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Release template
3 | about: Basic release checklist.
4 | title: Version [version_number]
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | Release checklist:
11 | - [ ] Create release branch
12 | - [ ] Update version number for all targets
13 | - [ ] Update version number in `Podspec`
14 | - [ ] Validate `README.md` is still current
15 | - [ ] Update `CHANGELOG.md` for the new release
16 | - [ ] Create pull request into `master`
17 | - [ ] Create version number tag in Git
18 | - [ ] Publish release on GitHub
19 | - [ ] Publish release on Cocoapods trunk
20 |
--------------------------------------------------------------------------------
/Tests/Helper/Mocks/MockKeyboardNavigatorDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MockKeyboardNavigatorDelegate.swift
3 | // Tests
4 | //
5 | // Copyright © 2019 Bottle Rocket. All rights reserved.
6 | //
7 |
8 | import Foundation
9 | import KeyboardSupport
10 |
11 | class MockKeyboardNavigatorDelegate: KeyboardNavigatorDelegate {
12 |
13 | var tapType: TapType?
14 |
15 | func keyboardNavigatorDidTapBack(_ navigator: KeyboardNavigator) {
16 | tapType = .back
17 | }
18 |
19 | func keyboardNavigatorDidTapNext(_ navigator: KeyboardNavigator) {
20 | tapType = .next
21 | }
22 |
23 | func keyboardNavigatorDidTapDone(_ navigator: KeyboardNavigator) {
24 | tapType = .done
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # OS X
2 | .DS_Store
3 |
4 | # Xcode
5 | build/
6 | *.pbxuser
7 | !default.pbxuser
8 | *.mode1v3
9 | !default.mode1v3
10 | *.mode2v3
11 | !default.mode2v3
12 | *.perspectivev3
13 | !default.perspectivev3
14 | xcuserdata/
15 | *.xccheckout
16 | profile
17 | *.moved-aside
18 | DerivedData
19 | *.hmap
20 | *.ipa
21 |
22 | # Bundler
23 | .bundle
24 |
25 | Carthage
26 | # We recommend against adding the Pods directory to your .gitignore. However
27 | # you should judge for yourself, the pros and cons are mentioned at:
28 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control
29 | #
30 | # Note: if you ignore the Pods directory, make sure to uncomment
31 | # `pod install` in .travis.yml
32 | #
33 | # Pods/
34 |
--------------------------------------------------------------------------------
/Tests/Helper/Mocks/MockKeyboardAccessoryDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MockKeyboardAccessoryDelegate.swift
3 | // Tests
4 | //
5 | // Copyright © 2019 Bottle Rocket. All rights reserved.
6 | //
7 |
8 | import UIKit
9 | import KeyboardSupport
10 |
11 | enum TapType {
12 | case back
13 | case next
14 | case done
15 | }
16 |
17 | class MockKeyboardAccessoryDelegate: KeyboardAccessoryDelegate {
18 | var tapType: TapType?
19 |
20 | func keyboardAccessoryDidTapBack(_ accessory: UIView) {
21 | tapType = .back
22 | }
23 |
24 | func keyboardAccessoryDidTapNext(_ accessory: UIView) {
25 | tapType = .next
26 | }
27 |
28 | func keyboardAccessoryDidTapDone(_ accessory: UIView) {
29 | tapType = .done
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Sources/Extensions/UIEdgeInsets+KeyboardSupport.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIEdgeInsets+KeyboardSupport.swift
3 | // KeyboardSupport
4 | //
5 | // Copyright © 2019 Bottle Rocket. All rights reserved.
6 | //
7 |
8 | import UIKit
9 |
10 | extension UIEdgeInsets {
11 | /// Constructs and returns a UIEdgeInsets instance using the max edge values from each argument.
12 | ///
13 | /// - Parameters:
14 | /// - lhs: UIEdgeInsets to compare
15 | /// - rhs: Other UIEdgeInsets to compare
16 | /// - Returns: Combined UIEdgeInsets using the max values for each edge
17 | static func max(lhs: UIEdgeInsets, rhs: UIEdgeInsets) -> UIEdgeInsets {
18 | return UIEdgeInsets(top: Swift.max(lhs.top, rhs.top),
19 | left: Swift.max(lhs.left, rhs.left),
20 | bottom: Swift.max(lhs.bottom, rhs.bottom),
21 | right: Swift.max(lhs.right, rhs.right))
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Sources/Extensions/UIScrollView+Inset.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIScrollView+Inset.swift
3 | // KeyboardSupport
4 | //
5 | // Copyright © 2018 Bottle Rocket. All rights reserved.
6 | //
7 |
8 | import UIKit
9 |
10 | extension UIScrollView {
11 |
12 | private class Box: NSObject {
13 | let value: UIEdgeInsets
14 |
15 | init(value: UIEdgeInsets) {
16 | self.value = value
17 | }
18 | }
19 |
20 | private static var insetKey = "originalContentInset"
21 | var originalContentInset: UIEdgeInsets? {
22 | get {
23 | guard let wrapper = objc_getAssociatedObject(self, &UIScrollView.insetKey) as? Box else { return nil }
24 | return wrapper.value
25 | }
26 | set {
27 | guard let newValue = newValue else { return }
28 | objc_setAssociatedObject(self, &UIScrollView.insetKey, Box(value: newValue), .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Sources/KeyboardDismissable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeyboardDismissable.swift
3 | // KeyboardSupport
4 | //
5 | // Copyright © 2019 Bottle Rocket. All rights reserved.
6 | //
7 |
8 | import UIKit
9 |
10 | /// Enables automatic keyboard dismissal via tapping the screen when the keyboard is displayed.
11 | public protocol KeyboardDismissable: AnyObject {
12 | /// Must be called once during setup ('viewDidLoad') to enable dismissal. Returns gesture recognizer used for keyboard dismissal.
13 | @discardableResult
14 | func setupKeyboardDismissalView() -> UIGestureRecognizer
15 | }
16 |
17 | public extension KeyboardDismissable where Self: UIViewController {
18 | @discardableResult
19 | func setupKeyboardDismissalView() -> UIGestureRecognizer {
20 | let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(keyboardDismissalViewTapped))
21 | tapGestureRecognizer.cancelsTouchesInView = false
22 | view.addGestureRecognizer(tapGestureRecognizer)
23 | return tapGestureRecognizer
24 | }
25 | }
26 |
27 | extension UIViewController {
28 | @objc func keyboardDismissalViewTapped(_ sender: UITapGestureRecognizer) {
29 | view.endEditing(true)
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Sources/Extensions/Array+KeyboardSupport.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Array+KeyboardSupport.swift
3 | // KeyboardSupport
4 | //
5 | // Copyright © 2018 Bottle Rocket. All rights reserved.
6 | //
7 |
8 | import UIKit
9 |
10 | // MARK: - UIView Array Extension
11 | extension Array where Element: UIView {
12 |
13 | /// Sorts an array of `UIView`s arranging them from left to right, top to bottom based on their minX and minY values.
14 | ///
15 | /// - Parameter container: Superview of all elements to be sorted by position.
16 | /// - Returns: Sorted array of `UIView` elements.
17 | func sortedByPosition(in container: UIView) -> [Element] {
18 | return sorted(by: { (view1: Element, view2: Element) -> Bool in
19 |
20 | let adjustedFrame1 = view1.convert(view1.frame, to: container)
21 | let adjustedFrame2 = view2.convert(view2.frame, to: container)
22 |
23 | let minX1 = adjustedFrame1.minX
24 | let minY1 = adjustedFrame1.minY
25 | let minX2 = adjustedFrame2.minX
26 | let minY2 = adjustedFrame2.minY
27 |
28 | if minY1 != minY2 {
29 | return minY1 < minY2
30 | } else {
31 | return minX1 < minX2
32 | }
33 | })
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/KeyboardSupport.podspec:
--------------------------------------------------------------------------------
1 | #
2 | # Be sure to run `pod lib lint KeyboardSupport.podspec' to ensure this is a
3 | # valid spec before submitting.
4 | #
5 | # Any lines starting with a # are optional, but their use is encouraged
6 | # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html
7 | #
8 |
9 | Pod::Spec.new do |s|
10 | s.name = 'KeyboardSupport'
11 | s.version = '2.2.0'
12 | s.summary = 'Makes dealing with common keyboard tasks simpler and easier.'
13 |
14 | s.description = <<-DESC
15 | KeyboardSupport makes it easy to automatically handle keyboard dismissal and scrolling to the active text input. With a few lines of code, it’s also easy to implement navigation between text inputs via toolbar back/next buttons or the keyboard’s “Return” key.
16 | DESC
17 |
18 | s.homepage = 'https://github.com/BottleRocketStudios/iOS-KeyboardSupport'
19 | s.license = { :type => 'Apache', :file => 'LICENSE' }
20 | s.author = { 'Bottle Rocket Studios' => 'earl.gaspard@bottlerocketstudios.com' }
21 | s.source = { :git => 'https://github.com/bottlerocketstudios/iOS-KeyboardSupport.git', :tag => s.version.to_s }
22 |
23 | s.swift_version = '5.0'
24 | s.ios.deployment_target = '12.0'
25 | s.source_files = 'Sources/**/*'
26 | s.frameworks = 'Foundation', 'UIKit'
27 |
28 | end
29 |
--------------------------------------------------------------------------------
/Tests/Helper/Mocks/MockKeyboardAutoNavigatorDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MockKeyboardAutoNavigatorDelegate.swift
3 | // Tests
4 | //
5 | // Copyright © 2018 Bottle Rocket. All rights reserved.
6 | //
7 |
8 | import Foundation
9 | import KeyboardSupport
10 |
11 | /// Mock Delegate to be used when testing the behavior of how KeyboardAutoNavigator invokes its delegate
12 | class MockKeyboardAutoNavigatorDelegate: KeyboardAutoNavigatorDelegate {
13 | var keyboardNavigatorDidTapBackCount: Int = 0
14 | var keyboardNavigatorDidTapBackLastNavigator: KeyboardAutoNavigator?
15 |
16 | var keyboardNavigatorDidTapNextCount: Int = 0
17 | var keyboardNavigatorDidTapNextLastNavigator: KeyboardAutoNavigator?
18 |
19 | var keyboardNavigatorDidTapDoneCount: Int = 0
20 | var keyboardNavigatorDidTapDoneLastNavigator: KeyboardAutoNavigator?
21 |
22 | func keyboardAutoNavigatorDidTapBack(_ navigator: KeyboardAutoNavigator) {
23 | keyboardNavigatorDidTapBackCount += 1
24 | keyboardNavigatorDidTapBackLastNavigator = navigator
25 | }
26 |
27 | func keyboardAutoNavigatorDidTapNext(_ navigator: KeyboardAutoNavigator) {
28 | keyboardNavigatorDidTapNextCount += 1
29 | keyboardNavigatorDidTapNextLastNavigator = navigator
30 | }
31 |
32 | func keyboardAutoNavigatorDidTapDone(_ navigator: KeyboardAutoNavigator) {
33 | keyboardNavigatorDidTapDoneCount += 1
34 | keyboardNavigatorDidTapDoneLastNavigator = navigator
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Sources/Extensions/CGRect+Modifying.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CGRect+Modifying.swift
3 | // KeyboardSupport
4 | //
5 | // Copyright © 2018 Bottle Rocket. All rights reserved.
6 | //
7 |
8 | import CoreGraphics
9 |
10 | extension CGRect {
11 |
12 | /// Returns a new `CGRect` instance with a modified minX property
13 | ///
14 | /// - Parameter minX: New value for minX
15 | /// - Returns: New `CGRect` instance with new minX
16 | func modifying(minX: CGFloat) -> CGRect {
17 | return CGRect(x: minX, y: minY, width: width, height: height)
18 | }
19 |
20 | /// Returns a new `CGRect` instance with a modified minY property
21 | ///
22 | /// - Parameter minY: New value for minY
23 | /// - Returns: New `CGRect` instance with new minY
24 | func modifying(minY: CGFloat) -> CGRect {
25 | return CGRect(x: minX, y: minY, width: width, height: height)
26 | }
27 |
28 | /// Returns a new `CGRect` instance with a modified height property
29 | ///
30 | /// - Parameter height: New value for height
31 | /// - Returns: New `CGRect` instance with new height
32 | func modifying(height: CGFloat) -> CGRect {
33 | return CGRect(x: minX, y: minY, width: width, height: height)
34 | }
35 |
36 | /// Returns a new `CGRect` instance with a modified width property
37 | ///
38 | /// - Parameter width: New value for width
39 | /// - Returns: New `CGRect` instance with new width
40 | func modifying(width: CGFloat) -> CGRect {
41 | return CGRect(x: minX, y: minY, width: width, height: height)
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/Example/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | APPL
17 | CFBundleShortVersionString
18 | $(MARKETING_VERSION)
19 | CFBundleVersion
20 | 1
21 | LSRequiresIPhoneOS
22 |
23 | UILaunchStoryboardName
24 | LaunchScreen
25 | UIMainStoryboardFile
26 | Main
27 | UIRequiredDeviceCapabilities
28 |
29 | armv7
30 |
31 | UISupportedInterfaceOrientations
32 |
33 | UIInterfaceOrientationPortrait
34 | UIInterfaceOrientationLandscapeLeft
35 | UIInterfaceOrientationLandscapeRight
36 |
37 | UISupportedInterfaceOrientations~ipad
38 |
39 | UIInterfaceOrientationPortrait
40 | UIInterfaceOrientationPortraitUpsideDown
41 | UIInterfaceOrientationLandscapeLeft
42 | UIInterfaceOrientationLandscapeRight
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/Example/Sources/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: Main
2 |
3 | on:
4 | push:
5 | branches: [ main, release/*]
6 | pull_request:
7 |
8 | jobs:
9 | Build:
10 | runs-on: macos-11
11 | env:
12 | DEVELOPER_DIR: /Applications/Xcode_13.2.app/Contents/Developer
13 | workspace: "KeyboardSupport.xcworkspace"
14 | strategy:
15 | fail-fast: false
16 | matrix:
17 | name: ["iOS"]
18 | include:
19 | - name: "iOS"
20 | scheme: "KeyboardSupport iOS"
21 | destination: "platform=iOS Simulator,OS=15.2,name=iPhone 12 Pro"
22 | test: true
23 |
24 | steps:
25 | - name: Checkout
26 | uses: actions/checkout@v2
27 |
28 | - name: Build and Test
29 | run: >
30 | if [[ ${{ matrix.test }} == true ]]; then
31 | xcodebuild test \
32 | -workspace ${{ env.workspace }} \
33 | -scheme "${{ matrix.scheme }}" \
34 | -destination "${{ matrix.destination }}" \
35 | ONLY_ACTIVE_ARCH=NO -enableCodeCoverage YES || exit 1
36 | else
37 | xcodebuild \
38 | -workspace ${{ env.workspace }} \
39 | -scheme "${{ matrix.scheme }}" \
40 | -destination "${{ matrix.destination }}" \
41 | ONLY_ACTIVE_ARCH=NO || exit 1
42 | fi
43 |
44 | Lint:
45 | runs-on: macos-11
46 | env:
47 | DEVELOPER_DIR: /Applications/Xcode_13.2.app/Contents/Developer
48 | cocoapods: true
49 | spm: true
50 |
51 | steps:
52 | - name: Checkout
53 | uses: actions/checkout@v2
54 |
55 | - name: Lint
56 | run: >
57 | if [[ ${{ env.spm }} == true ]]; then
58 | swift package describe
59 | fi
60 |
61 | if [[ ${{ env.cocoapods }} == true ]]; then
62 | pod lib lint
63 | fi
64 |
--------------------------------------------------------------------------------
/Sources/KeyboardAccessory.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeyboardAccessory.swift
3 | // KeyboardSupport
4 | //
5 | // Copyright © 2017 Bottle Rocket Studios. All rights reserved.
6 | //
7 |
8 | import UIKit
9 |
10 | @available(*, deprecated, renamed: "KeyboardAccessoryDelegate")
11 | public typealias KeyboardInputAccessoryDelegate = KeyboardAccessoryDelegate
12 |
13 | /// Contains callbacks for keyboard accessory navigation options.
14 | public protocol KeyboardAccessoryDelegate: AnyObject {
15 | func keyboardAccessoryDidTapBack(_ accessory: UIView)
16 | func keyboardAccessoryDidTapNext(_ accessory: UIView)
17 | func keyboardAccessoryDidTapDone(_ accessory: UIView)
18 | }
19 |
20 | public extension KeyboardAccessoryDelegate {
21 | func keyboardAccessoryDidTapBack(_ accessory: UIView) {}
22 | func keyboardAccessoryDidTapNext(_ accessory: UIView) {}
23 | func keyboardAccessoryDidTapDone(_ accessory: UIView) {}
24 | }
25 |
26 | @available(*, deprecated, renamed: "KeyboardAccessory")
27 | public typealias KeyboardInputAccessory = KeyboardAccessory
28 |
29 | /// Represents something that contains a done button and a `KeyboardAccessoryDelegate`.
30 | public protocol KeyboardAccessory: AnyObject {
31 | var doneButton: UIBarButtonItem? { get set }
32 |
33 | var keyboardAccessoryDelegate: KeyboardAccessoryDelegate? { get set }
34 | }
35 |
36 | /// Represents a keyboard accessory that contains navigation options in addition to the properties of `KeyboardAccessory`.
37 | public protocol NavigatingKeyboardAccessory: KeyboardAccessory {
38 | var nextButton: UIBarButtonItem? { get set }
39 | var backButton: UIBarButtonItem? { get set }
40 |
41 | func setNextAndBackButtonsHidden(_ hidden: Bool)
42 | }
43 |
44 | public typealias KeyboardAccessoryView = KeyboardAccessory & UIView
45 | public typealias NavigatingKeyboardAccessoryView = NavigatingKeyboardAccessory & UIView
46 |
--------------------------------------------------------------------------------
/Example/Sources/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "size" : "20x20",
6 | "scale" : "2x"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "size" : "20x20",
11 | "scale" : "3x"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "size" : "29x29",
16 | "scale" : "2x"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "size" : "29x29",
21 | "scale" : "3x"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "size" : "40x40",
26 | "scale" : "2x"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "size" : "40x40",
31 | "scale" : "3x"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "size" : "60x60",
36 | "scale" : "2x"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "size" : "60x60",
41 | "scale" : "3x"
42 | },
43 | {
44 | "idiom" : "ipad",
45 | "size" : "20x20",
46 | "scale" : "1x"
47 | },
48 | {
49 | "idiom" : "ipad",
50 | "size" : "20x20",
51 | "scale" : "2x"
52 | },
53 | {
54 | "idiom" : "ipad",
55 | "size" : "29x29",
56 | "scale" : "1x"
57 | },
58 | {
59 | "idiom" : "ipad",
60 | "size" : "29x29",
61 | "scale" : "2x"
62 | },
63 | {
64 | "idiom" : "ipad",
65 | "size" : "40x40",
66 | "scale" : "1x"
67 | },
68 | {
69 | "idiom" : "ipad",
70 | "size" : "40x40",
71 | "scale" : "2x"
72 | },
73 | {
74 | "idiom" : "ipad",
75 | "size" : "76x76",
76 | "scale" : "1x"
77 | },
78 | {
79 | "idiom" : "ipad",
80 | "size" : "76x76",
81 | "scale" : "2x"
82 | },
83 | {
84 | "idiom" : "ipad",
85 | "size" : "83.5x83.5",
86 | "scale" : "2x"
87 | },
88 | {
89 | "idiom" : "ios-marketing",
90 | "size" : "1024x1024",
91 | "scale" : "1x"
92 | }
93 | ],
94 | "info" : {
95 | "version" : 1,
96 | "author" : "xcode"
97 | }
98 | }
--------------------------------------------------------------------------------
/Tests/KeyboardAutoNavigatorTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeyboardAutoNavigatorTests.swift
3 | // Tests
4 | //
5 | // Copyright © 2018 Bottle Rocket. All rights reserved.
6 | //
7 |
8 | import XCTest
9 | @testable import KeyboardSupport
10 |
11 | class KeyboardAutoNavigatorTests: XCTestCase {
12 |
13 | // MARK: - Properties
14 |
15 | private var keyboardToolbar: KeyboardToolbar!
16 | private var keyboardNavigator: KeyboardAutoNavigator?
17 | private var delegateMock: MockKeyboardAutoNavigatorDelegate?
18 |
19 | // MARK: - Tests
20 |
21 | override func setUp() {
22 | super.setUp()
23 | delegateMock = MockKeyboardAutoNavigatorDelegate()
24 | keyboardToolbar = KeyboardToolbar()
25 | keyboardNavigator = KeyboardAutoNavigator(containerView: UIView(), defaultToolbar: keyboardToolbar, returnKeyNavigationEnabled: true)
26 | keyboardNavigator?.delegate = delegateMock
27 | }
28 |
29 | override func tearDown() {
30 | super.tearDown()
31 | delegateMock = nil
32 | keyboardToolbar = nil
33 | keyboardNavigator = nil
34 | }
35 |
36 | func test_keyboardAutoNavigator_invokesDelegateOnNext() {
37 | keyboardNavigator?.keyboardAccessoryDidTapNext(keyboardToolbar)
38 | XCTAssertEqual(delegateMock?.keyboardNavigatorDidTapNextCount, 1)
39 | XCTAssertTrue(delegateMock?.keyboardNavigatorDidTapNextLastNavigator === keyboardNavigator)
40 | }
41 |
42 | func test_keyboardAutoNavigator_invokesDelegateOnBack() {
43 | keyboardNavigator?.keyboardAccessoryDidTapBack(keyboardToolbar)
44 | XCTAssertEqual(delegateMock?.keyboardNavigatorDidTapBackCount, 1)
45 | XCTAssertTrue(delegateMock?.keyboardNavigatorDidTapBackLastNavigator === keyboardNavigator)
46 | }
47 |
48 | func test_keyboardAutoNavigator_invokesDelegateOnDone() {
49 | keyboardNavigator?.keyboardAccessoryDidTapDone(keyboardToolbar)
50 | XCTAssertEqual(delegateMock?.keyboardNavigatorDidTapDoneCount, 1)
51 | XCTAssertTrue(delegateMock?.keyboardNavigatorDidTapDoneLastNavigator === keyboardNavigator)
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/Documentation/Migrations/KeyboardSupport 1.x-2.0 Migration Guide.md:
--------------------------------------------------------------------------------
1 | # KeyboardSupport 1.x-2.0 Migration Guide
2 |
3 | This guide has been provided in order to ease the transition of existing applications using KeyboardSupport 1.x to the latest APIs, as well as to explain the structure of the new and changed functionality.
4 |
5 | ## Requirements
6 |
7 | - iOS 9.0
8 | - Swift 4.2
9 | - Xcode 10.1
10 |
11 | ## Overview
12 |
13 | KeyboardSupport 2.0 brings several refinements and improvements to the core functionality of KeyboardSupport. There are renaming and simplifications added but the overall functionality has not changed. Code changes should not be immediately neccessary to adopt this new version.
14 |
15 | ### Breaking Changes
16 |
17 | ### `KeyboardInputAccessory` and `KeyboardInputAccessoryDelegate` have been renamed
18 |
19 | The `KeyboardInputAccessory` protocol has been deprecated and renamed to `KeyboardAccessory`. Similarly, `KeyboardInputAccessoryDelegate` has been renamed to `KeyboardAccessoryDelegate`. Otherwise, their purpose and functionality remains the same.
20 |
21 | ### `KeyboardManager` and `KeyboardManagerDelegate` have been renamed
22 |
23 | The `KeyboardManager` class has been deprecated and renamed to `KeyboardNavigator`. Similarly, `KeyboardManagerDelegate` has been renamed to `KeyboardNavigatorDelegate`. The renaming better describes the class's purpose to assit with navigating back and forth between text inputs. Additionaly, `KeyboardNavigator` now supports navigating between `UITextView`s. Also, `KeyboardToolbar` is now the preferred view to pass into a `KeyboardNavigator` to allow navigating between text inputs.
24 |
25 | ### Additions
26 |
27 | ### `KeyboardToolbar` has been added
28 |
29 | This new subclass of `UIToolbar` allows you to quickly create an input accessory view for your text inputs. There are several ways you can add `UIBarButtonItem`s to it.
30 |
31 | ### `KeyboardScrollable`
32 |
33 | Hooks have been added to allow for animating your custom views alongside the keyboard appearance/disappearance animations. Simply implement `keyboardWillShow(_:)` and/or `keyboardWillHide(_:)` in your class that conforms to `KeyboardScrollable`.
34 |
--------------------------------------------------------------------------------
/Sources/KeyboardSafeAreaAdjustable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeyboardSafeAreaAdjustable.swift
3 | // KeyboardSupport
4 | //
5 | // Copyright © 2018 Bottle Rocket. All rights reserved.
6 | //
7 |
8 | import UIKit
9 |
10 | /// Enables automatic adjustment of additionalSafeAreaInsets when the keyboard is displayed
11 | @available(iOS 11.0, *)
12 | public protocol KeyboardSafeAreaAdjustable {
13 | func setupKeyboardSafeAreaListener()
14 | func stopKeyboardSafeAreaListener()
15 | }
16 |
17 | @available(iOS 11.0, *)
18 | extension KeyboardSafeAreaAdjustable where Self: UIViewController {
19 |
20 | public func setupKeyboardSafeAreaListener() {
21 | #if swift(>=4.2)
22 | NotificationCenter.default.addObserver(self, selector: #selector(keyboardFrameWillChange(_:)), name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
23 | #else
24 | NotificationCenter.default.addObserver(self, selector: #selector(keyboardFrameWillChange(_:)), name: .UIKeyboardWillChangeFrame, object: nil)
25 | #endif
26 | }
27 |
28 | public func stopKeyboardSafeAreaListener() {
29 | #if swift(>=4.2)
30 | NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
31 | #else
32 | NotificationCenter.default.removeObserver(self, name: .UIKeyboardWillChangeFrame, object: nil)
33 | #endif
34 | }
35 | }
36 |
37 | @available(iOS 11.0, *)
38 | fileprivate extension UIViewController {
39 |
40 | @objc func keyboardFrameWillChange(_ notification: Notification) {
41 | guard let keyboardInfo = KeyboardInfo(notification: notification) else { return }
42 |
43 | let keyboardFrameInView = view.convert(keyboardInfo.finalFrame, from: nil)
44 | let safeAreaFrame = view.safeAreaLayoutGuide.layoutFrame.insetBy(dx: 0, dy: -additionalSafeAreaInsets.bottom)
45 | let intersection = safeAreaFrame.intersection(keyboardFrameInView)
46 |
47 | UIView.animate(withDuration: keyboardInfo.animationDuration) {
48 | self.additionalSafeAreaInsets.bottom = intersection.height
49 | self.view.layoutIfNeeded()
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/Sources/Extensions/UIView+KeyboardSupport.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIView+KeyboardSupport.swift
3 | // KeyboardSupport
4 | //
5 | // Copyright © 2018 Bottle Rocket. All rights reserved.
6 | //
7 |
8 | import UIKit
9 |
10 | extension UIView {
11 |
12 | /// highest level superview of the view hierarchy
13 | var topLevelContainer: UIView {
14 | var returnView = self
15 |
16 | while let superview = returnView.superview {
17 | returnView = superview
18 | }
19 |
20 | return returnView
21 | }
22 |
23 | /// Computed array of UITextInput subviews. Walks the view hierachy starting with itself to build an array of non-hidden,
24 | /// UITextInput subviews that can become first responder.
25 | var textInputViews: [UITextInputView] {
26 | var fields: [UITextInputView] = []
27 |
28 | subviews.forEach { subview in
29 | guard !subview.isHidden else { return }
30 |
31 | if let textField = subview as? UITextInputView, textField.canBecomeFirstResponder, !textField.isHidden {
32 | fields.append(textField)
33 | } else {
34 | fields.append(contentsOf: subview.textInputViews)
35 | }
36 | }
37 |
38 | return fields
39 | }
40 |
41 | /// Attempts to resign first responder from a subview
42 | ///
43 | /// - Returns: Result of resignFirstResponder() or false if active first responder can not be found.
44 | @discardableResult
45 | public func resignActiveFirstResponder() -> Bool {
46 | return activeFirstResponder()?.resignFirstResponder() ?? false
47 | }
48 |
49 | /// Attempts to return a subview that is first responder
50 | ///
51 | /// - Returns: The subview that is currently first responder or nil if the first responder can not be found.
52 | public func activeFirstResponder() -> UIView? {
53 | return UIView.activeFirstResponder(for: self)
54 | }
55 |
56 | /// Static helper method to get the view that is the first responder
57 | static func activeFirstResponder(for view: UIView) -> UIView? {
58 | guard !view.isFirstResponder else { return view }
59 |
60 | for subview in view.subviews {
61 | if let firstResponder = activeFirstResponder(for: subview) {
62 | return firstResponder
63 | }
64 | }
65 |
66 | return nil
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/Example/Sources/SingleFieldAutoNavViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SingleFieldAutoNavViewController.swift
3 | // Example
4 | //
5 | // Copyright © 2018 Bottle Rocket. All rights reserved.
6 | //
7 |
8 | import UIKit
9 | import KeyboardSupport
10 |
11 | /// The `SingleFieldAutoNavViewController` demonstrates the configuration and use of a `KeyboardAutoNavigator` instance in an environment with only one input field.
12 | ///
13 | /// Note that the `KeyboardAutoNavigator` will hide the next and back buttons of a `NavigatingKeyboardAccessory` when used in a situation where there are no additional fields to navigate to.
14 | class SingleFieldAutoNavViewController: UIViewController, KeyboardRespondable {
15 |
16 | // IBOutlets
17 | @IBOutlet private var scrollView: UIScrollView!
18 | @IBOutlet private(set) var textField1: UITextField!
19 |
20 | // KeyboardScrollable
21 | var keyboardScrollableScrollView: UIScrollView?
22 | var keyboardWillShowObserver: NSObjectProtocol?
23 | var keyboardWillHideObserver: NSObjectProtocol?
24 |
25 | // KeyboardNavigator
26 | private(set) var keyboardNavigator: KeyboardAutoNavigator?
27 |
28 | // MARK: - Lifecycle
29 |
30 | override func viewDidLoad() {
31 | super.viewDidLoad()
32 |
33 | // KeyboardDismissable setup
34 | setupKeyboardDismissalView()
35 |
36 | // KeyboardScrollable setup
37 | keyboardScrollableScrollView = scrollView
38 | setupKeyboardObservers()
39 |
40 | // KeyboardToolbar setup
41 | let keyboardToolbar = KeyboardToolbar(frame: CGRect(x: 0, y: 0, width: view.bounds.width, height: 44.0))
42 | keyboardToolbar.addButton(type: .back, title: "Back")
43 | keyboardToolbar.addButton(type: .next, title: "Next")
44 | keyboardToolbar.addFlexibleSpace()
45 | keyboardToolbar.addSystemDoneButton()
46 |
47 | // KeyboardNavigator setup
48 | keyboardNavigator = KeyboardAutoNavigator(containerView: scrollView, defaultToolbar: keyboardToolbar, returnKeyNavigationEnabled: true)
49 | keyboardNavigator?.delegate = self
50 | }
51 |
52 | override func viewWillAppear(_ animated: Bool) {
53 | super.viewWillAppear(animated)
54 | setupKeyboardObservers()
55 | }
56 |
57 | override func viewWillDisappear(_ animated: Bool) {
58 | super.viewWillDisappear(animated)
59 | removeKeyboardObservers()
60 | }
61 | }
62 |
63 | // MARK: - KeyboardNavigatorDelegate
64 |
65 | extension SingleFieldAutoNavViewController: KeyboardAutoNavigatorDelegate {
66 |
67 | func keyboardAutoNavigatorDidTapBack(_ navigator: KeyboardAutoNavigator) {
68 | print("keyboardAutoNavigatorDidTapBack")
69 | }
70 |
71 | func keyboardAutoNavigatorDidTapNext(_ navigator: KeyboardAutoNavigator) {
72 | print("keyboardAutoNavigatorDidTapNext")
73 | }
74 |
75 | func keyboardAutoNavigatorDidTapDone(_ navigator: KeyboardAutoNavigator) {
76 | print("keyboardAutoNavigatorDidTapDone")
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/Example/Sources/AutoNavigatorViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AutoNavigatorViewController.swift
3 | // Example
4 | //
5 | // Copyright © 2018 Bottle Rocket. All rights reserved.
6 | //
7 |
8 | import UIKit
9 | import KeyboardSupport
10 |
11 | /// The `AutoNavigatorViewController` demonstrates the configuration and use of a `KeyboardAutoNavigator` instance.
12 | ///
13 | /// Note that the `KeyboardAutoNavigator` will disable and enable the next back buttons in a `NavigatingKeyboardAccessory` when no previous or next input field is available for navigation.
14 | class AutoNavigatorViewController: UIViewController, KeyboardRespondable {
15 |
16 | // IBOutlets
17 | @IBOutlet private var scrollView: UIScrollView!
18 | @IBOutlet private(set) var textField1: UITextField!
19 | @IBOutlet private(set) var textField2: UITextField!
20 | @IBOutlet private(set) var textField3: UITextField!
21 | @IBOutlet private(set) var textView: UITextView!
22 |
23 | // KeyboardScrollable
24 | var keyboardScrollableScrollView: UIScrollView? {
25 | return scrollView
26 | }
27 |
28 | var keyboardWillShowObserver: NSObjectProtocol?
29 | var keyboardWillHideObserver: NSObjectProtocol?
30 |
31 | // KeyboardNavigator
32 | private var keyboardNavigator: KeyboardAutoNavigator?
33 |
34 | // MARK: - Lifecycle
35 |
36 | override func viewDidLoad() {
37 | super.viewDidLoad()
38 |
39 | // KeyboardDismissable setup
40 | setupKeyboardDismissalView()
41 |
42 | // KeyboardToolbar setup
43 | let keyboardToolbar = KeyboardToolbar(frame: CGRect(x: 0, y: 0, width: view.bounds.width, height: 44.0))
44 | keyboardToolbar.addButton(type: .back, title: "Back")
45 | keyboardToolbar.addButton(type: .next, title: "Next")
46 | keyboardToolbar.addFlexibleSpace()
47 | keyboardToolbar.addSystemDoneButton()
48 |
49 | // KeyboardNavigator setup
50 | keyboardNavigator = KeyboardAutoNavigator(containerView: scrollView, defaultToolbar: keyboardToolbar, returnKeyNavigationEnabled: true)
51 | keyboardNavigator?.delegate = self
52 | }
53 |
54 | override func viewWillAppear(_ animated: Bool) {
55 | super.viewWillAppear(animated)
56 | setupKeyboardObservers()
57 | }
58 |
59 | override func viewWillDisappear(_ animated: Bool) {
60 | super.viewWillDisappear(animated)
61 | removeKeyboardObservers()
62 | }
63 | }
64 |
65 | // MARK: - KeyboardAutoNavigatorDelegate
66 |
67 | extension AutoNavigatorViewController: KeyboardAutoNavigatorDelegate {
68 |
69 | func keyboardAutoNavigatorDidTapBack(_ navigator: KeyboardAutoNavigator) {
70 | print("keyboardAutoNavigatorDidTapBack")
71 | }
72 |
73 | func keyboardAutoNavigatorDidTapNext(_ navigator: KeyboardAutoNavigator) {
74 | print("keyboardAutoNavigatorDidTapNext")
75 | }
76 |
77 | func keyboardAutoNavigatorDidTapDone(_ navigator: KeyboardAutoNavigator) {
78 | print("keyboardAutoNavigatorDidTapDone")
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/Example/Sources/ViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.swift
3 | // Example
4 | //
5 | // Copyright © 2018 Bottle Rocket. All rights reserved.
6 | //
7 |
8 | import UIKit
9 | import KeyboardSupport
10 |
11 | class ViewController: UIViewController, KeyboardRespondable {
12 |
13 | // IBOutlets
14 | @IBOutlet private var scrollView: UIScrollView!
15 | @IBOutlet private var textField1: UITextField!
16 | @IBOutlet private var textField2: UITextField!
17 | @IBOutlet private var textField3: UITextField!
18 | @IBOutlet private var textView: UITextView!
19 |
20 | // KeyboardScrollable
21 | var keyboardScrollableScrollView: UIScrollView? {
22 | return scrollView
23 | }
24 | var keyboardWillShowObserver: NSObjectProtocol?
25 | var keyboardWillHideObserver: NSObjectProtocol?
26 |
27 | // KeyboardNavigator
28 | private var keyboardNavigator: KeyboardNavigator?
29 |
30 | // MARK: - Lifecycle
31 |
32 | override func viewDidLoad() {
33 | super.viewDidLoad()
34 |
35 | // KeyboardDismissable setup
36 | setupKeyboardDismissalView()
37 |
38 | // KeyboardToolbar setup
39 | let keyboardToolbar = KeyboardToolbar(frame: CGRect(x: 0, y: 0, width: view.bounds.width, height: 50))
40 | keyboardToolbar.addButton(type: .back, title: "Back")
41 | keyboardToolbar.addButton(type: .next, title: "Next")
42 | keyboardToolbar.addFlexibleSpace()
43 | keyboardToolbar.addSystemDoneButton()
44 |
45 | // KeyboardNavigator setup
46 | keyboardNavigator = KeyboardNavigator(textInputs: [textField1, textField2, textView, textField3], keyboardToolbar: keyboardToolbar, returnKeyNavigationEnabled: true)
47 | keyboardNavigator?.delegate = self
48 | }
49 |
50 | override func viewWillAppear(_ animated: Bool) {
51 | super.viewWillAppear(animated)
52 | setupKeyboardObservers()
53 | }
54 |
55 | override func viewWillDisappear(_ animated: Bool) {
56 | super.viewWillDisappear(animated)
57 | removeKeyboardObservers()
58 | }
59 |
60 | // MARK: - KeyboardScrollable
61 |
62 | func keyboardWillShow(keyboardInfo: KeyboardInfo) {
63 | // Implement any custom animations or code you want to run alongside the appearance of the keyboard
64 | print("keyboardWillShow")
65 | }
66 |
67 | func keyboardWillHide(keyboardInfo: KeyboardInfo) {
68 | // Implement any custom animations or code you want to run alongside the disappearance of the keyboard
69 | print("keyboardWillHide")
70 | }
71 | }
72 |
73 | // MARK: - KeyboardNavigatorDelegate
74 |
75 | extension ViewController: KeyboardNavigatorDelegate {
76 |
77 | func keyboardNavigatorDidTapBack(_ navigator: KeyboardNavigator) {
78 | print("keyboardNavigatorDidTapBack")
79 | }
80 |
81 | func keyboardNavigatorDidTapNext(_ navigator: KeyboardNavigator) {
82 | print("keyboardNavigatorDidTapNext")
83 | }
84 |
85 | func keyboardNavigatorDidTapDone(_ navigator: KeyboardNavigator) {
86 | print("keyboardNavigatorDidTapDone")
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/Example/Example.xcodeproj/xcshareddata/xcschemes/iOS Example.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
43 |
45 |
51 |
52 |
53 |
54 |
60 |
62 |
68 |
69 |
70 |
71 |
73 |
74 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/KeyboardSupport.xcodeproj/xcshareddata/xcschemes/KeyboardSupport iOS.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
33 |
39 |
40 |
41 |
42 |
43 |
53 |
54 |
60 |
61 |
67 |
68 |
69 |
70 |
72 |
73 |
76 |
77 |
78 |
--------------------------------------------------------------------------------
/Tests/KeyboardToolbarTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeyboardToolbarTests.swift
3 | // Tests
4 | //
5 | // Copyright © 2018 Bottle Rocket. All rights reserved.
6 | //
7 |
8 | import XCTest
9 | @testable import KeyboardSupport
10 |
11 | class KeyboardToolbarTests: XCTestCase {
12 |
13 | func test_initWithFrame() {
14 | let frame = CGRect(x: 0, y: 0, width: 50, height: 50)
15 | let keyboardToolbar = KeyboardToolbar(frame: frame)
16 |
17 | XCTAssertNotNil(keyboardToolbar)
18 | XCTAssertEqual(keyboardToolbar.frame, frame)
19 | XCTAssertTrue((keyboardToolbar.items!.isEmpty))
20 | }
21 |
22 | func test_initWithCoder() {
23 | let archiver = NSKeyedUnarchiver(forReadingWith: Data())
24 | let keyboardToolbar = KeyboardToolbar(coder: archiver)
25 |
26 | XCTAssertNotNil(keyboardToolbar)
27 | XCTAssertTrue(keyboardToolbar!.items!.isEmpty)
28 | }
29 |
30 | func test_addButton() {
31 | let keyboardToolbar = KeyboardToolbar()
32 | let barButton = UIBarButtonItem(title: "Title", style: .plain, target: self, action: #selector(buttonTapped))
33 | keyboardToolbar.addButton(barButton)
34 |
35 | XCTAssertEqual(keyboardToolbar.items?.first, barButton)
36 | }
37 |
38 | func test_addBackButtonWithTitle() {
39 | let keyboardToolbar = KeyboardToolbar()
40 | keyboardToolbar.addButton(type: .back, title: "Back")
41 |
42 | XCTAssertEqual(keyboardToolbar.items?.count, 1)
43 | XCTAssertNotNil(keyboardToolbar.backButton)
44 | }
45 |
46 | func test_addBackButtonWithImage() {
47 | let keyboardToolbar = KeyboardToolbar()
48 | keyboardToolbar.addButton(type: .back, image: UIImage())
49 |
50 | XCTAssertEqual(keyboardToolbar.items?.count, 1)
51 | XCTAssertNotNil(keyboardToolbar.backButton)
52 | }
53 |
54 | func test_addNextButtonWithTitle() {
55 | let keyboardToolbar = KeyboardToolbar()
56 | keyboardToolbar.addButton(type: .next, title: "Next")
57 |
58 | XCTAssertEqual(keyboardToolbar.items?.count, 1)
59 | XCTAssertNotNil(keyboardToolbar.nextButton)
60 | }
61 |
62 | func test_addNextButtonWithImage() {
63 | let keyboardToolbar = KeyboardToolbar()
64 | keyboardToolbar.addButton(type: .next, image: UIImage())
65 |
66 | XCTAssertEqual(keyboardToolbar.items?.count, 1)
67 | XCTAssertNotNil(keyboardToolbar.nextButton)
68 | }
69 |
70 | func test_addDoneButtonWithTitle() {
71 | let keyboardToolbar = KeyboardToolbar()
72 | keyboardToolbar.addButton(type: .done, title: "Done")
73 |
74 | XCTAssertEqual(keyboardToolbar.items?.count, 1)
75 | XCTAssertNotNil(keyboardToolbar.doneButton)
76 | }
77 |
78 | func test_addDoneButtonWithImage() {
79 | let keyboardToolbar = KeyboardToolbar()
80 | keyboardToolbar.addButton(type: .done, image: UIImage())
81 |
82 | XCTAssertEqual(keyboardToolbar.items?.count, 1)
83 | XCTAssertNotNil(keyboardToolbar.doneButton)
84 | }
85 |
86 | func test_addSystemDoneButton() {
87 | let keyboardToolbar = KeyboardToolbar()
88 | keyboardToolbar.addSystemDoneButton()
89 |
90 | XCTAssertEqual(keyboardToolbar.items?.count, 1)
91 | XCTAssertNotNil(keyboardToolbar.doneButton)
92 | }
93 |
94 | func test_addFlexibleSpace() {
95 | let keyboardToolbar = KeyboardToolbar()
96 | keyboardToolbar.addFlexibleSpace()
97 |
98 | XCTAssertEqual(keyboardToolbar.items?.count, 1)
99 | }
100 |
101 | func test_backButtonTapped() {
102 | let keyboardToolbar = KeyboardToolbar()
103 | let mockDelegate = MockKeyboardAccessoryDelegate()
104 | keyboardToolbar.keyboardAccessoryDelegate = mockDelegate
105 | keyboardToolbar.addButton(type: .back, title: "Back")
106 | keyboardToolbar.backButtonTapped(keyboardToolbar.items!.first!)
107 | XCTAssertEqual(mockDelegate.tapType, .back)
108 | }
109 |
110 | func test_nextButtonTapped() {
111 | let keyboardToolbar = KeyboardToolbar()
112 | let mockDelegate = MockKeyboardAccessoryDelegate()
113 | keyboardToolbar.keyboardAccessoryDelegate = mockDelegate
114 | keyboardToolbar.addButton(type: .next, title: "Next")
115 | keyboardToolbar.nextButtonTapped(keyboardToolbar.items!.first!)
116 | XCTAssertEqual(mockDelegate.tapType, .next)
117 | }
118 |
119 | func test_doneButtonTapped() {
120 | let keyboardToolbar = KeyboardToolbar()
121 | let mockDelegate = MockKeyboardAccessoryDelegate()
122 | keyboardToolbar.keyboardAccessoryDelegate = mockDelegate
123 | keyboardToolbar.addButton(type: .done, title: "Done")
124 | keyboardToolbar.doneButtonTapped(keyboardToolbar.items!.first!)
125 | XCTAssertEqual(mockDelegate.tapType, .done)
126 | }
127 | }
128 |
129 | private extension KeyboardToolbarTests {
130 |
131 | @objc private func buttonTapped(_ sender: UIBarButtonItem) {}
132 | }
133 |
--------------------------------------------------------------------------------
/Sources/KeyboardNavigator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeyboardNavigator.swift
3 | // KeyboardSupport
4 | //
5 | // Copyright © 2018 Bottle Rocket. All rights reserved.
6 | //
7 |
8 | import UIKit
9 |
10 | @available(*, deprecated, renamed: "KeyboardNavigatorDelegate")
11 | public typealias KeyboardManagerDelegate = KeyboardNavigatorDelegate
12 |
13 | /// Contains callbacks for `KeyboardNavigator` navigation options.
14 | public protocol KeyboardNavigatorDelegate: AnyObject {
15 | func keyboardNavigatorDidTapBack(_ navigator: KeyboardNavigator)
16 | func keyboardNavigatorDidTapNext(_ navigator: KeyboardNavigator)
17 | func keyboardNavigatorDidTapDone(_ navigator: KeyboardNavigator)
18 | }
19 |
20 | @available(*, deprecated, renamed: "KeyboardNavigator")
21 | public typealias KeyboardManager = KeyboardNavigator
22 |
23 | public typealias UITextInputView = UIView & UITextInput
24 |
25 | /// Base class for KeyboardNavigators that aggregates a KeyboardToolbar instance and a flag that represents the enabled state of return key navigation
26 | open class KeyboardNavigatorBase {
27 | public private(set) var keyboardToolbar: KeyboardAccessoryView?
28 | public private(set) var returnKeyNavigationEnabled: Bool
29 |
30 | init(keyboardToolbar: KeyboardAccessoryView? = nil, returnKeyNavigationEnabled: Bool = false) {
31 | self.keyboardToolbar = keyboardToolbar
32 | self.returnKeyNavigationEnabled = returnKeyNavigationEnabled
33 | }
34 | }
35 |
36 | /// An object for handling navigation between text inputs.
37 | open class KeyboardNavigator: KeyboardNavigatorBase {
38 |
39 | // MARK: - Properties
40 |
41 | public private(set) var textInputs: [UITextInput]
42 | public var currentTextInputIndex = 0
43 | weak open var delegate: KeyboardNavigatorDelegate?
44 |
45 | // MARK: - Init
46 |
47 | public init(textInputs: [UITextInput], keyboardToolbar: KeyboardAccessoryView? = nil, returnKeyNavigationEnabled: Bool = false) {
48 | self.textInputs = textInputs
49 |
50 | super.init(keyboardToolbar: keyboardToolbar, returnKeyNavigationEnabled: returnKeyNavigationEnabled)
51 |
52 | addTargets()
53 | addInputAccessoryViews()
54 | }
55 |
56 | // MARK: - Private Methods
57 |
58 | private func addTargets() {
59 | textInputs.forEach {
60 | if let textField = $0 as? UITextField {
61 | // Updates currentIndex when a text field is tapped.
62 | textField.addTarget(self, action: #selector(textFieldEditingDidBegin(_:)), for: .editingDidBegin)
63 | // Notifies us when the keyboard's return button is tapped.
64 | textField.addTarget(self, action: #selector(textFieldEditingDidEndOnExit(_:)), for: .editingDidEndOnExit)
65 | }
66 | }
67 | }
68 |
69 | private func addInputAccessoryViews() {
70 | keyboardToolbar?.keyboardAccessoryDelegate = self
71 | textInputs.forEach {
72 | if let textField = $0 as? UITextField {
73 | textField.inputAccessoryView = keyboardToolbar
74 | } else if let textView = $0 as? UITextView {
75 | textView.inputAccessoryView = keyboardToolbar
76 | }
77 | }
78 | }
79 | }
80 |
81 | // MARK: - Private Extension
82 |
83 | private extension KeyboardNavigator {
84 |
85 | var currentTextInput: UIResponder? {
86 | return textInputs[currentTextInputIndex] as? UIResponder
87 | }
88 |
89 | func didTapBack() {
90 | if currentTextInputIndex > 0 {
91 | currentTextInputIndex -= 1
92 | currentTextInput?.becomeFirstResponder()
93 | }
94 |
95 | delegate?.keyboardNavigatorDidTapBack(self)
96 | }
97 |
98 | func didTapNext() {
99 | if currentTextInputIndex < textInputs.count - 1 {
100 | currentTextInputIndex += 1
101 | currentTextInput?.becomeFirstResponder()
102 | }
103 |
104 | delegate?.keyboardNavigatorDidTapNext(self)
105 | }
106 |
107 | func didTapDone() {
108 | currentTextInput?.resignFirstResponder()
109 | delegate?.keyboardNavigatorDidTapDone(self)
110 | }
111 |
112 | @objc func textFieldEditingDidBegin(_ textField: UITextField) {
113 | if let index = textInputs.firstIndex(where: { $0 as? UITextField == textField }) {
114 | currentTextInputIndex = index
115 | }
116 | }
117 |
118 | @objc func textFieldEditingDidEndOnExit(_ textField: UITextField) {
119 | if textField == textInputs.last as? UITextField {
120 | didTapDone()
121 | } else {
122 | didTapNext()
123 | }
124 | }
125 | }
126 |
127 | // MARK: - KeyboardAccessoryDelegate
128 |
129 | extension KeyboardNavigator: KeyboardAccessoryDelegate {
130 |
131 | public func keyboardAccessoryDidTapBack(_ accessory: UIView) {
132 | didTapBack()
133 | }
134 |
135 | public func keyboardAccessoryDidTapNext(_ accessory: UIView) {
136 | didTapNext()
137 | }
138 |
139 | public func keyboardAccessoryDidTapDone(_ accessory: UIView) {
140 | didTapDone()
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/Sources/KeyboardToolbar.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeyboardToolbar.swift
3 | // KeyboardSupport
4 | //
5 | // Copyright © 2018 Bottle Rocket. All rights reserved.
6 | //
7 |
8 | import UIKit
9 |
10 | /// AutoNavigator will ask a TextInputView that conforms to this protocol for a toolbar to use in place of the default toolbar.
11 | /// If this variable is nil, the default toolbar of the KeyboardAutoNavigator will be used.
12 | protocol KeyboardToolbarProviding {
13 | var keyboardToolbar: KeyboardAccessoryView? { get }
14 | }
15 |
16 | /// An object that displays a toolbar above the keyboard.
17 | open class KeyboardToolbar: UIToolbar, NavigatingKeyboardAccessory {
18 |
19 | // MARK: - Sub-types
20 |
21 | public enum ButtonNavigationType {
22 | case back
23 | case next
24 | case done
25 |
26 | var action: Selector {
27 | switch self {
28 | case .back:
29 | return #selector(backButtonTapped)
30 | case .next:
31 | return #selector(nextButtonTapped)
32 | case .done:
33 | return #selector(doneButtonTapped)
34 | }
35 | }
36 | }
37 |
38 | // MARK: - KeyboardAccessory
39 |
40 | open weak var keyboardAccessoryDelegate: KeyboardAccessoryDelegate?
41 | open var nextButton: UIBarButtonItem?
42 | open var backButton: UIBarButtonItem?
43 | open var doneButton: UIBarButtonItem?
44 |
45 | // MARK: - Init
46 |
47 | override public init(frame: CGRect) {
48 | super.init(frame: frame)
49 | items = []
50 | }
51 |
52 | required public init?(coder aDecoder: NSCoder) {
53 | super.init(coder: aDecoder)
54 | items = []
55 | }
56 |
57 | // MARK: - Configuring Buttons
58 |
59 | /// Adds a `UIBarButtonItem` to the toolbar.
60 | open func addButton(_ button: UIBarButtonItem) {
61 | items?.append(button)
62 | }
63 |
64 | /// Adds a `UIBarButtonItem` with a title for a `KeyboardToolbarButtonNavigationType`.
65 | open func addButton(type: ButtonNavigationType, title: String, width: CGFloat? = nil) {
66 | let button = UIBarButtonItem(title: title, style: .plain, target: self, action: type.action)
67 | width.flatMap { button.width = $0 }
68 | storeButton(button, ofType: type)
69 | items?.append(button)
70 | }
71 |
72 | /// Adds a `UIBarButtonItem` with images for a `KeyboardToolbarButtonNavigationType`.
73 | open func addButton(type: ButtonNavigationType, image: UIImage, landscapeImagePhone: UIImage? = nil, width: CGFloat? = nil) {
74 | let button = UIBarButtonItem(image: image, landscapeImagePhone: landscapeImagePhone, style: .plain, target: self, action: type.action)
75 | width.flatMap { button.width = $0 }
76 | storeButton(button, ofType: type)
77 | items?.append(button)
78 | }
79 |
80 | /// Adds a `UIBarButtonItem` set to the system item of `.done` for ending navigation.
81 | open func addSystemDoneButton() {
82 | let button = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(doneButtonTapped))
83 | doneButton = button
84 | items?.append(button)
85 | }
86 |
87 | /// Adds a `UIBarButtonItem` to the toolbar to show blank space between items.
88 | open func addFlexibleSpace() {
89 | let flexibleSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
90 | items?.append(flexibleSpace)
91 | }
92 | }
93 |
94 | // MARK: - Private helpers
95 | private extension KeyboardToolbar {
96 | private func storeButton(_ button: UIBarButtonItem, ofType type: ButtonNavigationType) {
97 | switch type {
98 | case .back:
99 | backButton = button
100 | case .next:
101 | nextButton = button
102 | case .done:
103 | doneButton = button
104 | }
105 | }
106 | }
107 |
108 | // MARK: - methods for hiding next and back buttons
109 | public extension KeyboardToolbar {
110 | func setNextAndBackButtonsHidden(_ hidden: Bool) {
111 | if hidden {
112 | removeNextButton()
113 | removeBackButton()
114 | } else {
115 | replaceNextAndBackButtons()
116 | }
117 | }
118 |
119 | private func replaceNextAndBackButtons() {
120 | let currentNextButtonIndex = nextButton.flatMap { items?.firstIndex(of: $0) }
121 | let currentBackButtonIndex = backButton.flatMap { items?.firstIndex(of: $0) }
122 |
123 | // If either button is not present, clean them out, and replace them.
124 | if currentBackButtonIndex == nil || currentNextButtonIndex == nil {
125 | removeBackButton()
126 | removeNextButton()
127 |
128 | nextButton.flatMap { items?.insert($0, at: 0) }
129 | backButton.flatMap { items?.insert($0, at: 0) }
130 | }
131 | }
132 |
133 | private func removeNextButton() {
134 | guard let nextButton = nextButton, let nextButtonIndex = items?.firstIndex(of: nextButton) else { return }
135 | items?.remove(at: nextButtonIndex)
136 | }
137 |
138 | private func removeBackButton() {
139 | guard let backButton = backButton, let backButtonIndex = items?.firstIndex(of: backButton) else { return }
140 | items?.remove(at: backButtonIndex)
141 | }
142 | }
143 |
144 | public extension KeyboardToolbar {
145 |
146 | @objc func backButtonTapped(_ sender: UIBarButtonItem) {
147 | keyboardAccessoryDelegate?.keyboardAccessoryDidTapBack(self)
148 | }
149 |
150 | @objc func nextButtonTapped(_ sender: UIBarButtonItem) {
151 | keyboardAccessoryDelegate?.keyboardAccessoryDidTapNext(self)
152 | }
153 |
154 | @objc func doneButtonTapped(_ sender: UIBarButtonItem) {
155 | keyboardAccessoryDelegate?.keyboardAccessoryDidTapDone(self)
156 | }
157 | }
158 |
--------------------------------------------------------------------------------
/Tests/KeyboardNavigatorTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeyboardNavigatorTests.swift
3 | // Tests
4 | //
5 | // Copyright © 2018 Bottle Rocket. All rights reserved.
6 | //
7 |
8 | import XCTest
9 | @testable import KeyboardSupport
10 |
11 | class KeyboardNavigatorTests: XCTestCase {
12 |
13 | // MARK: - Properties
14 |
15 | private let keyboardToolbar = KeyboardToolbar()
16 |
17 | // MARK: - Tests
18 |
19 | func test_KeyboardNavigator_InitializesWithTextFields() {
20 | // Arrange
21 | let keyboardNavigator = keyboardNavigatorWithTextFields()
22 |
23 | // Assert
24 | XCTAssertEqual(keyboardNavigator.textInputs.count, 3)
25 | XCTAssertTrue(keyboardNavigator.keyboardToolbar === keyboardToolbar)
26 | XCTAssertTrue(keyboardNavigator.returnKeyNavigationEnabled)
27 | XCTAssertEqual(keyboardNavigator.currentTextInputIndex, 0)
28 | }
29 |
30 | func test_KeyboardNavigator_InitializesWithTextViews() {
31 | // Arrange
32 | let keyboardNavigator = keyboardNavigatorWithTextViews()
33 |
34 | // Assert
35 | XCTAssertEqual(keyboardNavigator.textInputs.count, 3)
36 | XCTAssertTrue(keyboardNavigator.keyboardToolbar === keyboardToolbar)
37 | XCTAssertTrue(keyboardNavigator.returnKeyNavigationEnabled)
38 | XCTAssertEqual(keyboardNavigator.currentTextInputIndex, 0)
39 | }
40 |
41 | func test_KeyboardNavigatorWithTextFields_AccessoryDidTapBack() {
42 | // Arrange
43 | let textFieldNavigator = keyboardNavigatorWithTextFields()
44 | let mockDelegate = MockKeyboardNavigatorDelegate()
45 | textFieldNavigator.delegate = mockDelegate
46 |
47 | // Act
48 | textFieldNavigator.keyboardAccessoryDidTapNext(UIView())
49 | textFieldNavigator.keyboardAccessoryDidTapBack(UIView())
50 |
51 | // Assert
52 | XCTAssertEqual(textFieldNavigator.currentTextInputIndex, 0)
53 | XCTAssertEqual(mockDelegate.tapType, TapType.back)
54 | }
55 |
56 | func test_KeyboardNavigatorWithTextViews_AccessoryDidTapBack() {
57 | // Arrange
58 | let textViewNavigator = keyboardNavigatorWithTextViews()
59 | let mockDelegate = MockKeyboardNavigatorDelegate()
60 | textViewNavigator.delegate = mockDelegate
61 |
62 | // Act
63 | textViewNavigator.keyboardAccessoryDidTapNext(UIView())
64 | textViewNavigator.keyboardAccessoryDidTapBack(UIView())
65 |
66 | // Assert
67 | XCTAssertEqual(textViewNavigator.currentTextInputIndex, 0)
68 | XCTAssertEqual(mockDelegate.tapType, TapType.back)
69 | }
70 |
71 | func test_KeyboardNavigatorWithTextFields_AccessoryDidTapNext() {
72 | // Arrange
73 | let textFieldNavigator = keyboardNavigatorWithTextFields()
74 | let mockDelegate = MockKeyboardNavigatorDelegate()
75 | textFieldNavigator.delegate = mockDelegate
76 |
77 | // Act
78 | textFieldNavigator.keyboardAccessoryDidTapNext(UIView())
79 |
80 | // Assert
81 | XCTAssertEqual(textFieldNavigator.currentTextInputIndex, 1)
82 | XCTAssertEqual(mockDelegate.tapType, TapType.next)
83 | }
84 |
85 | func test_KeyboardNavigatorWithTextViews_AccessoryDidTapNext() {
86 | // Arrange
87 | let textViewNavigator = keyboardNavigatorWithTextViews()
88 | let mockDelegate = MockKeyboardNavigatorDelegate()
89 | textViewNavigator.delegate = mockDelegate
90 |
91 | // Act
92 | textViewNavigator.keyboardAccessoryDidTapNext(UIView())
93 |
94 | // Assert
95 | XCTAssertEqual(textViewNavigator.currentTextInputIndex, 1)
96 | XCTAssertEqual(mockDelegate.tapType, TapType.next)
97 | }
98 |
99 | func test_KeyboardNavigatorWithTextFields_AccessoryDidTapDone() {
100 | // Arrange
101 | let textFieldNavigator = keyboardNavigatorWithTextFields()
102 | let mockDelegate = MockKeyboardNavigatorDelegate()
103 | textFieldNavigator.delegate = mockDelegate
104 |
105 | // Act
106 | textFieldNavigator.keyboardAccessoryDidTapDone(UIView())
107 |
108 | // Assert
109 | XCTAssertEqual(mockDelegate.tapType, TapType.done)
110 | }
111 |
112 | func test_KeyboardNavigatorWithTextViews_AccessoryDidTapDone() {
113 | // Arrange
114 | let textViewNavigator = keyboardNavigatorWithTextViews()
115 | let mockDelegate = MockKeyboardNavigatorDelegate()
116 | textViewNavigator.delegate = mockDelegate
117 |
118 | // Act
119 | textViewNavigator.keyboardAccessoryDidTapDone(UIView())
120 |
121 | // Assert
122 | XCTAssertEqual(mockDelegate.tapType, TapType.done)
123 | }
124 | }
125 |
126 | // MARK: - KeyboardNavigator Initialization Helpers
127 |
128 | private extension KeyboardNavigatorTests {
129 |
130 | func keyboardNavigatorWithTextFields() -> KeyboardNavigator {
131 | let textInput1 = UITextField(frame: CGRect(x: 0, y: 0, width: 100, height: 50))
132 | let textInput2 = UITextField(frame: CGRect(x: 0, y: 100, width: 100, height: 50))
133 | let textInput3 = UITextField(frame: CGRect(x: 0, y: 200, width: 100, height: 50))
134 | let keyboardNavigator = KeyboardNavigator(textInputs: [textInput1, textInput2, textInput3], keyboardToolbar: keyboardToolbar, returnKeyNavigationEnabled: true)
135 |
136 | return keyboardNavigator
137 | }
138 |
139 | func keyboardNavigatorWithTextViews() -> KeyboardNavigator {
140 | let textInput1 = UITextView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
141 | let textInput2 = UITextView(frame: CGRect(x: 0, y: 100, width: 100, height: 100))
142 | let textInput3 = UITextView(frame: CGRect(x: 0, y: 200, width: 100, height: 100))
143 | let keyboardNavigator = KeyboardNavigator(textInputs: [textInput1, textInput2, textInput3], keyboardToolbar: keyboardToolbar, returnKeyNavigationEnabled: true)
144 |
145 | return keyboardNavigator
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## Master
2 |
3 | ##### Enhancements
4 |
5 | * None
6 |
7 | ##### Bug Fixes
8 |
9 | * None
10 |
11 | ## 2.2.0 (2022-01-14)
12 |
13 | ##### Enhancements
14 |
15 | * Adds support to not offset a KeyboardScrollable view if the viewController is presented in a popover.
16 |
17 | ##### Bug Fixes
18 |
19 | * Only update contentInset if the new insets are different from the existing insets. This avoids triggering unnecessary table/collectionview reloads.
20 | [Dimitar Milinski](https://github.com/dmilinski08)
21 | [#62](https://github.com/BottleRocketStudios/iOS-KeyboardSupport/pull/62)
22 |
23 | ## 2.1.2 (2020-12-09)
24 |
25 | ##### Enhancements
26 |
27 | * Added Swift Package Manager support.
28 | [Wil Turner](https://github.com/WSTurner)
29 | [#44](https://github.com/BottleRocketStudios/iOS-KeyboardSupport/issues/44)
30 |
31 | ##### Bug Fixes
32 |
33 | * Allowed setting the originalContentInsets before and after setting up the kyboard observers in KeyboardScrollable. This allows us to do things like setting content insets in viewDidLayoutSubviews() and such
34 | [Fernando Arocho](https://github.com/Specialist17)
35 | [#57](https://github.com/BottleRocketStudios/iOS-KeyboardSupport/pull/57)
36 |
37 | * Fix issue with calculating content inset when scrollview is not constrained to bottom of safe area
38 | [Dimitar Milinski](https://github.com/dmilinski08)
39 | [#58](https://github.com/BottleRocketStudios/iOS-KeyboardSupport/pull/58)
40 |
41 | ## 2.1.1 (2020-01-16)
42 |
43 | ##### Enhancements
44 |
45 | * Added Carthage support.
46 | [Ryan Gant](https://github.com/ganttastic)
47 | [#46](https://github.com/BottleRocketStudios/iOS-KeyboardSupport/pull/46)
48 |
49 | ##### Bug Fixes
50 |
51 | * Addressed issue where scrolling to the focused text field would not work properly.
52 | [Daniel Larsen](https://github.com/grandlarseny)
53 | [#48](https://github.com/BottleRocketStudios/iOS-KeyboardSupport/pull/48)
54 |
55 | ## 2.1.0 (2019-04-30)
56 |
57 | ##### Enhancements
58 |
59 | * Move protocols and extensions in KeyboardRespondable to separate files.
60 | * Migrate to Swift 5.0.
61 | [Earl Gaspard](https://github.com/earlgaspard)
62 | [#41](https://github.com/BottleRocketStudios/iOS-KeyboardSupport/pull/41)
63 |
64 | * `KeyboardRespondable` and `KeyboardDismissable` setup methods now return the generated gesture recognizer so consumers can work with it.
65 | * Modified `KeyboardToolbar` to track the next, back, and done buttons so they can be hidden / enabled / disabled as needed.
66 | * Modified `KeyboardScrollable` to support an additional padding around a text input when moving it into view.
67 | * Added a `KeyboardAutoNavigator` that is initialized with a toolbar. It will apply this toolbar to all text inputs, unless those inputs provide their own via implementing the `KeyboardToolbarProviding` protocol. The autonavigator will walk the view hiearchy and seek out text inputs before and after the current field to provide navigation via the toolbar.
68 | [John Davis](https://github.com/br-johndavis)
69 | [#36](https://github.com/BottleRocketStudios/iOS-KeyboardSupport/pull/36)
70 |
71 | ##### Bug Fixes
72 |
73 | * None
74 |
75 | ## 2.0.2 (2019-01-09)
76 |
77 | ##### Enhancements
78 |
79 | * None
80 |
81 | ##### Bug Fixes
82 |
83 | * Fix protocol extension function signature mismatch in `KeyboardScrollable`.
84 | [Earl Gaspard](https://github.com/earlgaspard)
85 | [#34](https://github.com/BottleRocketStudios/iOS-KeyboardSupport/pull/34)
86 |
87 | ## 2.0.1 (2019-01-09)
88 |
89 | ##### Enhancements
90 |
91 | * None
92 |
93 | ##### Bug Fixes
94 |
95 | * Declare `KeyboardAccessoryDelegate` extension as public. Declare `KeyboardInfo`'s properties as public.
96 | [Earl Gaspard](https://github.com/earlgaspard)
97 | [#32](https://github.com/BottleRocketStudios/iOS-KeyboardSupport/pull/32)
98 |
99 | ## 2.0.0 (2019-01-07)
100 |
101 | ##### Enhancements
102 |
103 | * Added `KeyboardToolbar` for fast creation of input accessorty views. Renamed `KeyboardManager` to `KeyboardNavigator`. `KeyboardNavigator` supports navigating between `UITextView`s. Renamed `KeyboardInputAccessory` to `KeyboardAccessory`. Renaming of methods in `KeyboardDismissable` and methods in `KeyboardScrollable`. Animations added when keyboard appears when using `KeyboardScrollable`.
104 | [Earl Gaspard](https://github.com/earlgaspard)
105 | [#29](https://github.com/BottleRocketStudios/iOS-KeyboardSupport/pull/29)
106 |
107 | * Adjusted project structure to better support Travis-CI. CI is fully up-and-running on all supported platforms.
108 | [Earl Gaspard](https://github.com/earlgaspard)
109 | [#10](https://github.com/BottleRocketStudios/iOS-KeyboardSupport/pull/10)
110 |
111 | * Added SwiftLint.
112 | [Earl Gaspard](https://github.com/earlgaspard)
113 | [#10](https://github.com/BottleRocketStudios/iOS-KeyboardSupport/pull/10)
114 |
115 | ##### Bug Fixes
116 |
117 | * None
118 |
119 |
120 | ## 1.0.2 (2018-09-19)
121 |
122 | ##### Enhancements
123 |
124 | * **[BREAKING]** Renamed `KeyboardScrollable`'s `shouldPreserveContentInsetWhenKeyboardVisible` to `preservesContentInsetWhenKeyboardVisible` in order to fix a SwiftLint warning.
125 | [Tyler Milner](https://github.com/tylermilner)
126 | [#26](https://github.com/BottleRocketStudios/iOS-KeyboardSupport/pull/26)
127 |
128 | * Updated Travis-CI to Xcode 9.4 image.
129 | [Tyler Milner](https://github.com/tylermilner)
130 | [#21](https://github.com/BottleRocketStudios/iOS-KeyboardSupport/pull/21)
131 |
132 | * On UITextView's, KeyboardRespondable now scrolls to cursor/selection.
133 | [Cuong Leo Ngo ](https://github.com/cuongcngo)
134 | [#23](https://github.com/BottleRocketStudios/iOS-KeyboardSupport/pull/23)
135 |
136 | * Updated project for Xcode 10.
137 | [Tyler Milner](https://github.com/tylermilner)
138 | [#25](https://github.com/BottleRocketStudios/iOS-KeyboardSupport/pull/25)
139 |
140 | ##### Bug Fixes
141 |
142 | * None
143 |
144 |
145 | ## 1.0.1 (2018-08-20)
146 |
147 | ##### Enhancements
148 |
149 | * Adjusted project structure to better support Travis-CI. CI is fully up-and-running on all supported platforms.
150 | [Earl Gaspard](https://github.com/earlgaspard)
151 | [#10](https://github.com/BottleRocketStudios/iOS-KeyboardSupport/pull/10)
152 |
153 | * Added SwiftLint.
154 | [Earl Gaspard](https://github.com/earlgaspard)
155 | [#10](https://github.com/BottleRocketStudios/iOS-KeyboardSupport/pull/10)
156 |
157 | * Add option to disregard original content inset when keyboard is visible.
158 | [Cuong Leo Ngo ](https://github.com/cuongcngo)
159 | [#17](https://github.com/BottleRocketStudios/iOS-KeyboardSupport/pull/17)
160 |
161 | * Added conditional compilation for Swift 4.2 compatibility.
162 | [Tyler Milner](https://github.com/tylermilner)
163 | [#19](https://github.com/BottleRocketStudios/iOS-KeyboardSupport/pull/19)
164 |
165 | ##### Bug Fixes
166 |
167 | * Fix issue where bottom contentInset is added more than once when user taps on first responder view again.
168 | [Cuong Leo Ngo ](https://github.com/cuongcngo)
169 | [#12](https://github.com/BottleRocketStudios/iOS-KeyboardSupport/pull/12)
170 |
171 | * Now subtracting the Safe Area bottom inset when calculating additional bottom inset since the keyboard frame encompasses the safe area and scroll views' insets are typically automatically adjusted to account for safe area.
172 | [Cuong Leo Ngo ](https://github.com/cuongcngo)
173 | [#13](https://github.com/BottleRocketStudios/iOS-KeyboardSupport/pull/13)
174 |
175 | * Add KeyboardSafeAreaAdjustable protocol, and KeyboardScrollable restores original content inset
176 | [Cuong Leo Ngo ](https://github.com/cuongcngo)
177 | [#16](https://github.com/BottleRocketStudios/iOS-KeyboardSupport/pull/16)
178 |
179 |
180 | ## 1.0.0 (2017-12-28)
181 |
182 | ##### Initial Release
183 |
184 | This is our initial release of KeyboardSupport. Enjoy!
185 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # KeyboardSupport
2 | 
3 | [](http://cocoapods.org/pods/KeyboardSupport)
4 | [](https://github.com/Carthage/Carthage)
5 | [](http://cocoapods.org/pods/KeyboardSupport)
6 | [](http://cocoapods.org/pods/KeyboardSupport)
7 | [](https://codecov.io/gh/BottleRocketStudios/iOS-KeyboardSupport)
8 | [](https://codebeat.co/projects/github-com-bottlerocketstudios-ios-keyboardsupport-master)
9 |
10 | ## Purpose
11 |
12 | This library provides conveniences for dealing with common keyboard tasks. There are a few main goals:
13 |
14 | * Make it easy to auto-dismiss the keyboard via tap on screen.
15 | * Auto-scrolling to the active `UITextField` or `UITextView`.
16 | * Easily implement navigation between text inputs by supplying your own input accessory view.
17 | * Allow keyboard "Return" key to navigate between `UITextField`s.
18 | * Provide a `UIToolbar` subclass so you can create your own input accessory views faster.
19 |
20 | ## Key Concepts
21 |
22 | * **KeyboardDismissable** - A protocol that enables automatic keyboard dismissal via tapping the screen when the keyboard is displayed.
23 | * **KeyboardScrollable** - A protocol that enables scrolling views to the first responder when a keyboard is shown. Must be used with a `UIScrollView` or one of its subclasses.
24 | * **KeyboardRespondable** - Inherits from both `KeyboardDismissable` and `KeyboardScrollable` for convenience.
25 | * **KeyboardToolbar** - A subclass of `UIToolbar` with customization options to quickly create your own input accessory views.
26 | * **KeyboardAccessory** - Have your custom view conform to this protocol to get callbacks for "back", "next", and "done" for moving between text inputs.
27 | * **KeyboardNavigator** - Handles navigation between text inputs by providing your `KeyboardToolbar` or using the keyboard's return key.
28 |
29 | ## Usage
30 |
31 | ### KeyboardDismissable
32 | Conform to this protocol to enable keyboard dismissal via tapping the screen when the keyboard is displayed.
33 | ``` swift
34 | class ViewController: UIViewController, KeyboardDismissable {
35 |
36 | override func viewDidLoad() {
37 | super.viewDidLoad()
38 | setupKeyboardDismissalView()
39 | }
40 | }
41 | ```
42 |
43 | ### KeyboardScrollable
44 | Conform to this protocol to enable scrolling to the first responder when the keyboard is shown. Must be used with a `UIScrollView` or one of its subclasses.
45 | ``` swift
46 | class ViewController: UIViewController, KeyboardScrollable {
47 |
48 | @IBOutlet private var scrollView: UIScrollView!
49 | var keyboardScrollableScrollView: UIScrollView? {
50 | return scrollView
51 | }
52 | var keyboardWillShowObserver: NSObjectProtocol?
53 | var keyboardWillHideObserver: NSObjectProtocol?
54 |
55 | override func viewWillAppear(_ animated: Bool) {
56 | super.viewWillAppear(animated)
57 | setupKeyboardObservers()
58 | }
59 |
60 | override func viewWillDisappear(_ animated: Bool) {
61 | super.viewWillDisappear(animated)
62 | removeKeyboardObservers()
63 | }
64 | }
65 | ```
66 |
67 | ### KeyboardToolbar
68 | Create your own input accessory view for navigation between text inputs. Use the convenience methods to create back/next/done buttons or supply your own `UIBarButtonItem`s.
69 | ``` swift
70 | let keyboardToolbar = KeyboardToolbar()
71 | keyboardToolbar.addButton(type: .back, title: "Back")
72 | keyboardToolbar.addButton(type: .next, title: "Next")
73 | keyboardToolbar.addFlexibleSpace()
74 | keyboardToolbar.addSystemDoneButton()
75 | ```
76 | Check out `KeyboardToolbar` for other button adding options.
77 |
78 | ### KeyboardNavigator - when using a KeyboardToolbar
79 | Create a `KeyboardToolbar`, configuring it with back/next/done buttons as appropriate. Then, create a `KeyboardNavigator`, passing in your text inputs and toolbar. The order of the text inputs determines the navigation order for traversing from one to the next. Optionally, implement `KeyboardNavigatorDelegate` to receive call backs when tapping "Back", "Next", and "Done" in your `KeyboardToolbar`.
80 | ``` swift
81 | class ViewController: UIViewController {
82 |
83 | @IBOutlet private var textInput1: UITextField!
84 | @IBOutlet private var textInput2: UITextView!
85 | private var keyboardNavigator: KeyboardNavigator?
86 |
87 | override func viewDidLoad() {
88 | super.viewDidLoad()
89 |
90 | let keyboardToolbar = KeyboardToolbar()
91 | keyboardNavigator = KeyboardNavigator(textInputs: [textInput1, textInput2], keyboardToolbar: keyboardToolbar)
92 | keyboardNavigator?.delegate = self
93 | }
94 | }
95 |
96 | extension ViewController: KeyboardNavigatorDelegate {
97 |
98 | func keyboardNavigatorDidTapBack(_ navigator: KeyboardNavigator) {
99 | // Your code here
100 | }
101 |
102 | func keyboardNavigatorDidTapNext(_ navigator: KeyboardNavigator) {
103 | // Your code here
104 | }
105 |
106 | func keyboardNavigatorDidTapDone(_ navigator: KeyboardNavigator) {
107 | // Your code here
108 | }
109 | }
110 | ```
111 |
112 | ### KeyboardNavigator - when using the keyboard's "Return" key
113 | Create a `KeyboardNavigator`, passing in your text inputs and setting the `returnKeyNavigationEnabled` parameter to `true`. The order of the text fields determines the navigation order for traversing from one text input to the next. It's important to note that the use of the `KeyboardToolbar` and the keyboard's "Return" keys are not mutually exclusive. **You can have a `KeyboardNavigator` use both a `KeyboardToolbar` and the keyboard's "Return" keys.**
114 | ``` swift
115 | class ViewController: UIViewController {
116 |
117 | @IBOutlet private var textInput1: UITextField!
118 | @IBOutlet private var textInput2: UITextField!
119 | private var keyboardNavigator: KeyboardNavigator?
120 |
121 | override func viewDidLoad() {
122 | super.viewDidLoad()
123 |
124 | keyboardNavigator = KeyboardNavigator(textInputs: [textInput1, textInput2], returnKeyNavigationEnabled: true)
125 | }
126 | }
127 | ```
128 |
129 | ### KeyboardAutoNavigator - when using a KeyboardToolbar
130 | Create a `KeyboardToolbar`, configuring it with back/next/done buttons as appropriate. Then, create a `KeyboardAutoNavigator`, passing in your toolbar. The position of the text inputs determines the navigation order for traversing from one to the next. Optionally, implement `KeyboardAutoNavigatorDelegate` to receive call backs when tapping "Back", "Next", and "Done" in your `KeyboardToolbar`.
131 |
132 | ``` swift
133 | class ViewController: UIViewController {
134 |
135 | @IBOutlet private var textInput1: UITextField!
136 | @IBOutlet private var textInput2: UITextView!
137 | private var keyboardNavigator: KeyboardAutoNavigator?
138 |
139 | override func viewDidLoad() {
140 | super.viewDidLoad()
141 |
142 | let keyboardToolbar = KeyboardToolbar(frame: CGRect(x: 0, y: 0, width: view.bounds.width, height: 44.0))
143 | keyboardNavigator = KeyboardAutoNavigator(navigationContainer: scrollView, defaultToolbar: keyboardToolbar, returnKeyNavigationEnabled: true)
144 | keyboardNavigator?.delegate = self
145 | }
146 | }
147 |
148 | extension ViewController: KeyboardAutoNavigatorDelegate {
149 | func keyboardAutoNavigatorDidTapBack(_ navigator: KeyboardAutoNavigator) {
150 | // Your code here
151 | }
152 |
153 | func keyboardAutoNavigatorDidTapNext(_ navigator: KeyboardAutoNavigator) {
154 | // Your code here
155 | }
156 |
157 | func keyboardAutoNavigatorDidTapDone(_ navigator: KeyboardAutoNavigator) {
158 | // Your code here
159 | }
160 | }
161 | ```
162 |
163 | ## Example
164 |
165 | To run the example project, clone the repo, and run `pod install` from the Example directory first.
166 |
167 | ## Requirements
168 |
169 | * iOS 9.0+
170 | * Swift 5.0
171 |
172 | ## Installation
173 |
174 | ### Swift Package Manager
175 |
176 | ```swift
177 | dependencies: [
178 | .package(url: "https://github.com/BottleRocketStudios/iOS-KeyboardSupport.git", from: "2.1.1")
179 | ]
180 | ```
181 |
182 | ### Cocoapods
183 |
184 | KeyboardSupport is available through [CocoaPods](http://cocoapods.org). To install
185 | it, simply add the following line to your Podfile:
186 |
187 | ```ruby
188 | pod 'KeyboardSupport'
189 | ```
190 |
191 | ### Carthage
192 |
193 | Add the following to your [Cartfile](https://github.com/Carthage/Carthage/blob/master/Documentation/Artifacts.md#cartfile):
194 |
195 | ```
196 | github "BottleRocketStudios/iOS-KeyboardSupport"
197 | ```
198 |
199 | Run `carthage update` and follow the steps as described in Carthage's [README](https://github.com/Carthage/Carthage#adding-frameworks-to-an-application).
200 |
201 | ## Author
202 |
203 | [Bottle Rocket Studios](https://www.bottlerocketstudios.com/)
204 |
205 | ## License
206 |
207 | KeyboardSupport is available under the Apache 2.0 license. See the LICENSE.txt file for more info.
208 |
209 | ## Contributing
210 |
211 | See the [CONTRIBUTING] document. Thank you, [contributors]!
212 |
213 | [CONTRIBUTING]: CONTRIBUTING.md
214 | [contributors]: https://github.com/BottleRocketStudios/iOS-KeyboardSupport/graphs/contributors
215 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
179 | APPENDIX: How to apply the Apache License to your work.
180 |
181 | To apply the Apache License to your work, attach the following
182 | boilerplate notice, with the fields enclosed by brackets "[]"
183 | replaced with your own identifying information. (Don't include
184 | the brackets!) The text should be enclosed in the appropriate
185 | comment syntax for the file format. We also recommend that a
186 | file or class name and description of purpose be included on the
187 | same "printed page" as the copyright notice for easier
188 | identification within third-party archives.
189 |
190 | Copyright 2015 Bottle Rocket LLC
191 |
192 | Licensed under the Apache License, Version 2.0 (the "License");
193 | you may not use this file except in compliance with the License.
194 | You may obtain a copy of the License at
195 |
196 | http://www.apache.org/licenses/LICENSE-2.0
197 |
198 | Unless required by applicable law or agreed to in writing, software
199 | distributed under the License is distributed on an "AS IS" BASIS,
200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 | See the License for the specific language governing permissions and
202 | limitations under the License.
203 |
--------------------------------------------------------------------------------
/Sources/KeyboardScrollable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeyboardScrollable.swift
3 | // KeyboardSupport
4 | //
5 | // Copyright © 2019 Bottle Rocket. All rights reserved.
6 | //
7 |
8 | import UIKit
9 |
10 | /// KeyboardScrollable will ask a UITextInputView that conforms to this protocol for preferred distance between the field and the keyboard.
11 | /// It will be used if it is non-nil and greater than the KeyboardScrollable's `minimumPaddingAroundInput`.
12 | public protocol KeyboardPaddingProviding {
13 | var inputPadding: UIEdgeInsets { get }
14 | }
15 |
16 | // MARK: - KeyboardInfo
17 |
18 | /// Stores info about the keyboard.
19 | public struct KeyboardInfo {
20 | public let initialFrame: CGRect
21 | public let finalFrame: CGRect
22 | public let animationDuration: TimeInterval
23 | public let animationCurve: UInt
24 |
25 | public init?(notification: Notification) {
26 | #if swift(>=4.2)
27 | guard let userInfo = notification.userInfo,
28 | let initialKeyboardFrame = userInfo[UIResponder.keyboardFrameBeginUserInfoKey] as? CGRect,
29 | let finalKeyboardFrame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect,
30 | let duration = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? TimeInterval,
31 | let curve = userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as? UInt else {
32 | return nil
33 | }
34 | #else
35 | guard let userInfo = notification.userInfo,
36 | let initialKeyboardFrame = userInfo[UIKeyboardFrameBeginUserInfoKey] as? CGRect,
37 | let finalKeyboardFrame = userInfo[UIKeyboardFrameEndUserInfoKey] as? CGRect,
38 | let duration = userInfo[UIKeyboardAnimationDurationUserInfoKey] as? TimeInterval,
39 | let curve = userInfo[UIKeyboardAnimationCurveUserInfoKey] as? UInt else {
40 | return nil
41 | }
42 | #endif
43 |
44 | initialFrame = initialKeyboardFrame
45 | finalFrame = finalKeyboardFrame
46 | animationDuration = duration
47 | animationCurve = curve
48 | }
49 |
50 | public var isMoving: Bool {
51 | return initialFrame.origin != finalFrame.origin
52 | }
53 | }
54 |
55 | // MARK: - KeyboardScrollable
56 |
57 | /// Enables scrolling views to the first responder when a keyboard is shown. Must be used with a UIScrollView or one of its subclasses.
58 | public protocol KeyboardScrollable: AnyObject {
59 | var minimumPaddingAroundInput: UIEdgeInsets { get }
60 |
61 | var keyboardScrollableScrollView: UIScrollView? { get }
62 | var keyboardWillShowObserver: NSObjectProtocol? { get set }
63 | var keyboardWillHideObserver: NSObjectProtocol? { get set }
64 |
65 | var preservesContentInsetWhenKeyboardVisible: Bool { get }
66 |
67 | /// Must be called during screen appearance ('viewWillAppear') to allow for keyboard notification observers to be registered.
68 | func setupKeyboardObservers()
69 |
70 | /// Must be called during screen disappearance ('viewWillDisappear') to allow for keyboard notification observers to be unregistered.
71 | func removeKeyboardObservers()
72 |
73 | /// Called when the keyboard is showing.
74 | func keyboardWillShow(keyboardInfo: KeyboardInfo)
75 |
76 | /// Called when the keyboard is hiding.
77 | func keyboardWillHide(keyboardInfo: KeyboardInfo)
78 | }
79 |
80 | public extension KeyboardScrollable where Self: UIViewController {
81 |
82 | // MARK: KeyboardScrollable Conformance
83 | var minimumPaddingAroundInput: UIEdgeInsets {
84 | return .zero
85 | }
86 |
87 | var preservesContentInsetWhenKeyboardVisible: Bool { return true }
88 |
89 | private var isPopover: Bool { return popoverPresentationController != nil }
90 |
91 | func setupKeyboardObservers() {
92 | let keyboardWillShowNotificationName: Notification.Name = {
93 | #if swift(>=4.2)
94 | return UIResponder.keyboardWillShowNotification
95 | #else
96 | return .UIKeyboardWillShow
97 | #endif
98 | }()
99 | let keyboardWillHideNotificationName: Notification.Name = {
100 | #if swift(>=4.2)
101 | return UIResponder.keyboardWillHideNotification
102 | #else
103 | return .UIKeyboardWillHide
104 | #endif
105 | }()
106 |
107 | keyboardWillShowObserver = NotificationCenter.default.addObserver(forName: keyboardWillShowNotificationName, object: nil, queue: OperationQueue.main, using: { [weak self] (notification) in
108 | guard (self?.isPopover ?? false) == false else { return }
109 | guard let keyboardInfo = KeyboardInfo(notification: notification), let activeField = self?.view.activeFirstResponder() else { return }
110 | if self?.keyboardScrollableScrollView?.originalContentInset == nil {
111 | self?.keyboardScrollableScrollView?.originalContentInset = self?.keyboardScrollableScrollView?.contentInset
112 | }
113 |
114 | self?.adjustViewForKeyboardAppearance(with: keyboardInfo, firstResponder: activeField)
115 | self?.keyboardWillShow(keyboardInfo: keyboardInfo)
116 | })
117 | keyboardWillHideObserver = NotificationCenter.default.addObserver(forName: keyboardWillHideNotificationName, object: nil, queue: OperationQueue.main, using: { [weak self] (notification) in
118 | guard (self?.isPopover ?? false) == false else { return }
119 | guard let keyboardInfo = KeyboardInfo(notification: notification) else { return }
120 | self?.resetViewForKeyboardDisappearance(with: keyboardInfo)
121 | self?.keyboardWillHide(keyboardInfo: keyboardInfo)
122 | })
123 | }
124 |
125 | func removeKeyboardObservers() {
126 | keyboardScrollableScrollView?.originalContentInset.flatMap { keyboardScrollableScrollView?.contentInset = $0 }
127 | if let keyboardWillShowObserver = keyboardWillShowObserver {
128 | NotificationCenter.default.removeObserver(keyboardWillShowObserver)
129 | }
130 | if let keyboardWillHideObserver = keyboardWillHideObserver {
131 | NotificationCenter.default.removeObserver(keyboardWillHideObserver)
132 | }
133 | }
134 |
135 | func keyboardWillShow(keyboardInfo: KeyboardInfo) {
136 | // No-op by default. Opt-in by implementing this method in your class conforming to KeyboardScrollable.
137 | }
138 |
139 | func keyboardWillHide(keyboardInfo: KeyboardInfo) {
140 | // No-op by default. Opt-in by implementing this method in your class conforming to KeyboardScrollable.
141 | }
142 |
143 | // MARK: Private Methods
144 |
145 | private func adjustViewForKeyboardAppearance(with keyboardInfo: KeyboardInfo, firstResponder: UIView) {
146 | guard let scrollView = keyboardScrollableScrollView else { return }
147 |
148 | var mutableInset: UIEdgeInsets
149 | if preservesContentInsetWhenKeyboardVisible, let originalContentInset = scrollView.originalContentInset {
150 | mutableInset = originalContentInset
151 | } else {
152 | mutableInset = .zero
153 | }
154 |
155 | // Adjust scroll view insets for keyboard height
156 | let keyboardHeight = keyboardInfo.finalFrame.height
157 | if #available(iOS 11.0, *) {
158 | mutableInset.bottom += keyboardHeight - view.safeAreaInsets.bottom
159 | } else {
160 | mutableInset.bottom += keyboardHeight
161 | }
162 |
163 | let scrollViewConvertedFrame = view.window?.convert(scrollView.frame, from: scrollView.superview) ?? .zero
164 | let bottomGap = (view.window?.frame.height ?? 0) - scrollViewConvertedFrame.maxY
165 | mutableInset.bottom -= min(bottomGap, mutableInset.bottom)
166 |
167 | adjustScrollViewInset(mutableInset, keyboardInfo: keyboardInfo)
168 |
169 | // If active text field is hidden by keyboard, scroll so it's visible
170 | if let textView = firstResponder as? UITextView {
171 | scrollToSelectedText(for: textView, keyboardInfo: keyboardInfo)
172 | } else {
173 | let preferredPaddingAroundInput = (firstResponder as? KeyboardPaddingProviding)?.inputPadding ?? .zero
174 | scrollToRectIfNecessary(rect: firstResponder.bounds, of: firstResponder, keyboardInfo: keyboardInfo, preferredPaddingAroundInput: preferredPaddingAroundInput)
175 | }
176 | }
177 |
178 | private func scrollToSelectedText(for textView: UITextView, keyboardInfo: KeyboardInfo) {
179 | // Get the frame of the cursor/selection to improve scrolling position for UITextView's
180 | // DispatchQueue.async() is necessary because the selectedTextRange typically hasn't not been updated when UIResponder.keyboardWillShowNotification is posted
181 | DispatchQueue.main.async {
182 | guard let textRange = textView.selectedTextRange, let selectionRect = textView.selectionRects(for: textRange).first else { return }
183 | // Set an arbitrary width to the target CGRect in case the width is zero. Otherwise, scrollRectToVisible has no effect.
184 | self.scrollToRectIfNecessary(rect: selectionRect.rect.modifying(width: 1), of: textView, keyboardInfo: keyboardInfo)
185 | }
186 | }
187 |
188 | private func scrollToRectIfNecessary(rect: CGRect, of coordinateSpaceView: UIView, keyboardInfo: KeyboardInfo, preferredPaddingAroundInput: UIEdgeInsets = .zero) {
189 | guard let scrollView = keyboardScrollableScrollView else { return }
190 |
191 | let paddingAroundInput = UIEdgeInsets.max(lhs: preferredPaddingAroundInput, rhs: minimumPaddingAroundInput)
192 |
193 | // Inflate the frame being scrolled into view by the padding
194 | let paddedFrameOfFirstResponder = rect.modifying(minY: rect.minY - paddingAroundInput.top)
195 | .modifying(minX: rect.minX - paddingAroundInput.left)
196 | .modifying(height: rect.height + paddingAroundInput.top + paddingAroundInput.bottom)
197 | .modifying(width: rect.width + paddingAroundInput.left + paddingAroundInput.right)
198 |
199 | // Convert the padded rect to the scrollview coordinate space and scroll it into view
200 | let paddedFrameOfFirstResponderInScrollView = coordinateSpaceView.convert(paddedFrameOfFirstResponder, to: scrollView)
201 | UIView.animate(withDuration: keyboardInfo.animationDuration, delay: 0, options: [UIView.AnimationOptions(rawValue: keyboardInfo.animationCurve)], animations: {
202 | scrollView.scrollRectToVisible(paddedFrameOfFirstResponderInScrollView, animated: false)
203 | }, completion: nil)
204 | }
205 |
206 | private func resetViewForKeyboardDisappearance(with keyboardInfo: KeyboardInfo) {
207 | guard let scrollView = keyboardScrollableScrollView else { return }
208 | let originalContentInset = scrollView.originalContentInset ?? .zero
209 | adjustScrollViewInset(originalContentInset, keyboardInfo: keyboardInfo)
210 | }
211 |
212 | private func adjustScrollViewInset(_ inset: UIEdgeInsets, keyboardInfo: KeyboardInfo) {
213 | UIView.animate(withDuration: keyboardInfo.animationDuration, delay: 0, options: [UIView.AnimationOptions(rawValue: keyboardInfo.animationCurve)], animations: {
214 | if self.keyboardScrollableScrollView?.contentInset != inset {
215 | self.keyboardScrollableScrollView?.contentInset = inset
216 | }
217 | if self.keyboardScrollableScrollView?.scrollIndicatorInsets != inset {
218 | self.keyboardScrollableScrollView?.scrollIndicatorInsets = inset
219 | }
220 | }, completion: nil)
221 | }
222 | }
223 |
--------------------------------------------------------------------------------
/Sources/KeyboardAutoNavigator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeyboardAutoNavigator.swift
3 | // KeyboardSupport
4 | //
5 | // Copyright © 2018 Bottle Rocket. All rights reserved.
6 | //
7 |
8 | import UIKit
9 |
10 | /// Contains callbacks for `KeyboardAutoNavigator` navigation events.
11 | public protocol KeyboardAutoNavigatorDelegate: AnyObject {
12 | func keyboardAutoNavigatorDidTapBack(_ navigator: KeyboardAutoNavigator)
13 | func keyboardAutoNavigatorDidTapNext(_ navigator: KeyboardAutoNavigator)
14 | func keyboardAutoNavigatorDidTapDone(_ navigator: KeyboardAutoNavigator)
15 | }
16 |
17 | /// Handles navigating between text fields in a containing view hierarchy.
18 | open class KeyboardAutoNavigator: KeyboardNavigatorBase {
19 |
20 | /// AutoPilot is a collection of static functions that enable navigating between `UITextInputViews` in a view hierarchy
21 | public enum AutoPilot {
22 |
23 | /// Returns the "next" `UITextInputView` from the provided view within the provided container
24 | /// The next view is found in a left-to-right, top-to-bottom fashion
25 | ///
26 | /// - Parameters:
27 | /// - source: `UITextInputView` to find the next field from
28 | /// - container: Optional form container. If nil, the top-level container of the source will be determined and used.
29 | /// - Returns: The next `UITextInputView` from the source, or nil if one could not be found.
30 | public static func nextField(from source: UITextInputView, in container: UIView?) -> UITextInputView? {
31 | let fields = sortedFields(around: source, in: container)
32 |
33 | guard let currentFieldIndex = fields.firstIndex(where: { $0 == source }) else { return nil }
34 | let nextIndex = min(currentFieldIndex + 1, fields.count - 1) // Add to index or max out
35 |
36 | let nextField = fields[nextIndex]
37 | return (nextField as UIView) != (source as UIView) ? nextField : nil
38 | }
39 |
40 | /// Returns the "previous" `UITextInputView` from the provided view.
41 | /// The previous view is found in a right-to-left, bottom-to-top fashion
42 | ///
43 | /// - Parameters:
44 | /// - source: `UITextInputView` to find the previous field from
45 | /// - container: Optional form container. If nil, the top-level container of the source will be determined and used.
46 | /// - Returns: The previous `UITextInputView` from the source, or nil if one could not be found.
47 | public static func previousField(from source: UITextInputView, in container: UIView?) -> UITextInputView? {
48 | let fields = sortedFields(around: source, in: container)
49 |
50 | guard let currentFieldIndex = fields.firstIndex(where: { $0 == source }) else { return nil }
51 | let previousIndex = max(currentFieldIndex - 1, 0) // Subtract from index, or bottom out at zero
52 |
53 | let previousField = fields[previousIndex]
54 | return (previousField as UIView) != (source as UIView) ? previousField : nil
55 | }
56 |
57 | /// Indicates if a following `UITextInputView` from the provided view exists.
58 | ///
59 | /// - Parameters:
60 | /// - source: `UITextInputView` to find the next field from
61 | /// - container: Optional form container. If nil, the top-level container of the source will be determined and used.
62 | /// - Returns: True if there is a next field. Otherwise false.
63 | public static func hasNextField(from source: UITextInputView, in container: UIView?) -> Bool {
64 | return nextField(from: source, in: container) != nil
65 | }
66 |
67 | /// Indicates if a preceding `UITextInputView` from the provided view exists.
68 | ///
69 | /// - Parameters:
70 | /// - source: `UITextInputView` to find the previous field from
71 | /// - container: Optional form container. If nil, the top-level container of the source will be determined and used.
72 | /// - Returns: True if there is a previous field. Otherwise false.
73 | public static func hasPreviousField(from source: UITextInputView, in container: UIView?) -> Bool {
74 | return previousField(from: source, in: container) != nil
75 | }
76 |
77 | private static func sortedFields(around source: UITextInputView, in container: UIView?) -> [UITextInputView] {
78 | let container = container ?? source.topLevelContainer
79 | return container.textInputViews.sortedByPosition(in: container)
80 | }
81 | }
82 |
83 | // MARK: - Properties
84 | private var currentTextInputView: UITextInputView? {
85 | willSet {
86 | guard let currentTextField = currentTextInputView else { return }
87 |
88 | if let toolbar = currentTextField.inputAccessoryView as? KeyboardToolbar {
89 | toolbar.keyboardAccessoryDelegate = nil
90 | }
91 |
92 | if let control = currentTextField as? UIControl {
93 | control.removeTarget(self, action: #selector(textFieldEditingDidEndOnExit(_:)), for: UIControl.Event.editingDidEndOnExit)
94 | }
95 | }
96 | }
97 |
98 | /// Containing view of text inputs that can be navigated by the AutoNavigator instance
99 | private var containerView: UIView
100 |
101 | /// Delegate that will be informed of navigation tap events
102 | weak open var delegate: KeyboardAutoNavigatorDelegate?
103 |
104 | // MARK: - Init
105 | /// Initializes a `KeyboardAutoNavigator`
106 | ///
107 | /// - Parameters:
108 | /// - containerView: Containing view of text inputs that can be navigated by the AutoNavigator instance
109 | /// - defaultToolbar: Default toolbar to be populated on a textInput when editing begins. If that input implements `KeyboardToolbarProviding` that input's toolbar will be used instead.
110 | /// - returnKeyNavigationEnabled: If enabled, the auto navigator will add itself as a target to a `UITextField`'s textFieldEditingDidEndOnExit action and advance to the next field when the return key is tapped.
111 | public init(containerView: UIView, defaultToolbar: NavigatingKeyboardAccessoryView? = nil, returnKeyNavigationEnabled: Bool = true) {
112 | self.containerView = containerView
113 |
114 | super.init(keyboardToolbar: defaultToolbar, returnKeyNavigationEnabled: returnKeyNavigationEnabled)
115 |
116 | NotificationCenter.default.addObserver(self, selector: #selector(textEditingDidBegin(_:)), name: UITextField.textDidBeginEditingNotification, object: nil)
117 | NotificationCenter.default.addObserver(self, selector: #selector(textEditingDidBegin(_:)), name: UITextView.textDidBeginEditingNotification, object: nil)
118 | }
119 |
120 | deinit {
121 | NotificationCenter.default.removeObserver(self, name: UITextField.textDidBeginEditingNotification, object: nil)
122 | NotificationCenter.default.removeObserver(self, name: UITextView.textDidBeginEditingNotification, object: nil)
123 | }
124 |
125 | public func refreshCurrentToolbarButtonStates() {
126 | guard let currentTextInput = currentTextInputView,
127 | let currentToolbar = currentTextInputView?.inputAccessoryView as? NavigatingKeyboardAccessory else { return }
128 |
129 | let hasNext = AutoPilot.hasNextField(from: currentTextInput, in: containerView)
130 | let hasPrevious = AutoPilot.hasPreviousField(from: currentTextInput, in: containerView)
131 |
132 | if !hasNext && !hasPrevious {
133 | currentToolbar.setNextAndBackButtonsHidden(true)
134 | } else {
135 | currentToolbar.setNextAndBackButtonsHidden(false)
136 |
137 | currentToolbar.backButton?.isEnabled = hasPrevious
138 | currentToolbar.nextButton?.isEnabled = hasNext
139 | }
140 | }
141 | }
142 |
143 | // MARK: - UI Event Handlers
144 | extension KeyboardAutoNavigator {
145 | @objc
146 | private func textEditingDidBegin(_ notification: Notification) {
147 | guard let inputView = notification.object as? UITextInputView,
148 | inputView.isDescendant(of: containerView) else { return }
149 | currentTextInputView = inputView
150 |
151 | if returnKeyNavigationEnabled, let controlInput = currentTextInputView as? UIControl {
152 | controlInput.addTarget(self, action: #selector(textFieldEditingDidEndOnExit(_:)), for: UIControl.Event.editingDidEndOnExit)
153 | }
154 |
155 | applyToolbarToTextInput(inputView)
156 |
157 | // There's a chance that the TextInput gaining first responder will trigger a containing scroll view to scroll new textfields into view.
158 | // We want to try to refresh our toolbar buttons after the scrollview has settled so those new views are taken into consideration. Using
159 | // async here is a "best effort" approach. If your app has an opportunity to call this method at a more concrete time, such as in
160 | // ScrollViewDidEndDragging, or ScrollViewDidEndDecelerating, do so for the best results.
161 | DispatchQueue.main.async {
162 | self.refreshCurrentToolbarButtonStates()
163 | }
164 | }
165 |
166 | @objc
167 | private func textFieldEditingDidEndOnExit(_ sender: UITextInputView) {
168 | if returnKeyNavigationEnabled {
169 | AutoPilot.nextField(from: sender, in: containerView)?.becomeFirstResponder()
170 | }
171 | }
172 | }
173 |
174 | // MARK: - Helpers
175 | extension KeyboardAutoNavigator {
176 | private func applyToolbarToTextInput(_ textInput: UITextInputView) {
177 | let toolbar = (textInput as? KeyboardToolbarProviding)?.keyboardToolbar ?? keyboardToolbar
178 | toolbar?.keyboardAccessoryDelegate = self
179 |
180 | if let textInput = textInput as? UITextField {
181 | textInput.inputAccessoryView = toolbar
182 | } else if let textInput = textInput as? UITextView {
183 | textInput.inputAccessoryView = toolbar
184 |
185 | // UITextView does not display its toolbar if it's set via a textDidBeginEditingNotification handler. Force a reload of the input views to make it display.
186 | textInput.reloadInputViews()
187 | }
188 | }
189 | }
190 |
191 | // MARK: - KeyboardAccessoryDelegate
192 | extension KeyboardAutoNavigator: KeyboardAccessoryDelegate {
193 | private func didTapBack() {
194 | defer {
195 | delegate?.keyboardAutoNavigatorDidTapBack(self)
196 | }
197 |
198 | guard let currentTextField = currentTextInputView else { return }
199 | AutoPilot.previousField(from: currentTextField, in: containerView)?.becomeFirstResponder()
200 | }
201 |
202 | private func didTapNext() {
203 | defer {
204 | delegate?.keyboardAutoNavigatorDidTapNext(self)
205 | }
206 |
207 | guard let currentTextField = currentTextInputView else { return }
208 | AutoPilot.nextField(from: currentTextField, in: containerView)?.becomeFirstResponder()
209 | }
210 |
211 | private func didTapDone() {
212 | currentTextInputView?.resignFirstResponder()
213 | delegate?.keyboardAutoNavigatorDidTapDone(self)
214 | }
215 |
216 | public func keyboardAccessoryDidTapBack(_ inputAccessory: UIView) {
217 | didTapBack()
218 | }
219 |
220 | public func keyboardAccessoryDidTapNext(_ inputAccessory: UIView) {
221 | didTapNext()
222 | }
223 |
224 | public func keyboardAccessoryDidTapDone(_ inputAccessory: UIView) {
225 | didTapDone()
226 | }
227 | }
228 |
--------------------------------------------------------------------------------
/Example/Example.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 50;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 0E3F8D52278F555B00C9E69A /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3F8D4A278F555B00C9E69A /* ViewController.swift */; };
11 | 0E3F8D53278F555B00C9E69A /* AutoNavigatorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3F8D4B278F555B00C9E69A /* AutoNavigatorViewController.swift */; };
12 | 0E3F8D54278F555B00C9E69A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0E3F8D4C278F555B00C9E69A /* Assets.xcassets */; };
13 | 0E3F8D55278F555B00C9E69A /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0E3F8D4D278F555B00C9E69A /* LaunchScreen.storyboard */; };
14 | 0E3F8D56278F555B00C9E69A /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0E3F8D4F278F555B00C9E69A /* Main.storyboard */; };
15 | 0E3F8D57278F555B00C9E69A /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3F8D50278F555B00C9E69A /* AppDelegate.swift */; };
16 | 0E3F8D58278F555B00C9E69A /* SingleFieldAutoNavViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3F8D51278F555B00C9E69A /* SingleFieldAutoNavViewController.swift */; };
17 | 0E3F9027278F8F0900C9E69A /* KeyboardSupport.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0E3F9026278F8F0900C9E69A /* KeyboardSupport.framework */; };
18 | 0E3F9029278F8F1400C9E69A /* KeyboardSupport.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 0E3F9026278F8F0900C9E69A /* KeyboardSupport.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
19 | /* End PBXBuildFile section */
20 |
21 | /* Begin PBXCopyFilesBuildPhase section */
22 | 0E3F9028278F8F0C00C9E69A /* CopyFiles */ = {
23 | isa = PBXCopyFilesBuildPhase;
24 | buildActionMask = 2147483647;
25 | dstPath = "";
26 | dstSubfolderSpec = 10;
27 | files = (
28 | 0E3F9029278F8F1400C9E69A /* KeyboardSupport.framework in CopyFiles */,
29 | );
30 | runOnlyForDeploymentPostprocessing = 0;
31 | };
32 | 626DEACF20BC9D360036D5A6 /* Embed Frameworks */ = {
33 | isa = PBXCopyFilesBuildPhase;
34 | buildActionMask = 2147483647;
35 | dstPath = "";
36 | dstSubfolderSpec = 10;
37 | files = (
38 | );
39 | name = "Embed Frameworks";
40 | runOnlyForDeploymentPostprocessing = 0;
41 | };
42 | /* End PBXCopyFilesBuildPhase section */
43 |
44 | /* Begin PBXFileReference section */
45 | 0E3F8D4A278F555B00C9E69A /* ViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; };
46 | 0E3F8D4B278F555B00C9E69A /* AutoNavigatorViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AutoNavigatorViewController.swift; sourceTree = ""; };
47 | 0E3F8D4C278F555B00C9E69A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
48 | 0E3F8D4E278F555B00C9E69A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
49 | 0E3F8D4F278F555B00C9E69A /* Main.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Main.storyboard; sourceTree = ""; };
50 | 0E3F8D50278F555B00C9E69A /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
51 | 0E3F8D51278F555B00C9E69A /* SingleFieldAutoNavViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SingleFieldAutoNavViewController.swift; sourceTree = ""; };
52 | 0E3F8D59278F556700C9E69A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
53 | 0E3F9026278F8F0900C9E69A /* KeyboardSupport.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = KeyboardSupport.framework; sourceTree = BUILT_PRODUCTS_DIR; };
54 | 626DEA8620BC94ED0036D5A6 /* iOS Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "iOS Example.app"; sourceTree = BUILT_PRODUCTS_DIR; };
55 | /* End PBXFileReference section */
56 |
57 | /* Begin PBXFrameworksBuildPhase section */
58 | 626DEA8320BC94ED0036D5A6 /* Frameworks */ = {
59 | isa = PBXFrameworksBuildPhase;
60 | buildActionMask = 2147483647;
61 | files = (
62 | 0E3F9027278F8F0900C9E69A /* KeyboardSupport.framework in Frameworks */,
63 | );
64 | runOnlyForDeploymentPostprocessing = 0;
65 | };
66 | /* End PBXFrameworksBuildPhase section */
67 |
68 | /* Begin PBXGroup section */
69 | 0E3F8D49278F555B00C9E69A /* Sources */ = {
70 | isa = PBXGroup;
71 | children = (
72 | 0E3F8D4A278F555B00C9E69A /* ViewController.swift */,
73 | 0E3F8D4B278F555B00C9E69A /* AutoNavigatorViewController.swift */,
74 | 0E3F8D4C278F555B00C9E69A /* Assets.xcassets */,
75 | 0E3F8D4D278F555B00C9E69A /* LaunchScreen.storyboard */,
76 | 0E3F8D4F278F555B00C9E69A /* Main.storyboard */,
77 | 0E3F8D50278F555B00C9E69A /* AppDelegate.swift */,
78 | 0E3F8D51278F555B00C9E69A /* SingleFieldAutoNavViewController.swift */,
79 | );
80 | path = Sources;
81 | sourceTree = "";
82 | };
83 | 0E3F9025278F8F0900C9E69A /* Frameworks */ = {
84 | isa = PBXGroup;
85 | children = (
86 | 0E3F9026278F8F0900C9E69A /* KeyboardSupport.framework */,
87 | );
88 | name = Frameworks;
89 | sourceTree = "";
90 | };
91 | 626DEA4620BC94360036D5A6 = {
92 | isa = PBXGroup;
93 | children = (
94 | 0E3F8D59278F556700C9E69A /* Info.plist */,
95 | 0E3F8D49278F555B00C9E69A /* Sources */,
96 | 626DEA5020BC94360036D5A6 /* Products */,
97 | 0E3F9025278F8F0900C9E69A /* Frameworks */,
98 | );
99 | sourceTree = "";
100 | };
101 | 626DEA5020BC94360036D5A6 /* Products */ = {
102 | isa = PBXGroup;
103 | children = (
104 | 626DEA8620BC94ED0036D5A6 /* iOS Example.app */,
105 | );
106 | name = Products;
107 | sourceTree = "";
108 | };
109 | /* End PBXGroup section */
110 |
111 | /* Begin PBXNativeTarget section */
112 | 626DEA8520BC94ED0036D5A6 /* iOS Example */ = {
113 | isa = PBXNativeTarget;
114 | buildConfigurationList = 626DEAA020BC94EE0036D5A6 /* Build configuration list for PBXNativeTarget "iOS Example" */;
115 | buildPhases = (
116 | 626DEA8220BC94ED0036D5A6 /* Sources */,
117 | 626DEA8320BC94ED0036D5A6 /* Frameworks */,
118 | 626DEA8420BC94ED0036D5A6 /* Resources */,
119 | 626DEACF20BC9D360036D5A6 /* Embed Frameworks */,
120 | 0E3F9028278F8F0C00C9E69A /* CopyFiles */,
121 | );
122 | buildRules = (
123 | );
124 | dependencies = (
125 | );
126 | name = "iOS Example";
127 | productName = "KeyboardSupport-iOSExample";
128 | productReference = 626DEA8620BC94ED0036D5A6 /* iOS Example.app */;
129 | productType = "com.apple.product-type.application";
130 | };
131 | /* End PBXNativeTarget section */
132 |
133 | /* Begin PBXProject section */
134 | 626DEA4720BC94360036D5A6 /* Project object */ = {
135 | isa = PBXProject;
136 | attributes = {
137 | LastSwiftUpdateCheck = 0930;
138 | LastUpgradeCheck = 1310;
139 | ORGANIZATIONNAME = "Bottle Rocket Studios";
140 | TargetAttributes = {
141 | 626DEA8520BC94ED0036D5A6 = {
142 | CreatedOnToolsVersion = 9.3.1;
143 | LastSwiftMigration = 1020;
144 | };
145 | };
146 | };
147 | buildConfigurationList = 626DEA4A20BC94360036D5A6 /* Build configuration list for PBXProject "Example" */;
148 | compatibilityVersion = "Xcode 9.3";
149 | developmentRegion = en;
150 | hasScannedForEncodings = 0;
151 | knownRegions = (
152 | en,
153 | Base,
154 | );
155 | mainGroup = 626DEA4620BC94360036D5A6;
156 | productRefGroup = 626DEA5020BC94360036D5A6 /* Products */;
157 | projectDirPath = "";
158 | projectRoot = "";
159 | targets = (
160 | 626DEA8520BC94ED0036D5A6 /* iOS Example */,
161 | );
162 | };
163 | /* End PBXProject section */
164 |
165 | /* Begin PBXResourcesBuildPhase section */
166 | 626DEA8420BC94ED0036D5A6 /* Resources */ = {
167 | isa = PBXResourcesBuildPhase;
168 | buildActionMask = 2147483647;
169 | files = (
170 | 0E3F8D56278F555B00C9E69A /* Main.storyboard in Resources */,
171 | 0E3F8D54278F555B00C9E69A /* Assets.xcassets in Resources */,
172 | 0E3F8D55278F555B00C9E69A /* LaunchScreen.storyboard in Resources */,
173 | );
174 | runOnlyForDeploymentPostprocessing = 0;
175 | };
176 | /* End PBXResourcesBuildPhase section */
177 |
178 | /* Begin PBXSourcesBuildPhase section */
179 | 626DEA8220BC94ED0036D5A6 /* Sources */ = {
180 | isa = PBXSourcesBuildPhase;
181 | buildActionMask = 2147483647;
182 | files = (
183 | 0E3F8D58278F555B00C9E69A /* SingleFieldAutoNavViewController.swift in Sources */,
184 | 0E3F8D57278F555B00C9E69A /* AppDelegate.swift in Sources */,
185 | 0E3F8D52278F555B00C9E69A /* ViewController.swift in Sources */,
186 | 0E3F8D53278F555B00C9E69A /* AutoNavigatorViewController.swift in Sources */,
187 | );
188 | runOnlyForDeploymentPostprocessing = 0;
189 | };
190 | /* End PBXSourcesBuildPhase section */
191 |
192 | /* Begin PBXVariantGroup section */
193 | 0E3F8D4D278F555B00C9E69A /* LaunchScreen.storyboard */ = {
194 | isa = PBXVariantGroup;
195 | children = (
196 | 0E3F8D4E278F555B00C9E69A /* Base */,
197 | );
198 | name = LaunchScreen.storyboard;
199 | sourceTree = "";
200 | };
201 | /* End PBXVariantGroup section */
202 |
203 | /* Begin XCBuildConfiguration section */
204 | 626DEA5F20BC94380036D5A6 /* Debug */ = {
205 | isa = XCBuildConfiguration;
206 | buildSettings = {
207 | ALWAYS_SEARCH_USER_PATHS = NO;
208 | CLANG_ANALYZER_NONNULL = YES;
209 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
210 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
211 | CLANG_CXX_LIBRARY = "libc++";
212 | CLANG_ENABLE_MODULES = YES;
213 | CLANG_ENABLE_OBJC_ARC = YES;
214 | CLANG_ENABLE_OBJC_WEAK = YES;
215 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
216 | CLANG_WARN_BOOL_CONVERSION = YES;
217 | CLANG_WARN_COMMA = YES;
218 | CLANG_WARN_CONSTANT_CONVERSION = YES;
219 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
220 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
221 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
222 | CLANG_WARN_EMPTY_BODY = YES;
223 | CLANG_WARN_ENUM_CONVERSION = YES;
224 | CLANG_WARN_INFINITE_RECURSION = YES;
225 | CLANG_WARN_INT_CONVERSION = YES;
226 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
227 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
228 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
229 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
230 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
231 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
232 | CLANG_WARN_STRICT_PROTOTYPES = YES;
233 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
234 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
235 | CLANG_WARN_UNREACHABLE_CODE = YES;
236 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
237 | CODE_SIGN_IDENTITY = "iPhone Developer";
238 | COPY_PHASE_STRIP = NO;
239 | DEBUG_INFORMATION_FORMAT = dwarf;
240 | ENABLE_STRICT_OBJC_MSGSEND = YES;
241 | ENABLE_TESTABILITY = YES;
242 | GCC_C_LANGUAGE_STANDARD = gnu11;
243 | GCC_DYNAMIC_NO_PIC = NO;
244 | GCC_NO_COMMON_BLOCKS = YES;
245 | GCC_OPTIMIZATION_LEVEL = 0;
246 | GCC_PREPROCESSOR_DEFINITIONS = (
247 | "DEBUG=1",
248 | "$(inherited)",
249 | );
250 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
251 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
252 | GCC_WARN_UNDECLARED_SELECTOR = YES;
253 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
254 | GCC_WARN_UNUSED_FUNCTION = YES;
255 | GCC_WARN_UNUSED_VARIABLE = YES;
256 | IPHONEOS_DEPLOYMENT_TARGET = 12.0;
257 | MARKETING_VERSION = 2.1.2;
258 | MTL_ENABLE_DEBUG_INFO = YES;
259 | ONLY_ACTIVE_ARCH = YES;
260 | SDKROOT = iphoneos;
261 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
262 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
263 | SWIFT_VERSION = 5.0;
264 | };
265 | name = Debug;
266 | };
267 | 626DEA6020BC94380036D5A6 /* Release */ = {
268 | isa = XCBuildConfiguration;
269 | buildSettings = {
270 | ALWAYS_SEARCH_USER_PATHS = NO;
271 | CLANG_ANALYZER_NONNULL = YES;
272 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
273 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
274 | CLANG_CXX_LIBRARY = "libc++";
275 | CLANG_ENABLE_MODULES = YES;
276 | CLANG_ENABLE_OBJC_ARC = YES;
277 | CLANG_ENABLE_OBJC_WEAK = YES;
278 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
279 | CLANG_WARN_BOOL_CONVERSION = YES;
280 | CLANG_WARN_COMMA = YES;
281 | CLANG_WARN_CONSTANT_CONVERSION = YES;
282 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
283 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
284 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
285 | CLANG_WARN_EMPTY_BODY = YES;
286 | CLANG_WARN_ENUM_CONVERSION = YES;
287 | CLANG_WARN_INFINITE_RECURSION = YES;
288 | CLANG_WARN_INT_CONVERSION = YES;
289 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
290 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
291 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
292 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
293 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
294 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
295 | CLANG_WARN_STRICT_PROTOTYPES = YES;
296 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
297 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
298 | CLANG_WARN_UNREACHABLE_CODE = YES;
299 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
300 | CODE_SIGN_IDENTITY = "iPhone Developer";
301 | COPY_PHASE_STRIP = NO;
302 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
303 | ENABLE_NS_ASSERTIONS = NO;
304 | ENABLE_STRICT_OBJC_MSGSEND = YES;
305 | GCC_C_LANGUAGE_STANDARD = gnu11;
306 | GCC_NO_COMMON_BLOCKS = YES;
307 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
308 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
309 | GCC_WARN_UNDECLARED_SELECTOR = YES;
310 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
311 | GCC_WARN_UNUSED_FUNCTION = YES;
312 | GCC_WARN_UNUSED_VARIABLE = YES;
313 | IPHONEOS_DEPLOYMENT_TARGET = 12.0;
314 | MARKETING_VERSION = 2.1.2;
315 | MTL_ENABLE_DEBUG_INFO = NO;
316 | SDKROOT = iphoneos;
317 | SWIFT_COMPILATION_MODE = wholemodule;
318 | SWIFT_OPTIMIZATION_LEVEL = "-O";
319 | SWIFT_VERSION = 5.0;
320 | VALIDATE_PRODUCT = YES;
321 | };
322 | name = Release;
323 | };
324 | 626DEAA120BC94EE0036D5A6 /* Debug */ = {
325 | isa = XCBuildConfiguration;
326 | buildSettings = {
327 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
328 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
329 | CODE_SIGN_STYLE = Automatic;
330 | DEVELOPMENT_TEAM = AJSV2L9F8Q;
331 | INFOPLIST_FILE = "$(SRCROOT)/Info.plist";
332 | IPHONEOS_DEPLOYMENT_TARGET = 12.0;
333 | LD_RUNPATH_SEARCH_PATHS = (
334 | "$(inherited)",
335 | "@executable_path/Frameworks",
336 | );
337 | MARKETING_VERSION = 1.0.0;
338 | PRODUCT_BUNDLE_IDENTIFIER = "com.bottlerocketstudios.keyboardsupport.iOS-Example";
339 | PRODUCT_NAME = "$(TARGET_NAME)";
340 | SWIFT_VERSION = 5.0;
341 | TARGETED_DEVICE_FAMILY = "1,2";
342 | };
343 | name = Debug;
344 | };
345 | 626DEAA220BC94EE0036D5A6 /* Release */ = {
346 | isa = XCBuildConfiguration;
347 | buildSettings = {
348 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
349 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
350 | CODE_SIGN_STYLE = Automatic;
351 | DEVELOPMENT_TEAM = AJSV2L9F8Q;
352 | INFOPLIST_FILE = "$(SRCROOT)/Info.plist";
353 | IPHONEOS_DEPLOYMENT_TARGET = 12.0;
354 | LD_RUNPATH_SEARCH_PATHS = (
355 | "$(inherited)",
356 | "@executable_path/Frameworks",
357 | );
358 | MARKETING_VERSION = 1.0.0;
359 | PRODUCT_BUNDLE_IDENTIFIER = "com.bottlerocketstudios.keyboardsupport.iOS-Example";
360 | PRODUCT_NAME = "$(TARGET_NAME)";
361 | SWIFT_VERSION = 5.0;
362 | TARGETED_DEVICE_FAMILY = "1,2";
363 | };
364 | name = Release;
365 | };
366 | /* End XCBuildConfiguration section */
367 |
368 | /* Begin XCConfigurationList section */
369 | 626DEA4A20BC94360036D5A6 /* Build configuration list for PBXProject "Example" */ = {
370 | isa = XCConfigurationList;
371 | buildConfigurations = (
372 | 626DEA5F20BC94380036D5A6 /* Debug */,
373 | 626DEA6020BC94380036D5A6 /* Release */,
374 | );
375 | defaultConfigurationIsVisible = 0;
376 | defaultConfigurationName = Release;
377 | };
378 | 626DEAA020BC94EE0036D5A6 /* Build configuration list for PBXNativeTarget "iOS Example" */ = {
379 | isa = XCConfigurationList;
380 | buildConfigurations = (
381 | 626DEAA120BC94EE0036D5A6 /* Debug */,
382 | 626DEAA220BC94EE0036D5A6 /* Release */,
383 | );
384 | defaultConfigurationIsVisible = 0;
385 | defaultConfigurationName = Release;
386 | };
387 | /* End XCConfigurationList section */
388 | };
389 | rootObject = 626DEA4720BC94360036D5A6 /* Project object */;
390 | }
391 |
--------------------------------------------------------------------------------
/KeyboardSupport.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 55;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 0E3F8FA4278F8D2700C9E69A /* KeyboardSupport.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0E3F8F9B278F8D2700C9E69A /* KeyboardSupport.framework */; };
11 | 0E3F8FCD278F8D6500C9E69A /* KeyboardAutoNavigator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3F8FB4278F8D6500C9E69A /* KeyboardAutoNavigator.swift */; };
12 | 0E3F8FCE278F8D6500C9E69A /* KeyboardNavigator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3F8FB5278F8D6500C9E69A /* KeyboardNavigator.swift */; };
13 | 0E3F8FCF278F8D6500C9E69A /* KeyboardToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3F8FB6278F8D6500C9E69A /* KeyboardToolbar.swift */; };
14 | 0E3F8FD0278F8D6500C9E69A /* KeyboardScrollable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3F8FB7278F8D6500C9E69A /* KeyboardScrollable.swift */; };
15 | 0E3F8FD1278F8D6500C9E69A /* KeyboardSupport.h in Headers */ = {isa = PBXBuildFile; fileRef = 0E3F8FB8278F8D6500C9E69A /* KeyboardSupport.h */; settings = {ATTRIBUTES = (Public, ); }; };
16 | 0E3F8FD2278F8D6500C9E69A /* KeyboardAccessory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3F8FB9278F8D6500C9E69A /* KeyboardAccessory.swift */; };
17 | 0E3F8FD3278F8D6500C9E69A /* UIView+KeyboardSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3F8FBB278F8D6500C9E69A /* UIView+KeyboardSupport.swift */; };
18 | 0E3F8FD4278F8D6500C9E69A /* UIScrollView+Inset.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3F8FBC278F8D6500C9E69A /* UIScrollView+Inset.swift */; };
19 | 0E3F8FD5278F8D6500C9E69A /* CGRect+Modifying.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3F8FBD278F8D6500C9E69A /* CGRect+Modifying.swift */; };
20 | 0E3F8FD6278F8D6500C9E69A /* UIEdgeInsets+KeyboardSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3F8FBE278F8D6500C9E69A /* UIEdgeInsets+KeyboardSupport.swift */; };
21 | 0E3F8FD7278F8D6500C9E69A /* Array+KeyboardSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3F8FBF278F8D6500C9E69A /* Array+KeyboardSupport.swift */; };
22 | 0E3F8FD8278F8D6500C9E69A /* KeyboardRespondable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3F8FC0278F8D6500C9E69A /* KeyboardRespondable.swift */; };
23 | 0E3F8FD9278F8D6500C9E69A /* KeyboardSafeAreaAdjustable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3F8FC1278F8D6500C9E69A /* KeyboardSafeAreaAdjustable.swift */; };
24 | 0E3F8FDA278F8D6500C9E69A /* KeyboardDismissable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3F8FC2278F8D6500C9E69A /* KeyboardDismissable.swift */; };
25 | 0E3F8FE2278F8D6800C9E69A /* KeyboardAutoNavigatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3F8FC5278F8D6500C9E69A /* KeyboardAutoNavigatorTests.swift */; };
26 | 0E3F8FE3278F8D6800C9E69A /* KeyboardToolbarTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3F8FC6278F8D6500C9E69A /* KeyboardToolbarTests.swift */; };
27 | 0E3F8FE4278F8D6800C9E69A /* KeyboardNavigatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3F8FC4278F8D6500C9E69A /* KeyboardNavigatorTests.swift */; };
28 | 0E3F8FE5278F8D6E00C9E69A /* MockKeyboardAccessoryDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3F8FCA278F8D6500C9E69A /* MockKeyboardAccessoryDelegate.swift */; };
29 | 0E3F8FE6278F8D6E00C9E69A /* MockKeyboardNavigatorDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3F8FC9278F8D6500C9E69A /* MockKeyboardNavigatorDelegate.swift */; };
30 | 0E3F8FE7278F8D6E00C9E69A /* MockKeyboardAutoNavigatorDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3F8FCB278F8D6500C9E69A /* MockKeyboardAutoNavigatorDelegate.swift */; };
31 | /* End PBXBuildFile section */
32 |
33 | /* Begin PBXContainerItemProxy section */
34 | 0E3F8FA5278F8D2700C9E69A /* PBXContainerItemProxy */ = {
35 | isa = PBXContainerItemProxy;
36 | containerPortal = 0E3F8F92278F8D2700C9E69A /* Project object */;
37 | proxyType = 1;
38 | remoteGlobalIDString = 0E3F8F9A278F8D2700C9E69A;
39 | remoteInfo = KeyboardSupport;
40 | };
41 | /* End PBXContainerItemProxy section */
42 |
43 | /* Begin PBXFileReference section */
44 | 0E3F8F9B278F8D2700C9E69A /* KeyboardSupport.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = KeyboardSupport.framework; sourceTree = BUILT_PRODUCTS_DIR; };
45 | 0E3F8FA3278F8D2700C9E69A /* KeyboardSupport iOS Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "KeyboardSupport iOS Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
46 | 0E3F8FB4278F8D6500C9E69A /* KeyboardAutoNavigator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyboardAutoNavigator.swift; sourceTree = ""; };
47 | 0E3F8FB5278F8D6500C9E69A /* KeyboardNavigator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyboardNavigator.swift; sourceTree = ""; };
48 | 0E3F8FB6278F8D6500C9E69A /* KeyboardToolbar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyboardToolbar.swift; sourceTree = ""; };
49 | 0E3F8FB7278F8D6500C9E69A /* KeyboardScrollable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyboardScrollable.swift; sourceTree = ""; };
50 | 0E3F8FB8278F8D6500C9E69A /* KeyboardSupport.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = KeyboardSupport.h; sourceTree = ""; };
51 | 0E3F8FB9278F8D6500C9E69A /* KeyboardAccessory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyboardAccessory.swift; sourceTree = ""; };
52 | 0E3F8FBB278F8D6500C9E69A /* UIView+KeyboardSupport.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIView+KeyboardSupport.swift"; sourceTree = ""; };
53 | 0E3F8FBC278F8D6500C9E69A /* UIScrollView+Inset.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIScrollView+Inset.swift"; sourceTree = ""; };
54 | 0E3F8FBD278F8D6500C9E69A /* CGRect+Modifying.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CGRect+Modifying.swift"; sourceTree = ""; };
55 | 0E3F8FBE278F8D6500C9E69A /* UIEdgeInsets+KeyboardSupport.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIEdgeInsets+KeyboardSupport.swift"; sourceTree = ""; };
56 | 0E3F8FBF278F8D6500C9E69A /* Array+KeyboardSupport.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Array+KeyboardSupport.swift"; sourceTree = ""; };
57 | 0E3F8FC0278F8D6500C9E69A /* KeyboardRespondable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyboardRespondable.swift; sourceTree = ""; };
58 | 0E3F8FC1278F8D6500C9E69A /* KeyboardSafeAreaAdjustable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyboardSafeAreaAdjustable.swift; sourceTree = ""; };
59 | 0E3F8FC2278F8D6500C9E69A /* KeyboardDismissable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyboardDismissable.swift; sourceTree = ""; };
60 | 0E3F8FC4278F8D6500C9E69A /* KeyboardNavigatorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyboardNavigatorTests.swift; sourceTree = ""; };
61 | 0E3F8FC5278F8D6500C9E69A /* KeyboardAutoNavigatorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyboardAutoNavigatorTests.swift; sourceTree = ""; };
62 | 0E3F8FC6278F8D6500C9E69A /* KeyboardToolbarTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyboardToolbarTests.swift; sourceTree = ""; };
63 | 0E3F8FC9278F8D6500C9E69A /* MockKeyboardNavigatorDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockKeyboardNavigatorDelegate.swift; sourceTree = ""; };
64 | 0E3F8FCA278F8D6500C9E69A /* MockKeyboardAccessoryDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockKeyboardAccessoryDelegate.swift; sourceTree = ""; };
65 | 0E3F8FCB278F8D6500C9E69A /* MockKeyboardAutoNavigatorDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockKeyboardAutoNavigatorDelegate.swift; sourceTree = ""; };
66 | 0E3F8FEA278F8D8B00C9E69A /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; };
67 | 0E3F8FEB278F8D8B00C9E69A /* KeyboardSupport.podspec */ = {isa = PBXFileReference; lastKnownFileType = text; path = KeyboardSupport.podspec; sourceTree = ""; };
68 | 0E3F8FEC278F8D8B00C9E69A /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = ""; };
69 | 0E3F8FEF278F8D9500C9E69A /* KeyboardSupport 1.x-2.0 Migration Guide.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = "KeyboardSupport 1.x-2.0 Migration Guide.md"; sourceTree = ""; };
70 | 0E3F8FF0278F8DA300C9E69A /* CONTRIBUTING.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CONTRIBUTING.md; sourceTree = SOURCE_ROOT; };
71 | 0E3F8FF1278F8DA300C9E69A /* Dangerfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dangerfile.swift; sourceTree = SOURCE_ROOT; };
72 | 0E3F8FF2278F8DA300C9E69A /* CODEOWNERS */ = {isa = PBXFileReference; lastKnownFileType = text; path = CODEOWNERS; sourceTree = SOURCE_ROOT; };
73 | 0E3F8FF3278F8DA300C9E69A /* CHANGELOG.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CHANGELOG.md; sourceTree = SOURCE_ROOT; };
74 | 0E3F8FF4278F8DA300C9E69A /* NOTICE */ = {isa = PBXFileReference; lastKnownFileType = text; path = NOTICE; sourceTree = SOURCE_ROOT; };
75 | 0E3F8FF5278F8DA300C9E69A /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = SOURCE_ROOT; };
76 | /* End PBXFileReference section */
77 |
78 | /* Begin PBXFrameworksBuildPhase section */
79 | 0E3F8F98278F8D2700C9E69A /* Frameworks */ = {
80 | isa = PBXFrameworksBuildPhase;
81 | buildActionMask = 2147483647;
82 | files = (
83 | );
84 | runOnlyForDeploymentPostprocessing = 0;
85 | };
86 | 0E3F8FA0278F8D2700C9E69A /* Frameworks */ = {
87 | isa = PBXFrameworksBuildPhase;
88 | buildActionMask = 2147483647;
89 | files = (
90 | 0E3F8FA4278F8D2700C9E69A /* KeyboardSupport.framework in Frameworks */,
91 | );
92 | runOnlyForDeploymentPostprocessing = 0;
93 | };
94 | /* End PBXFrameworksBuildPhase section */
95 |
96 | /* Begin PBXGroup section */
97 | 0E3F8F91278F8D2700C9E69A = {
98 | isa = PBXGroup;
99 | children = (
100 | 0E3F8FE9278F8D7E00C9E69A /* Deployment */,
101 | 0E3F8FED278F8D9500C9E69A /* Documentation */,
102 | 0E3F8FB3278F8D6500C9E69A /* Sources */,
103 | 0E3F8FC3278F8D6500C9E69A /* Tests */,
104 | 0E3F8F9C278F8D2700C9E69A /* Products */,
105 | );
106 | sourceTree = "";
107 | };
108 | 0E3F8F9C278F8D2700C9E69A /* Products */ = {
109 | isa = PBXGroup;
110 | children = (
111 | 0E3F8F9B278F8D2700C9E69A /* KeyboardSupport.framework */,
112 | 0E3F8FA3278F8D2700C9E69A /* KeyboardSupport iOS Tests.xctest */,
113 | );
114 | name = Products;
115 | sourceTree = "";
116 | };
117 | 0E3F8FB3278F8D6500C9E69A /* Sources */ = {
118 | isa = PBXGroup;
119 | children = (
120 | 0E3F8FB8278F8D6500C9E69A /* KeyboardSupport.h */,
121 | 0E3F8FB4278F8D6500C9E69A /* KeyboardAutoNavigator.swift */,
122 | 0E3F8FB5278F8D6500C9E69A /* KeyboardNavigator.swift */,
123 | 0E3F8FB6278F8D6500C9E69A /* KeyboardToolbar.swift */,
124 | 0E3F8FB7278F8D6500C9E69A /* KeyboardScrollable.swift */,
125 | 0E3F8FB9278F8D6500C9E69A /* KeyboardAccessory.swift */,
126 | 0E3F8FBA278F8D6500C9E69A /* Extensions */,
127 | 0E3F8FC0278F8D6500C9E69A /* KeyboardRespondable.swift */,
128 | 0E3F8FC1278F8D6500C9E69A /* KeyboardSafeAreaAdjustable.swift */,
129 | 0E3F8FC2278F8D6500C9E69A /* KeyboardDismissable.swift */,
130 | );
131 | path = Sources;
132 | sourceTree = "";
133 | };
134 | 0E3F8FBA278F8D6500C9E69A /* Extensions */ = {
135 | isa = PBXGroup;
136 | children = (
137 | 0E3F8FBB278F8D6500C9E69A /* UIView+KeyboardSupport.swift */,
138 | 0E3F8FBC278F8D6500C9E69A /* UIScrollView+Inset.swift */,
139 | 0E3F8FBD278F8D6500C9E69A /* CGRect+Modifying.swift */,
140 | 0E3F8FBE278F8D6500C9E69A /* UIEdgeInsets+KeyboardSupport.swift */,
141 | 0E3F8FBF278F8D6500C9E69A /* Array+KeyboardSupport.swift */,
142 | );
143 | path = Extensions;
144 | sourceTree = "";
145 | };
146 | 0E3F8FC3278F8D6500C9E69A /* Tests */ = {
147 | isa = PBXGroup;
148 | children = (
149 | 0E3F8FC4278F8D6500C9E69A /* KeyboardNavigatorTests.swift */,
150 | 0E3F8FC5278F8D6500C9E69A /* KeyboardAutoNavigatorTests.swift */,
151 | 0E3F8FC6278F8D6500C9E69A /* KeyboardToolbarTests.swift */,
152 | 0E3F8FC7278F8D6500C9E69A /* Helper */,
153 | );
154 | path = Tests;
155 | sourceTree = "";
156 | };
157 | 0E3F8FC7278F8D6500C9E69A /* Helper */ = {
158 | isa = PBXGroup;
159 | children = (
160 | 0E3F8FC8278F8D6500C9E69A /* Mocks */,
161 | );
162 | path = Helper;
163 | sourceTree = "";
164 | };
165 | 0E3F8FC8278F8D6500C9E69A /* Mocks */ = {
166 | isa = PBXGroup;
167 | children = (
168 | 0E3F8FC9278F8D6500C9E69A /* MockKeyboardNavigatorDelegate.swift */,
169 | 0E3F8FCA278F8D6500C9E69A /* MockKeyboardAccessoryDelegate.swift */,
170 | 0E3F8FCB278F8D6500C9E69A /* MockKeyboardAutoNavigatorDelegate.swift */,
171 | );
172 | path = Mocks;
173 | sourceTree = "";
174 | };
175 | 0E3F8FE9278F8D7E00C9E69A /* Deployment */ = {
176 | isa = PBXGroup;
177 | children = (
178 | 0E3F8FEA278F8D8B00C9E69A /* Package.swift */,
179 | 0E3F8FEB278F8D8B00C9E69A /* KeyboardSupport.podspec */,
180 | 0E3F8FEC278F8D8B00C9E69A /* LICENSE */,
181 | );
182 | name = Deployment;
183 | sourceTree = "";
184 | };
185 | 0E3F8FED278F8D9500C9E69A /* Documentation */ = {
186 | isa = PBXGroup;
187 | children = (
188 | 0E3F8FEE278F8D9500C9E69A /* Migrations */,
189 | 0E3F8FF3278F8DA300C9E69A /* CHANGELOG.md */,
190 | 0E3F8FF2278F8DA300C9E69A /* CODEOWNERS */,
191 | 0E3F8FF0278F8DA300C9E69A /* CONTRIBUTING.md */,
192 | 0E3F8FF1278F8DA300C9E69A /* Dangerfile.swift */,
193 | 0E3F8FF4278F8DA300C9E69A /* NOTICE */,
194 | 0E3F8FF5278F8DA300C9E69A /* README.md */,
195 | );
196 | path = Documentation;
197 | sourceTree = "";
198 | };
199 | 0E3F8FEE278F8D9500C9E69A /* Migrations */ = {
200 | isa = PBXGroup;
201 | children = (
202 | 0E3F8FEF278F8D9500C9E69A /* KeyboardSupport 1.x-2.0 Migration Guide.md */,
203 | );
204 | path = Migrations;
205 | sourceTree = "";
206 | };
207 | /* End PBXGroup section */
208 |
209 | /* Begin PBXHeadersBuildPhase section */
210 | 0E3F8F96278F8D2700C9E69A /* Headers */ = {
211 | isa = PBXHeadersBuildPhase;
212 | buildActionMask = 2147483647;
213 | files = (
214 | 0E3F8FD1278F8D6500C9E69A /* KeyboardSupport.h in Headers */,
215 | );
216 | runOnlyForDeploymentPostprocessing = 0;
217 | };
218 | /* End PBXHeadersBuildPhase section */
219 |
220 | /* Begin PBXNativeTarget section */
221 | 0E3F8F9A278F8D2700C9E69A /* KeyboardSupport iOS */ = {
222 | isa = PBXNativeTarget;
223 | buildConfigurationList = 0E3F8FAD278F8D2700C9E69A /* Build configuration list for PBXNativeTarget "KeyboardSupport iOS" */;
224 | buildPhases = (
225 | 0E3F8F96278F8D2700C9E69A /* Headers */,
226 | 0E3F8F97278F8D2700C9E69A /* Sources */,
227 | 0E3F8F98278F8D2700C9E69A /* Frameworks */,
228 | 0E3F8F99278F8D2700C9E69A /* Resources */,
229 | );
230 | buildRules = (
231 | );
232 | dependencies = (
233 | );
234 | name = "KeyboardSupport iOS";
235 | productName = KeyboardSupport;
236 | productReference = 0E3F8F9B278F8D2700C9E69A /* KeyboardSupport.framework */;
237 | productType = "com.apple.product-type.framework";
238 | };
239 | 0E3F8FA2278F8D2700C9E69A /* KeyboardSupport iOS Tests */ = {
240 | isa = PBXNativeTarget;
241 | buildConfigurationList = 0E3F8FB0278F8D2700C9E69A /* Build configuration list for PBXNativeTarget "KeyboardSupport iOS Tests" */;
242 | buildPhases = (
243 | 0E3F8F9F278F8D2700C9E69A /* Sources */,
244 | 0E3F8FA0278F8D2700C9E69A /* Frameworks */,
245 | 0E3F8FA1278F8D2700C9E69A /* Resources */,
246 | );
247 | buildRules = (
248 | );
249 | dependencies = (
250 | 0E3F8FA6278F8D2700C9E69A /* PBXTargetDependency */,
251 | );
252 | name = "KeyboardSupport iOS Tests";
253 | productName = KeyboardSupportTests;
254 | productReference = 0E3F8FA3278F8D2700C9E69A /* KeyboardSupport iOS Tests.xctest */;
255 | productType = "com.apple.product-type.bundle.unit-test";
256 | };
257 | /* End PBXNativeTarget section */
258 |
259 | /* Begin PBXProject section */
260 | 0E3F8F92278F8D2700C9E69A /* Project object */ = {
261 | isa = PBXProject;
262 | attributes = {
263 | BuildIndependentTargetsInParallel = 1;
264 | LastSwiftUpdateCheck = 1320;
265 | LastUpgradeCheck = 1320;
266 | TargetAttributes = {
267 | 0E3F8F9A278F8D2700C9E69A = {
268 | CreatedOnToolsVersion = 13.2.1;
269 | };
270 | 0E3F8FA2278F8D2700C9E69A = {
271 | CreatedOnToolsVersion = 13.2.1;
272 | };
273 | };
274 | };
275 | buildConfigurationList = 0E3F8F95278F8D2700C9E69A /* Build configuration list for PBXProject "KeyboardSupport" */;
276 | compatibilityVersion = "Xcode 13.0";
277 | developmentRegion = en;
278 | hasScannedForEncodings = 0;
279 | knownRegions = (
280 | en,
281 | Base,
282 | );
283 | mainGroup = 0E3F8F91278F8D2700C9E69A;
284 | productRefGroup = 0E3F8F9C278F8D2700C9E69A /* Products */;
285 | projectDirPath = "";
286 | projectRoot = "";
287 | targets = (
288 | 0E3F8F9A278F8D2700C9E69A /* KeyboardSupport iOS */,
289 | 0E3F8FA2278F8D2700C9E69A /* KeyboardSupport iOS Tests */,
290 | );
291 | };
292 | /* End PBXProject section */
293 |
294 | /* Begin PBXResourcesBuildPhase section */
295 | 0E3F8F99278F8D2700C9E69A /* Resources */ = {
296 | isa = PBXResourcesBuildPhase;
297 | buildActionMask = 2147483647;
298 | files = (
299 | );
300 | runOnlyForDeploymentPostprocessing = 0;
301 | };
302 | 0E3F8FA1278F8D2700C9E69A /* Resources */ = {
303 | isa = PBXResourcesBuildPhase;
304 | buildActionMask = 2147483647;
305 | files = (
306 | );
307 | runOnlyForDeploymentPostprocessing = 0;
308 | };
309 | /* End PBXResourcesBuildPhase section */
310 |
311 | /* Begin PBXSourcesBuildPhase section */
312 | 0E3F8F97278F8D2700C9E69A /* Sources */ = {
313 | isa = PBXSourcesBuildPhase;
314 | buildActionMask = 2147483647;
315 | files = (
316 | 0E3F8FCE278F8D6500C9E69A /* KeyboardNavigator.swift in Sources */,
317 | 0E3F8FD2278F8D6500C9E69A /* KeyboardAccessory.swift in Sources */,
318 | 0E3F8FD5278F8D6500C9E69A /* CGRect+Modifying.swift in Sources */,
319 | 0E3F8FD4278F8D6500C9E69A /* UIScrollView+Inset.swift in Sources */,
320 | 0E3F8FD6278F8D6500C9E69A /* UIEdgeInsets+KeyboardSupport.swift in Sources */,
321 | 0E3F8FCF278F8D6500C9E69A /* KeyboardToolbar.swift in Sources */,
322 | 0E3F8FDA278F8D6500C9E69A /* KeyboardDismissable.swift in Sources */,
323 | 0E3F8FD9278F8D6500C9E69A /* KeyboardSafeAreaAdjustable.swift in Sources */,
324 | 0E3F8FD8278F8D6500C9E69A /* KeyboardRespondable.swift in Sources */,
325 | 0E3F8FCD278F8D6500C9E69A /* KeyboardAutoNavigator.swift in Sources */,
326 | 0E3F8FD0278F8D6500C9E69A /* KeyboardScrollable.swift in Sources */,
327 | 0E3F8FD7278F8D6500C9E69A /* Array+KeyboardSupport.swift in Sources */,
328 | 0E3F8FD3278F8D6500C9E69A /* UIView+KeyboardSupport.swift in Sources */,
329 | );
330 | runOnlyForDeploymentPostprocessing = 0;
331 | };
332 | 0E3F8F9F278F8D2700C9E69A /* Sources */ = {
333 | isa = PBXSourcesBuildPhase;
334 | buildActionMask = 2147483647;
335 | files = (
336 | 0E3F8FE6278F8D6E00C9E69A /* MockKeyboardNavigatorDelegate.swift in Sources */,
337 | 0E3F8FE5278F8D6E00C9E69A /* MockKeyboardAccessoryDelegate.swift in Sources */,
338 | 0E3F8FE7278F8D6E00C9E69A /* MockKeyboardAutoNavigatorDelegate.swift in Sources */,
339 | 0E3F8FE3278F8D6800C9E69A /* KeyboardToolbarTests.swift in Sources */,
340 | 0E3F8FE4278F8D6800C9E69A /* KeyboardNavigatorTests.swift in Sources */,
341 | 0E3F8FE2278F8D6800C9E69A /* KeyboardAutoNavigatorTests.swift in Sources */,
342 | );
343 | runOnlyForDeploymentPostprocessing = 0;
344 | };
345 | /* End PBXSourcesBuildPhase section */
346 |
347 | /* Begin PBXTargetDependency section */
348 | 0E3F8FA6278F8D2700C9E69A /* PBXTargetDependency */ = {
349 | isa = PBXTargetDependency;
350 | target = 0E3F8F9A278F8D2700C9E69A /* KeyboardSupport iOS */;
351 | targetProxy = 0E3F8FA5278F8D2700C9E69A /* PBXContainerItemProxy */;
352 | };
353 | /* End PBXTargetDependency section */
354 |
355 | /* Begin XCBuildConfiguration section */
356 | 0E3F8FAB278F8D2700C9E69A /* Debug */ = {
357 | isa = XCBuildConfiguration;
358 | buildSettings = {
359 | ALWAYS_SEARCH_USER_PATHS = NO;
360 | CLANG_ANALYZER_NONNULL = YES;
361 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
362 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
363 | CLANG_CXX_LIBRARY = "libc++";
364 | CLANG_ENABLE_MODULES = YES;
365 | CLANG_ENABLE_OBJC_ARC = YES;
366 | CLANG_ENABLE_OBJC_WEAK = YES;
367 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
368 | CLANG_WARN_BOOL_CONVERSION = YES;
369 | CLANG_WARN_COMMA = YES;
370 | CLANG_WARN_CONSTANT_CONVERSION = YES;
371 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
372 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
373 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
374 | CLANG_WARN_EMPTY_BODY = YES;
375 | CLANG_WARN_ENUM_CONVERSION = YES;
376 | CLANG_WARN_INFINITE_RECURSION = YES;
377 | CLANG_WARN_INT_CONVERSION = YES;
378 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
379 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
380 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
381 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
382 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
383 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
384 | CLANG_WARN_STRICT_PROTOTYPES = YES;
385 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
386 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
387 | CLANG_WARN_UNREACHABLE_CODE = YES;
388 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
389 | COPY_PHASE_STRIP = NO;
390 | CURRENT_PROJECT_VERSION = 1;
391 | DEBUG_INFORMATION_FORMAT = dwarf;
392 | ENABLE_STRICT_OBJC_MSGSEND = YES;
393 | ENABLE_TESTABILITY = YES;
394 | GCC_C_LANGUAGE_STANDARD = gnu11;
395 | GCC_DYNAMIC_NO_PIC = NO;
396 | GCC_NO_COMMON_BLOCKS = YES;
397 | GCC_OPTIMIZATION_LEVEL = 0;
398 | GCC_PREPROCESSOR_DEFINITIONS = (
399 | "DEBUG=1",
400 | "$(inherited)",
401 | );
402 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
403 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
404 | GCC_WARN_UNDECLARED_SELECTOR = YES;
405 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
406 | GCC_WARN_UNUSED_FUNCTION = YES;
407 | GCC_WARN_UNUSED_VARIABLE = YES;
408 | IPHONEOS_DEPLOYMENT_TARGET = 12.0;
409 | MARKETING_VERSION = 2.2.0;
410 | MTL_ENABLE_DEBUG_INFO = YES;
411 | ONLY_ACTIVE_ARCH = YES;
412 | PRODUCT_NAME = KeyboardSupport;
413 | SDKROOT = iphoneos;
414 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
415 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
416 | SWIFT_VERSION = 5.0;
417 | VERSIONING_SYSTEM = "apple-generic";
418 | VERSION_INFO_PREFIX = "";
419 | };
420 | name = Debug;
421 | };
422 | 0E3F8FAC278F8D2700C9E69A /* Release */ = {
423 | isa = XCBuildConfiguration;
424 | buildSettings = {
425 | ALWAYS_SEARCH_USER_PATHS = NO;
426 | CLANG_ANALYZER_NONNULL = YES;
427 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
428 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
429 | CLANG_CXX_LIBRARY = "libc++";
430 | CLANG_ENABLE_MODULES = YES;
431 | CLANG_ENABLE_OBJC_ARC = YES;
432 | CLANG_ENABLE_OBJC_WEAK = YES;
433 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
434 | CLANG_WARN_BOOL_CONVERSION = YES;
435 | CLANG_WARN_COMMA = YES;
436 | CLANG_WARN_CONSTANT_CONVERSION = YES;
437 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
438 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
439 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
440 | CLANG_WARN_EMPTY_BODY = YES;
441 | CLANG_WARN_ENUM_CONVERSION = YES;
442 | CLANG_WARN_INFINITE_RECURSION = YES;
443 | CLANG_WARN_INT_CONVERSION = YES;
444 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
445 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
446 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
447 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
448 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
449 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
450 | CLANG_WARN_STRICT_PROTOTYPES = YES;
451 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
452 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
453 | CLANG_WARN_UNREACHABLE_CODE = YES;
454 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
455 | COPY_PHASE_STRIP = NO;
456 | CURRENT_PROJECT_VERSION = 1;
457 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
458 | ENABLE_NS_ASSERTIONS = NO;
459 | ENABLE_STRICT_OBJC_MSGSEND = YES;
460 | GCC_C_LANGUAGE_STANDARD = gnu11;
461 | GCC_NO_COMMON_BLOCKS = YES;
462 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
463 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
464 | GCC_WARN_UNDECLARED_SELECTOR = YES;
465 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
466 | GCC_WARN_UNUSED_FUNCTION = YES;
467 | GCC_WARN_UNUSED_VARIABLE = YES;
468 | IPHONEOS_DEPLOYMENT_TARGET = 12.0;
469 | MARKETING_VERSION = 2.2.0;
470 | MTL_ENABLE_DEBUG_INFO = NO;
471 | MTL_FAST_MATH = YES;
472 | PRODUCT_NAME = KeyboardSupport;
473 | SDKROOT = iphoneos;
474 | SWIFT_COMPILATION_MODE = wholemodule;
475 | SWIFT_OPTIMIZATION_LEVEL = "-O";
476 | VALIDATE_PRODUCT = YES;
477 | VERSIONING_SYSTEM = "apple-generic";
478 | VERSION_INFO_PREFIX = "";
479 | };
480 | name = Release;
481 | };
482 | 0E3F8FAE278F8D2700C9E69A /* Debug */ = {
483 | isa = XCBuildConfiguration;
484 | buildSettings = {
485 | APPLICATION_EXTENSION_API_ONLY = YES;
486 | CODE_SIGN_STYLE = Automatic;
487 | CURRENT_PROJECT_VERSION = 1;
488 | DEFINES_MODULE = YES;
489 | DYLIB_COMPATIBILITY_VERSION = 1;
490 | DYLIB_CURRENT_VERSION = 1;
491 | DYLIB_INSTALL_NAME_BASE = "@rpath";
492 | GENERATE_INFOPLIST_FILE = YES;
493 | INFOPLIST_KEY_NSHumanReadableCopyright = "";
494 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
495 | IPHONEOS_DEPLOYMENT_TARGET = 12.0;
496 | LD_RUNPATH_SEARCH_PATHS = (
497 | "$(inherited)",
498 | "@executable_path/Frameworks",
499 | "@loader_path/Frameworks",
500 | );
501 | PRODUCT_BUNDLE_IDENTIFIER = com.bottlerocketstudios.KeyboardSupport;
502 | SKIP_INSTALL = YES;
503 | SUPPORTS_MACCATALYST = NO;
504 | SWIFT_EMIT_LOC_STRINGS = YES;
505 | SWIFT_VERSION = 5.0;
506 | TARGETED_DEVICE_FAMILY = "1,2";
507 | };
508 | name = Debug;
509 | };
510 | 0E3F8FAF278F8D2700C9E69A /* Release */ = {
511 | isa = XCBuildConfiguration;
512 | buildSettings = {
513 | APPLICATION_EXTENSION_API_ONLY = YES;
514 | CODE_SIGN_STYLE = Automatic;
515 | CURRENT_PROJECT_VERSION = 1;
516 | DEFINES_MODULE = YES;
517 | DYLIB_COMPATIBILITY_VERSION = 1;
518 | DYLIB_CURRENT_VERSION = 1;
519 | DYLIB_INSTALL_NAME_BASE = "@rpath";
520 | GENERATE_INFOPLIST_FILE = YES;
521 | INFOPLIST_KEY_NSHumanReadableCopyright = "";
522 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
523 | IPHONEOS_DEPLOYMENT_TARGET = 12.0;
524 | LD_RUNPATH_SEARCH_PATHS = (
525 | "$(inherited)",
526 | "@executable_path/Frameworks",
527 | "@loader_path/Frameworks",
528 | );
529 | PRODUCT_BUNDLE_IDENTIFIER = com.bottlerocketstudios.KeyboardSupport;
530 | SKIP_INSTALL = YES;
531 | SUPPORTS_MACCATALYST = NO;
532 | SWIFT_EMIT_LOC_STRINGS = YES;
533 | SWIFT_VERSION = 5.0;
534 | TARGETED_DEVICE_FAMILY = "1,2";
535 | };
536 | name = Release;
537 | };
538 | 0E3F8FB1278F8D2700C9E69A /* Debug */ = {
539 | isa = XCBuildConfiguration;
540 | buildSettings = {
541 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
542 | CODE_SIGN_STYLE = Automatic;
543 | CURRENT_PROJECT_VERSION = 1;
544 | GENERATE_INFOPLIST_FILE = YES;
545 | MARKETING_VERSION = 1.0;
546 | PRODUCT_BUNDLE_IDENTIFIER = com.bottlerocketstudios.KeyboardSupportTests;
547 | PRODUCT_NAME = "$(TARGET_NAME)";
548 | SWIFT_EMIT_LOC_STRINGS = NO;
549 | SWIFT_VERSION = 5.0;
550 | TARGETED_DEVICE_FAMILY = "1,2";
551 | };
552 | name = Debug;
553 | };
554 | 0E3F8FB2278F8D2700C9E69A /* Release */ = {
555 | isa = XCBuildConfiguration;
556 | buildSettings = {
557 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
558 | CODE_SIGN_STYLE = Automatic;
559 | CURRENT_PROJECT_VERSION = 1;
560 | GENERATE_INFOPLIST_FILE = YES;
561 | MARKETING_VERSION = 1.0;
562 | PRODUCT_BUNDLE_IDENTIFIER = com.bottlerocketstudios.KeyboardSupportTests;
563 | PRODUCT_NAME = "$(TARGET_NAME)";
564 | SWIFT_EMIT_LOC_STRINGS = NO;
565 | SWIFT_VERSION = 5.0;
566 | TARGETED_DEVICE_FAMILY = "1,2";
567 | };
568 | name = Release;
569 | };
570 | /* End XCBuildConfiguration section */
571 |
572 | /* Begin XCConfigurationList section */
573 | 0E3F8F95278F8D2700C9E69A /* Build configuration list for PBXProject "KeyboardSupport" */ = {
574 | isa = XCConfigurationList;
575 | buildConfigurations = (
576 | 0E3F8FAB278F8D2700C9E69A /* Debug */,
577 | 0E3F8FAC278F8D2700C9E69A /* Release */,
578 | );
579 | defaultConfigurationIsVisible = 0;
580 | defaultConfigurationName = Release;
581 | };
582 | 0E3F8FAD278F8D2700C9E69A /* Build configuration list for PBXNativeTarget "KeyboardSupport iOS" */ = {
583 | isa = XCConfigurationList;
584 | buildConfigurations = (
585 | 0E3F8FAE278F8D2700C9E69A /* Debug */,
586 | 0E3F8FAF278F8D2700C9E69A /* Release */,
587 | );
588 | defaultConfigurationIsVisible = 0;
589 | defaultConfigurationName = Release;
590 | };
591 | 0E3F8FB0278F8D2700C9E69A /* Build configuration list for PBXNativeTarget "KeyboardSupport iOS Tests" */ = {
592 | isa = XCConfigurationList;
593 | buildConfigurations = (
594 | 0E3F8FB1278F8D2700C9E69A /* Debug */,
595 | 0E3F8FB2278F8D2700C9E69A /* Release */,
596 | );
597 | defaultConfigurationIsVisible = 0;
598 | defaultConfigurationName = Release;
599 | };
600 | /* End XCConfigurationList section */
601 | };
602 | rootObject = 0E3F8F92278F8D2700C9E69A /* Project object */;
603 | }
604 |
--------------------------------------------------------------------------------
/Example/Sources/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
274 |
275 |
276 |
277 |
278 |
279 |
280 |
281 |
282 |
283 |
284 |
285 |
286 |
287 |
288 |
289 |
290 |
291 |
292 |
293 |
294 |
295 |
296 |
297 |
298 |
299 |
300 |
301 |
302 |
303 |
304 |
305 |
306 |
307 |
308 |
309 |
310 |
311 |
312 |
313 |
314 |
315 |
316 |
317 |
318 |
319 |
320 |
321 |
322 |
323 |
324 |
325 |
326 |
327 |
328 |
329 |
330 |
331 |
332 |
333 |
334 |
335 |
336 |
337 |
338 |
339 |
340 |
341 |
342 |
343 |
344 |
345 |
346 |
347 |
348 |
349 |
--------------------------------------------------------------------------------