├── 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 | ![CI Status](https://github.com/BottleRocketStudios/iOS-KeyboardSupport/actions/workflows/main.yml/badge.svg) 3 | [![Version](https://img.shields.io/cocoapods/v/KeyboardSupport.svg?style=flat)](http://cocoapods.org/pods/KeyboardSupport) 4 | [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) 5 | [![License](https://img.shields.io/cocoapods/l/KeyboardSupport.svg?style=flat)](http://cocoapods.org/pods/KeyboardSupport) 6 | [![Platform](https://img.shields.io/cocoapods/p/KeyboardSupport.svg?style=flat)](http://cocoapods.org/pods/KeyboardSupport) 7 | [![codecov](https://codecov.io/gh/BottleRocketStudios/iOS-KeyboardSupport/branch/master/graph/badge.svg)](https://codecov.io/gh/BottleRocketStudios/iOS-KeyboardSupport) 8 | [![codebeat badge](https://codebeat.co/badges/3ef15dda-15d5-4bb6-a7f1-13f22da10813)](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 | --------------------------------------------------------------------------------