完善任务流程、键盘适配与页面交互

This commit is contained in:
2026-07-10 15:56:15 +08:00
parent f88a85a807
commit ab5220e460
189 changed files with 16779 additions and 1038 deletions

View File

@ -0,0 +1,43 @@
//
// IQKeyboardAppearanceConfiguration.swift
// https://github.com/hackiftekhar/IQKeyboardManager
// Copyright (c) 2013-24 Iftekhar Qurashi.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import UIKit
@available(iOSApplicationExtension, unavailable)
@MainActor
@objcMembers public final class IQKeyboardAppearanceConfiguration: NSObject {
/**
Override the keyboardAppearance for all textInputView. Default is NO.
*/
public var overrideAppearance: Bool = false
/**
If overrideKeyboardAppearance is YES, then all the textInputView keyboardAppearance is set using this property.
*/
public var appearance: UIKeyboardAppearance = .default
}
@available(*, unavailable, renamed: "IQKeyboardAppearanceConfiguration")
@MainActor
@objcMembers public final class IQKeyboardConfiguration: NSObject {}

View File

@ -0,0 +1,50 @@
//
// IQKeyboardAppearanceManager+Internal.swift
// https://github.com/hackiftekhar/IQKeyboardManager
// Copyright (c) 2013-24 Iftekhar Qurashi.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import UIKit
@available(iOSApplicationExtension, unavailable)
@MainActor
internal extension IQKeyboardAppearanceManager {
func removeTextInputViewObserver() {
textInputViewObserver.unsubscribe(identifier: "IQKeyboardAppearanceManager")
}
func addTextInputViewObserver() {
textInputViewObserver.subscribe(identifier: "IQKeyboardAppearanceManager",
changeHandler: { [weak self] event, textInputView in
guard let self = self else { return }
switch event {
case .beginEditing:
guard self.keyboardConfiguration.overrideAppearance,
textInputView.keyboardAppearance != self.keyboardConfiguration.appearance else { return }
textInputView.keyboardAppearance = self.keyboardConfiguration.appearance
textInputView.reloadInputViews()
case .endEditing:
break
}
})
}
}

View File

@ -0,0 +1,44 @@
//
// IQKeyboardAppearanceManager.swift
// https://github.com/hackiftekhar/IQKeyboardManager
// Copyright (c) 2013-24 Iftekhar Qurashi.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import UIKit
import IQTextInputViewNotification
@available(iOSApplicationExtension, unavailable)
@MainActor
@objcMembers internal final class IQKeyboardAppearanceManager: NSObject {
let textInputViewObserver: IQTextInputViewNotification = .init()
/**
Configuration related to keyboard appearance
*/
var keyboardConfiguration: IQKeyboardAppearanceConfiguration = .init()
public override init() {
super.init()
// Registering one time only
addTextInputViewObserver()
}
}

View File

@ -0,0 +1,55 @@
//
// IQKeyboardManager+Appearance.swift
// https://github.com/hackiftekhar/IQKeyboardManager
// Copyright (c) 2013-24 Iftekhar Qurashi.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import UIKit
@available(iOSApplicationExtension, unavailable)
@MainActor
@objc public extension IQKeyboardManager {
@MainActor
private struct AssociatedKeys {
static var appearanceManager: Int = 0
}
internal var appearanceManager: IQKeyboardAppearanceManager {
if let object = objc_getAssociatedObject(self, &AssociatedKeys.appearanceManager)
as? IQKeyboardAppearanceManager {
return object
}
let object: IQKeyboardAppearanceManager = .init()
objc_setAssociatedObject(self, &AssociatedKeys.appearanceManager,
object, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
return object
}
/**
Configuration related to keyboard appearance
*/
var keyboardConfiguration: IQKeyboardAppearanceConfiguration {
get { appearanceManager.keyboardConfiguration }
set { appearanceManager.keyboardConfiguration = newValue }
}
}

View File

@ -0,0 +1,43 @@
//
// IQKeyboardManager+Appearance_Deprecated.swift
// https://github.com/hackiftekhar/IQKeyboardManager
// Copyright (c) 2013-24 Iftekhar Qurashi.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import UIKit
// swiftlint:disable unused_setter_value
@available(iOSApplicationExtension, unavailable)
@MainActor
@objc public extension IQKeyboardManager {
@available(*, unavailable, renamed: "keyboardConfiguration.overrideAppearance")
var overrideKeyboardAppearance: Bool {
get { false }
set { }
}
@available(*, unavailable, renamed: "keyboardConfiguration.appearance")
var keyboardAppearance: UIKeyboardAppearance {
get { .default }
set { }
}
}
// swiftlint:enable unused_setter_value

View File

@ -0,0 +1,273 @@
//
// IQActiveConfiguration.swift
// https://github.com/hackiftekhar/IQKeyboardManager
// Copyright (c) 2013-24 Iftekhar Qurashi.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import UIKit
import IQKeyboardCore
import IQKeyboardNotification
import IQTextInputViewNotification
import Combine
@available(iOSApplicationExtension, unavailable)
@MainActor
internal final class IQActiveConfiguration: NSObject {
private let keyboardObserver: IQKeyboardNotification = .init()
private let textInputViewObserver: IQTextInputViewNotification = .init()
private var changeObservers: [AnyHashable: ConfigurationCompletion] = [:]
var cancellable: Set<AnyCancellable> = []
enum Event: Int {
case hide
case show
case change
var name: String {
switch self {
case .hide:
return "hide"
case .show:
return "show"
case .change:
return "change"
}
}
}
private var lastEvent: Event = .hide
var rootConfiguration: IQRootControllerConfiguration?
var isReady: Bool {
if textInputViewInfo != nil,
let rootConfiguration = rootConfiguration {
return rootConfiguration.isReady
}
return false
}
override init() {
super.init()
addKeyboardObserver()
addTextInputViewObserver()
}
private func sendEvent() {
guard let rootConfiguration = rootConfiguration,
rootConfiguration.isReady else { return }
if keyboardInfo.isVisible {
if lastEvent == .hide {
self.notify(event: .show, keyboardInfo: keyboardInfo, textInputViewInfo: textInputViewInfo)
} else {
self.notify(event: .change, keyboardInfo: keyboardInfo, textInputViewInfo: textInputViewInfo)
}
} else if lastEvent != .hide {
if rootConfiguration.beginOrientation == rootConfiguration.currentOrientation {
// If interactive pop gesture is active then it manipulate viewController.view's frame
// To overcome with this, we have to do this workaround.
if rootConfiguration.isInteractiveGestureActive,
let rootController: UIViewController = rootConfiguration.rootController {
self.cancellable.forEach { $0.cancel() }
self.cancellable.removeAll()
// Saving current keyboard info and textInputView
let keyboardInfo = keyboardObserver.keyboardInfo
let textInputViewInfo = textInputViewObserver.textInputViewInfo
// Start observing frame changes.
// If pop successful, then we'll not get callbacks here again
// If user cancels the pop, then we'll get frame as .zero at some time
// Also the interactiveGesture becomes inactive (genuinely it's state is .possible)
// At this moment.
rootController.view.publisher(for: \.frame)
.removeDuplicates()
.sink(receiveValue: { [weak self] frame in
guard let self = self else { return }
guard frame.origin == .zero,
!rootConfiguration.isInteractiveGestureActive else { return }
self.cancellable.forEach { $0.cancel() }
self.cancellable.removeAll()
// Restore keyboard info and textInputViewInfo
self.notify(event: .change, keyboardInfo: keyboardInfo, textInputViewInfo: textInputViewInfo)
}).store(in: &cancellable)
} else {
self.notify(event: .hide, keyboardInfo: keyboardInfo, textInputViewInfo: textInputViewInfo)
self.rootConfiguration = nil
}
} else if rootConfiguration.hasChanged {
animate(alongsideTransition: {
rootConfiguration.restore()
}, completion: nil)
}
}
}
private func updateRootController(textInputView: IQTextInputView?) {
guard let textInputView: UIView = textInputView,
let controller: UIViewController = textInputView.iq.parentContainerViewController() else {
if let rootConfiguration = rootConfiguration,
rootConfiguration.hasChanged {
animate(alongsideTransition: {
rootConfiguration.restore()
}, completion: nil)
}
rootConfiguration = nil
return
}
let newConfiguration = IQRootControllerConfiguration(rootController: controller)
guard newConfiguration.rootController?.view.window != rootConfiguration?.rootController?.view.window ||
newConfiguration.beginOrientation != rootConfiguration?.beginOrientation else { return }
if rootConfiguration?.rootController != newConfiguration.rootController {
// If there was an old configuration but things are changed
if let rootConfiguration = rootConfiguration,
rootConfiguration.hasChanged {
animate(alongsideTransition: {
rootConfiguration.restore()
}, completion: nil)
}
}
rootConfiguration = newConfiguration
}
}
@available(iOSApplicationExtension, unavailable)
@MainActor
extension IQActiveConfiguration {
var keyboardInfo: IQKeyboardInfo {
return keyboardObserver.keyboardInfo
}
private func addKeyboardObserver() {
keyboardObserver.subscribe(identifier: "IQActiveConfiguration", changeHandler: { [weak self] _, endFrame in
guard let self = self else { return }
guard self.keyboardObserver.oldKeyboardInfo.endFrame.height != endFrame.height else { return }
if let info = self.textInputViewInfo, self.keyboardInfo.isVisible {
if let rootConfiguration = self.rootConfiguration {
let beginIsPortrait: Bool = rootConfiguration.beginOrientation.isPortrait
let currentIsPortrait: Bool = rootConfiguration.currentOrientation.isPortrait
if beginIsPortrait != currentIsPortrait {
self.updateRootController(textInputView: info.textInputView)
}
} else {
self.updateRootController(textInputView: info.textInputView)
}
}
self.sendEvent()
// If interactive pop gesture is active then we don't want to remove this textField
if endFrame.height == 0,
!(self.rootConfiguration?.isInteractiveGestureActive ?? false) {
self.updateRootController(textInputView: nil)
}
})
}
public func animate(alongsideTransition transition: @escaping () -> Void, completion: (() -> Void)? = nil) {
keyboardObserver.animate(alongsideTransition: transition, completion: completion)
}
}
@available(iOSApplicationExtension, unavailable)
@MainActor
extension IQActiveConfiguration {
var textInputView: (any IQTextInputView)? {
guard let textInputView: UIView = textInputViewObserver.textInputView,
textInputView.iq.isAlertViewTextField() == false else {
return nil
}
return textInputViewObserver.textInputView
}
var textInputViewInfo: IQTextInputViewInfo? {
guard let textInputView: UIView = textInputView,
textInputView.iq.isAlertViewTextField() == false else {
return nil
}
return textInputViewObserver.textInputViewInfo
}
private func addTextInputViewObserver() {
textInputViewObserver.subscribe(identifier: "IQActiveConfiguration",
changeHandler: { [weak self] event, textInputView in
guard let self = self else { return }
guard (textInputView as UIView).iq.isAlertViewTextField() == false else {
return
}
if event == .beginEditing {
self.updateRootController(textInputView: textInputView)
self.sendEvent()
}
})
}
}
@available(iOSApplicationExtension, unavailable)
@MainActor
extension IQActiveConfiguration {
typealias ConfigurationCompletion = (_ event: Event,
_ keyboardInfo: IQKeyboardInfo,
_ textInputViewInfo: IQTextInputViewInfo?) -> Void
func subscribe(identifier: AnyHashable, changeHandler: @escaping ConfigurationCompletion) {
changeObservers[identifier] = changeHandler
}
func unsubscribe(identifier: AnyHashable) {
changeObservers[identifier] = nil
}
private func notify(event: Event, keyboardInfo: IQKeyboardInfo, textInputViewInfo: IQTextInputViewInfo?) {
lastEvent = event
for block in changeObservers.values {
block(event, keyboardInfo, textInputViewInfo)
}
}
}

View File

@ -0,0 +1,95 @@
//
// IQRootControllerConfiguration.swift
// https://github.com/hackiftekhar/IQKeyboardManager
// Copyright (c) 2013-24 Iftekhar Qurashi.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import UIKit
@available(iOSApplicationExtension, unavailable)
@MainActor
internal struct IQRootControllerConfiguration {
weak var rootController: UIViewController?
let beginOrigin: CGPoint
let beginSafeAreaInsets: UIEdgeInsets
let beginOrientation: UIInterfaceOrientation
init(rootController: UIViewController) {
self.rootController = rootController
beginOrigin = rootController.view.frame.origin
beginSafeAreaInsets = rootController.view.safeAreaInsets
let interfaceOrientation: UIInterfaceOrientation
if let scene = rootController.view.window?.windowScene {
interfaceOrientation = scene.interfaceOrientation
} else {
interfaceOrientation = .unknown
}
beginOrientation = interfaceOrientation
}
var currentOrientation: UIInterfaceOrientation {
let interfaceOrientation: UIInterfaceOrientation
if let scene = rootController?.view.window?.windowScene {
interfaceOrientation = scene.interfaceOrientation
} else {
interfaceOrientation = .unknown
}
return interfaceOrientation
}
var isReady: Bool {
return rootController?.view.window != nil
}
var hasChanged: Bool {
let origin: CGPoint = rootController?.view.frame.origin ?? .zero
return !origin.equalTo(beginOrigin)
}
var isInteractiveGestureActive: Bool {
guard let rootController: UIViewController = rootController,
let navigationController: UINavigationController = rootController.navigationController,
let interactiveGestureRecognizer = navigationController.interactivePopGestureRecognizer else {
return false
}
switch interactiveGestureRecognizer.state {
case .began, .changed:
return true
case .possible, .ended, .cancelled, .failed, .recognized:
// swiftlint:disable:next no_fallthrough_only
fallthrough
default:
return false
}
}
@discardableResult
func restore() -> Bool {
guard let rootController: UIViewController = rootController,
!rootController.view.frame.origin.equalTo(beginOrigin) else { return false }
// Setting it's new frame
var rect: CGRect = rootController.view.frame
rect.origin = beginOrigin
rootController.view.frame = rect
return true
}
}

View File

@ -0,0 +1,100 @@
//
// IQScrollViewConfiguration.swift
// https://github.com/hackiftekhar/IQKeyboardManager
// Copyright (c) 2013-24 Iftekhar Qurashi.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import UIKit
import IQKeyboardCore
@available(iOSApplicationExtension, unavailable)
@MainActor
internal struct IQScrollViewConfiguration {
let scrollView: UIScrollView
let startingContentOffset: CGPoint
let startingScrollIndicatorInsets: UIEdgeInsets
let startingContentInset: UIEdgeInsets
private let canRestoreContentOffset: Bool
init(scrollView: UIScrollView, canRestoreContentOffset: Bool) {
self.scrollView = scrollView
self.canRestoreContentOffset = canRestoreContentOffset
startingContentOffset = scrollView.contentOffset
startingContentInset = scrollView.contentInset
startingScrollIndicatorInsets = scrollView.verticalScrollIndicatorInsets
}
var hasChanged: Bool {
if scrollView.contentInset != self.startingContentInset {
return true
}
if canRestoreContentOffset,
scrollView.iq.restoreContentOffset,
!scrollView.contentOffset.equalTo(startingContentOffset) {
return true
}
return false
}
@discardableResult
func restore(for textInputView: (some IQTextInputView)?) -> Bool {
var success: Bool = false
if scrollView.contentInset != self.startingContentInset {
scrollView.contentInset = self.startingContentInset
scrollView.layoutIfNeeded() // (Bug ID: #1996)
success = true
}
if scrollView.verticalScrollIndicatorInsets != self.startingScrollIndicatorInsets {
scrollView.verticalScrollIndicatorInsets = self.startingScrollIndicatorInsets
}
if canRestoreContentOffset,
scrollView.iq.restoreContentOffset,
!scrollView.contentOffset.equalTo(startingContentOffset) {
// (Bug ID: #1365, #1508, #1541)
let stackView: UIStackView?
if let textInputView: UIView = textInputView {
stackView = textInputView.iq.superviewOf(type: UIStackView.self,
belowView: scrollView)
} else {
stackView = nil
}
// (Bug ID: #1901, #1996)
let animatedContentOffset: Bool = stackView != nil ||
scrollView is UICollectionView ||
scrollView is UITableView
if animatedContentOffset {
scrollView.setContentOffset(startingContentOffset, animated: UIView.areAnimationsEnabled)
} else {
scrollView.contentOffset = startingContentOffset
}
success = true
}
return success
}
}

View File

@ -0,0 +1,78 @@
//
// IQKeyboardManager+Debug.swift
// https://github.com/hackiftekhar/IQKeyboardManager
// Copyright (c) 2013-24 Iftekhar Qurashi.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import UIKit
// MARK: Debugging & Developer options
@available(iOSApplicationExtension, unavailable)
@MainActor
@objc public extension IQKeyboardManager {
@MainActor
private struct AssociatedKeys {
static var isDebuggingEnabled: Int = 0
static var logIndentation: Int = 0
}
var isDebuggingEnabled: Bool {
get {
return objc_getAssociatedObject(self, &AssociatedKeys.isDebuggingEnabled) as? Bool ?? false
}
set(newValue) {
objc_setAssociatedObject(self, &AssociatedKeys.isDebuggingEnabled,
newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
private var logIndentation: Int {
get {
return objc_getAssociatedObject(self, &AssociatedKeys.logIndentation) as? Int ?? 0
}
set(newValue) {
objc_setAssociatedObject(self, &AssociatedKeys.logIndentation,
newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
internal func showLog(_ logString: String, indentation: Int = 0) {
guard isDebuggingEnabled else {
return
}
if indentation < 0 {
logIndentation = max(0, logIndentation + indentation)
}
var preLog: String = "IQKeyboardManager"
for _ in 0 ... logIndentation {
preLog += "|\t"
}
print(preLog + logString)
if indentation > 0 {
logIndentation += indentation
}
}
}

View File

@ -0,0 +1,169 @@
//
// IQKeyboardManager+Deprecated.swift
// https://github.com/hackiftekhar/IQKeyboardManager
// Copyright (c) 2013-24 Iftekhar Qurashi.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import UIKit
// swiftlint:disable unused_setter_value
// swiftlint:disable line_length
// swiftlint:disable type_name
@available(iOSApplicationExtension, unavailable)
@MainActor
@objc public extension IQKeyboardManager {
@available(*, unavailable, renamed: "keyboardDistance")
var keyboardDistanceFromTextField: CGFloat {
get { fatalError() }
set { }
}
}
@available(iOSApplicationExtension, unavailable)
@MainActor
@objc public extension IQKeyboardManager {
@available(*, unavailable, message: "This feature has been removed due to few compatibility problems")
func registerTextFieldViewClass(_ aClass: UIView.Type,
didBeginEditingNotificationName: String,
didEndEditingNotificationName: String) {
}
@available(*, unavailable, message: "This feature has been removed due to few compatibility problems")
func unregisterTextFieldViewClass(_ aClass: UIView.Type,
didBeginEditingNotificationName: String,
didEndEditingNotificationName: String) {
}
}
@available(iOSApplicationExtension, unavailable)
@MainActor
@objc public extension IQKeyboardManager {
typealias SizeBlock = (_ size: CGSize) -> Void
@available(*, unavailable, message: "Please use `IQKeyboardNotification` independently from https://github.com/hackiftekhar/IQKeyboardNotification")
func registerKeyboardSizeChange(identifier: AnyHashable, sizeHandler: @escaping SizeBlock) {}
@available(*, unavailable, message: "Please use `IQKeyboardNotification` independently from https://github.com/hackiftekhar/IQKeyboardNotification")
func unregisterKeyboardSizeChange(identifier: AnyHashable) {}
@available(*, unavailable, message: "Please use `IQKeyboardNotification` independently from https://github.com/hackiftekhar/IQKeyboardNotification")
var keyboardShowing: Bool { false }
@available(*, unavailable, message: "Please use `IQKeyboardNotification` independently from https://github.com/hackiftekhar/IQKeyboardNotification")
var keyboardFrame: CGRect { .zero }
}
@available(*, unavailable, renamed: "IQKeyboardReturnManager", message: "Please use `IQKeyboardReturnManager` independently from https://github.com/hackiftekhar/IQKeyboardReturnManager")
@MainActor
@objcMembers public final class IQKeyboardReturnKeyHandler: NSObject {}
@available(*, unavailable, renamed: "IQKeyboardNotification", message: "Please use `IQKeyboardNotification` independently from https://github.com/hackiftekhar/IQKeyboardNotification")
@MainActor
@objcMembers public final class IQKeyboardListener: NSObject {}
@available(*, unavailable, renamed: "IQTextInputViewNotification", message: "Please use `IQTextInputViewNotification` independently from https://github.com/hackiftekhar/IQTextInputViewNotification")
@MainActor
@objcMembers public final class IQTextFieldViewListener: NSObject {}
@available(*, unavailable, renamed: "IQDeepResponderContainerView", message: "Please use `IQDeepResponderContainerView` class which is now part of `IQKeyboardToolbarManager` from https://github.com/hackiftekhar/IQKeyboardToolbarManager.")
@MainActor
@objcMembers open class IQPreviousNextView: UIView {}
@available(*, unavailable, message: "Please use `IQKeyboardToolbar` independently https://github.com/hackiftekhar/IQKeyboardToolbar or through `IQKeyboardToolbarManager` from https://github.com/hackiftekhar/IQKeyboardToolbarManager")
@MainActor
@objcMembers public final class IQToolbarPlaceholderConfigurationDeprecated: NSObject {
public var showPlaceholder: Bool = true
public var font: UIFont?
public var color: UIColor?
public var buttonColor: UIColor?
public override var accessibilityLabel: String? { didSet { } }
}
@available(iOSApplicationExtension, unavailable)
@available(*, unavailable, message: "Please use `IQKeyboardToolbar` independently https://github.com/hackiftekhar/IQKeyboardToolbar or through `IQKeyboardToolbarManager` from https://github.com/hackiftekhar/IQKeyboardToolbarManager")
@MainActor
@objcMembers public final class IQBarButtonItemConfigurationDeprecated: NSObject {
public init(systemItem: UIBarButtonItem.SystemItem, action: Selector? = nil) {
self.systemItem = systemItem
self.image = nil
self.title = nil
self.action = action
super.init()
}
public init(image: UIImage, action: Selector? = nil) {
self.systemItem = nil
self.image = image
self.title = nil
self.action = action
super.init()
}
public init(title: String, action: Selector? = nil) {
self.systemItem = nil
self.image = nil
self.title = title
self.action = action
super.init()
}
public let systemItem: UIBarButtonItem.SystemItem?
public let image: UIImage?
public let title: String?
public var action: Selector?
public override var accessibilityLabel: String? { didSet { } }
}
@available(*, unavailable, message: "Please use `IQKeyboardToolbarManager` independently from https://github.com/hackiftekhar/IQKeyboardToolbarManager")
@objc public enum IQAutoToolbarManageBehaviorDeprecated: Int {
case bySubviews
case byTag
case byPosition
}
@available(*, unavailable, message: "Please use `IQKeyboardToolbarManager` independently from https://github.com/hackiftekhar/IQKeyboardToolbarManager")
@objc public enum IQPreviousNextDisplayModeDeprecated: Int {
case `default`
case alwaysHide
case alwaysShow
}
@available(*, unavailable, message: "Please use `IQKeyboardToolbarManager` independently from https://github.com/hackiftekhar/IQKeyboardToolbarManager")
@MainActor
@objcMembers public final class IQToolbarConfiguration: NSObject {
public var useTextInputViewTintColor: Bool = false
public var tintColor: UIColor?
public var barTintColor: UIColor?
public var previousNextDisplayMode: IQPreviousNextDisplayModeDeprecated = .default
public var manageBehavior: IQAutoToolbarManageBehaviorDeprecated = .bySubviews
public var previousBarButtonConfiguration: IQBarButtonItemConfigurationDeprecated?
public var nextBarButtonConfiguration: IQBarButtonItemConfigurationDeprecated?
public var doneBarButtonConfiguration: IQBarButtonItemConfigurationDeprecated?
public let placeholderConfiguration: IQToolbarPlaceholderConfigurationDeprecated = .init()
}
// swiftlint:enable line_length
// swiftlint:enable unused_setter_value
// swiftlint:enable type_name

View File

@ -0,0 +1,126 @@
//
// IQKeyboardManager+ActiveConfiguration.swift
// https://github.com/hackiftekhar/IQKeyboardManager
// Copyright (c) 2013-24 Iftekhar Qurashi.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import UIKit
import IQKeyboardCore
import Combine
// MARK: UIKeyboard Notifications
@available(iOSApplicationExtension, unavailable)
@MainActor
internal extension IQKeyboardManager {
func addActiveConfigurationObserver() {
activeConfiguration.subscribe(identifier: "IQKeyboardManager", changeHandler: { [weak self] event, _, _ in
guard let self = self else { return }
switch event {
case .show:
self.handleKeyboardTextInputViewVisible()
case .change:
self.handleKeyboardTextInputViewChanged()
case .hide:
self.handleKeyboardTextInputViewHide()
}
})
}
private func handleKeyboardTextInputViewVisible() {
setupTextInputView()
if privateIsEnabled() {
adjustPosition()
} else {
restorePosition()
}
}
private func handleKeyboardTextInputViewChanged() {
setupTextInputView()
if privateIsEnabled() {
adjustPosition()
} else {
restorePosition()
}
}
private func handleKeyboardTextInputViewHide() {
self.restorePosition()
self.banishTextInputViewSetup()
self.lastScrollViewConfiguration = nil
}
}
@available(iOSApplicationExtension, unavailable)
@MainActor
internal extension IQKeyboardManager {
func setupTextInputView() {
guard let textInputView = activeConfiguration.textInputView else {
return
}
if let startingConfiguration = startingTextViewConfiguration,
startingConfiguration.hasChanged {
if startingConfiguration.scrollView.contentInset != startingConfiguration.startingContentInset {
showLog("""
Restoring textView.contentInset to: \(startingConfiguration.startingContentInset)
""")
}
activeConfiguration.animate(alongsideTransition: {
startingConfiguration.restore(for: textInputView)
})
}
startingTextViewConfiguration = nil
}
func banishTextInputViewSetup() {
guard let textInputView = activeConfiguration.textInputView else {
return
}
if let startingConfiguration = startingTextViewConfiguration,
startingConfiguration.hasChanged {
if startingConfiguration.scrollView.contentInset != startingConfiguration.startingContentInset {
showLog("""
Restoring textView.contentInset to: \(startingConfiguration.startingContentInset)
""")
}
activeConfiguration.animate(alongsideTransition: {
startingConfiguration.restore(for: textInputView)
})
}
startingTextViewConfiguration = nil
}
}

View File

@ -0,0 +1,93 @@
//
// IQKeyboardManager+Internal.swift
// https://github.com/hackiftekhar/IQKeyboardManager
// Copyright (c) 2013-24 Iftekhar Qurashi.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import UIKit
import IQKeyboardCore
@available(iOSApplicationExtension, unavailable)
@MainActor
internal extension IQKeyboardManager {
func privateIsEnabled() -> Bool {
guard let textInputView: any IQTextInputView = activeConfiguration.textInputView else {
return isEnabled
}
switch textInputView.internalEnableMode {
case .default:
guard var controller = (textInputView as UIView).iq.viewContainingController() else {
return isEnabled
}
if textInputView is UISearchTextField {
if let navController: UINavigationController = controller as? UINavigationController,
let topController: UIViewController = navController.topViewController {
controller = topController
}
// Not adjusting for searchTextField inside searchController.
if controller.navigationItem.searchController?.searchBar.searchTextField == textInputView {
return false
}
}
// If viewController is in enabledDistanceHandlingClasses, then assuming it's enabled.
let isWithEnabledClass: Bool = enabledDistanceHandlingClasses.contains(where: { controller.isKind(of: $0) })
var isEnabled: Bool = isEnabled || isWithEnabledClass
if isEnabled {
// If viewController is in disabledDistanceHandlingClasses,
// then assuming it's disabled.
if disabledDistanceHandlingClasses.contains(where: { controller.isKind(of: $0) }) {
isEnabled = false
} else {
// Special Controllers
let classNameString: String = "\(type(of: controller.self))"
// _UIAlertControllerTextFieldViewController
if classNameString.contains("UIAlertController"),
classNameString.hasSuffix("TextFieldViewController") {
isEnabled = false
}
}
}
return isEnabled
case .enabled:
return true
case .disabled:
return false
@unknown default:
return false
}
}
}
@available(iOSApplicationExtension, unavailable)
@MainActor
fileprivate extension IQTextInputView {
var internalEnableMode: IQEnableMode {
return iq.enableMode
}
}

View File

@ -0,0 +1,798 @@
//
// IQKeyboardManager+Position.swift
// https://github.com/hackiftekhar/IQKeyboardManager
// Copyright (c) 2013-24 Iftekhar Qurashi.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import UIKit
import IQKeyboardCore
// swiftlint:disable file_length
@available(iOSApplicationExtension, unavailable)
@MainActor
@objc public extension IQKeyboardManager {
private typealias IQLayoutGuide = (top: CGFloat, bottom: CGFloat)
@MainActor
private struct AssociatedKeys {
static var movedDistance: Int = 0
static var movedDistanceChanged: Int = 0
static var lastScrollViewConfiguration: Int = 0
static var startingTextViewConfiguration: Int = 0
static var activeConfiguration: Int = 0
}
/**
moved distance to the top used to maintain distance between keyboard and textInputView.
Most of the time this will be a positive value.
*/
private(set) var movedDistance: CGFloat {
get {
return objc_getAssociatedObject(self, &AssociatedKeys.movedDistance) as? CGFloat ?? 0.0
}
set(newValue) {
objc_setAssociatedObject(self, &AssociatedKeys.movedDistance, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
movedDistanceChanged?(movedDistance)
}
}
/**
Will be called then movedDistance will be changed
*/
var movedDistanceChanged: ((CGFloat) -> Void)? {
get {
return objc_getAssociatedObject(self, &AssociatedKeys.movedDistanceChanged) as? ((CGFloat) -> Void)
}
set(newValue) {
objc_setAssociatedObject(self, &AssociatedKeys.movedDistanceChanged,
newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
movedDistanceChanged?(movedDistance)
}
}
/** Variable to save lastScrollView that was scrolled. */
@nonobjc
internal var lastScrollViewConfiguration: IQScrollViewConfiguration? {
get {
return objc_getAssociatedObject(self,
&AssociatedKeys.lastScrollViewConfiguration) as? IQScrollViewConfiguration
}
set(newValue) {
objc_setAssociatedObject(self, &AssociatedKeys.lastScrollViewConfiguration,
newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
/** used to adjust contentInset of UITextView. */
@nonobjc
internal var startingTextViewConfiguration: IQScrollViewConfiguration? {
get {
return objc_getAssociatedObject(self,
&AssociatedKeys.startingTextViewConfiguration) as? IQScrollViewConfiguration
}
set(newValue) {
objc_setAssociatedObject(self, &AssociatedKeys.startingTextViewConfiguration,
newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
internal func applicationDidBecomeActive(_ notification: Notification) {
guard privateIsEnabled(),
activeConfiguration.keyboardInfo.isVisible,
activeConfiguration.isReady else {
return
}
adjustPosition()
}
/* Adjusting RootViewController's frame according to interface orientation. */
// swiftlint:disable function_body_length
internal func adjustPosition() {
guard UIApplication.shared.applicationState == .active,
let textInputView: any IQTextInputView = activeConfiguration.textInputView,
let superview: UIView = textInputView.superview,
let rootConfiguration = activeConfiguration.rootConfiguration,
let rootController: UIViewController = rootConfiguration.rootController,
let window: UIWindow = rootController.view.window else {
return
}
showLog(">>>>> \(#function) started >>>>>", indentation: 1)
defer {
showLog("<<<<< \(#function) ended <<<<<", indentation: -1)
}
let textInputViewRectInWindow: CGRect = superview.convert(textInputView.frame, to: window)
let textInputViewRectInRootSuperview: CGRect = superview.convert(textInputView.frame,
to: rootController.view.superview)
// Getting RootViewOrigin.
let rootViewOrigin: CGPoint = rootController.view.frame.origin
let keyboardDistance: CGFloat = getSpecialTextInputViewDistance(textInputView: textInputView)
let kbSize: CGSize = Self.getKeyboardSize(keyboardDistance: keyboardDistance,
keyboardFrame: activeConfiguration.keyboardInfo.endFrame,
safeAreaInsets: rootConfiguration.beginSafeAreaInsets,
windowFrame: window.frame)
let originalKbSize: CGSize = activeConfiguration.keyboardInfo.endFrame.size
let isScrollableTextInputView: Bool
if let textInputView: UIScrollView = textInputView as? UITextView {
isScrollableTextInputView = textInputView.isScrollEnabled
} else {
isScrollableTextInputView = false
}
let layoutGuide: IQLayoutGuide = Self.getLayoutGuides(rootController: rootController, window: window,
isScrollableTextInputView: isScrollableTextInputView)
var superScrollView: UIScrollView?
var superView: UIScrollView? = (textInputView as UIView).iq.superviewOf(type: UIScrollView.self)
// Getting UIScrollView whose scrolling is enabled. // (Bug ID: #285)
while let view: UIScrollView = superView {
if view.isScrollEnabled, !view.iq.ignoreScrollingAdjustment {
superScrollView = view
break
} else {
// Getting it's superScrollView. // (Enhancement ID: #21, #24)
superView = view.iq.superviewOf(type: UIScrollView.self)
}
}
// Move positive = textInputView is hidden.
// Move negative = textInputView is showing.
// Calculating move position.
var moveUp: CGFloat = Self.getMoveUpDistance(keyboardSize: kbSize,
layoutGuide: layoutGuide,
textInputViewRectInRootSuperview: textInputViewRectInRootSuperview,
textInputViewRectInWindow: textInputViewRectInWindow,
windowFrame: window.frame)
showLog("Need to move: \(moveUp), will be moving \(moveUp < 0 ? "down" : "up")")
setupActiveScrollViewConfiguration(superScrollView: superScrollView, textInputView: textInputView)
// Special case for ScrollView.
// If we found lastScrollView then setting it's contentOffset to show textInputView.
if let lastScrollViewConfiguration: IQScrollViewConfiguration = lastScrollViewConfiguration {
adjustScrollViewContentOffsets(moveUp: &moveUp, textInputView: textInputView,
lastScrollViewConfiguration: lastScrollViewConfiguration,
rootSuperview: rootController.view.superview, layoutGuide: layoutGuide,
textInputViewRectInRootSuperview: textInputViewRectInRootSuperview,
isScrollableTextInputView: isScrollableTextInputView, window: window,
kbSize: kbSize, keyboardDistance: keyboardDistance,
rootBeginSafeAreaInsets: rootConfiguration.beginSafeAreaInsets)
}
// Special case for UITextView
// (Readjusting textInputView.contentInset when textInputView hight is too big to fit on screen)
// _lastScrollView If not having inside any scrollView, now contentInset manages the full screen textInputView.
// If is a UITextView type
if isScrollableTextInputView, let textInputView = textInputView as? UITextView {
adjustTextInputViewContentInset(window: window, originalKbSize: originalKbSize,
rootSuperview: rootController.view.superview,
layoutGuide: layoutGuide,
textInputView: textInputView)
}
adjustRootController(moveUp: moveUp, rootViewOrigin: rootViewOrigin, originalKbSize: originalKbSize,
rootController: rootController, rootBeginOrigin: rootConfiguration.beginOrigin)
}
// swiftlint:enable function_body_length
internal func restorePosition() {
// Setting rootViewController frame to it's original position. // (Bug ID: #18)
guard let configuration: IQRootControllerConfiguration = activeConfiguration.rootConfiguration else {
return
}
showLog(">>>>> \(#function) started >>>>>", indentation: 1)
defer {
showLog("<<<<< \(#function) ended <<<<<", indentation: -1)
}
activeConfiguration.animate(alongsideTransition: {
if configuration.hasChanged {
let classNameString: String = "\(type(of: configuration.rootController.self))"
self.showLog("Restoring \(classNameString) origin to: \(configuration.beginOrigin)")
}
configuration.restore()
// Animating content if needed (Bug ID: #204)
if self.layoutIfNeededOnUpdate {
// Animating content (Bug ID: #160)
configuration.rootController?.view.setNeedsLayout()
configuration.rootController?.view.layoutIfNeeded()
}
})
// Restoring the contentOffset of the lastScrollView
if let lastConfiguration: IQScrollViewConfiguration = lastScrollViewConfiguration {
let textInputView: (any IQTextInputView)? = activeConfiguration.textInputView
restoreScrollViewConfigurationIfChanged(configuration: lastConfiguration, textInputView: textInputView)
activeConfiguration.animate(alongsideTransition: {
// This is temporary solution. Have to implement the save and restore scrollView state
self.restoreScrollViewContentOffset(superScrollView: lastConfiguration.scrollView,
textInputView: textInputView)
})
}
self.movedDistance = 0
}
}
// swiftlint:disable function_parameter_count
@available(iOSApplicationExtension, unavailable)
@MainActor
private extension IQKeyboardManager {
func getSpecialTextInputViewDistance(textInputView: some IQTextInputView) -> CGFloat {
// Maintain keyboardDistance
let specialKeyboardDistance: CGFloat
if let searchBar: UISearchBar = textInputView.iq.textFieldSearchBar() {
specialKeyboardDistance = searchBar.iq.distanceFromKeyboard
} else {
specialKeyboardDistance = textInputView.iq.distanceFromKeyboard
}
if specialKeyboardDistance == UIView.defaultKeyboardDistance {
return keyboardDistance
} else {
return specialKeyboardDistance
}
}
static func getKeyboardSize(keyboardDistance: CGFloat, keyboardFrame: CGRect,
safeAreaInsets: UIEdgeInsets, windowFrame: CGRect) -> CGSize {
let kbSize: CGSize
var kbFrame: CGRect = keyboardFrame
kbFrame.origin.y -= keyboardDistance
kbFrame.size.height += keyboardDistance
kbFrame.origin.y -= safeAreaInsets.bottom
kbFrame.size.height += safeAreaInsets.bottom
// (Bug ID: #469) (Bug ID: #381) (Bug ID: #1506)
// Calculating actual keyboard covered size respect to window,
// keyboard frame may be different when hardware keyboard is attached
let intersectRect: CGRect = kbFrame.intersection(windowFrame)
if intersectRect.isNull {
kbSize = CGSize(width: kbFrame.size.width, height: 0)
} else {
kbSize = intersectRect.size
}
return kbSize
}
static private func getLayoutGuides(rootController: UIViewController, window: UIWindow,
isScrollableTextInputView: Bool) -> IQLayoutGuide {
let navigationBarAreaHeight: CGFloat
if let navigationController: UINavigationController = rootController.navigationController {
navigationBarAreaHeight = navigationController.navigationBar.frame.maxY
} else {
let statusBarHeight: CGFloat = window.windowScene?.statusBarManager?.statusBarFrame.height ?? 0
navigationBarAreaHeight = statusBarHeight
}
let directionalLayoutMargin: NSDirectionalEdgeInsets = rootController.view.directionalLayoutMargins
let topLayoutGuide: CGFloat = CGFloat.maximum(navigationBarAreaHeight, directionalLayoutMargin.top)
// Validation of textInputView for case where there is a tab bar
// at the bottom or running on iPhone X and textInputView is at the bottom.
let bottomLayoutGuide: CGFloat = isScrollableTextInputView ? 0 : directionalLayoutMargin.bottom
return (topLayoutGuide, bottomLayoutGuide)
}
static private func getMoveUpDistance(keyboardSize: CGSize,
layoutGuide: IQLayoutGuide,
textInputViewRectInRootSuperview: CGRect,
textInputViewRectInWindow: CGRect,
windowFrame: CGRect) -> CGFloat {
// Move positive = textInputView is hidden.
// Move negative = textInputView is showing.
// Calculating move position.
let visibleHeight: CGFloat = windowFrame.height-keyboardSize.height
let topMovement: CGFloat = textInputViewRectInRootSuperview.minY-layoutGuide.top
let bottomMovement: CGFloat = textInputViewRectInWindow.maxY - visibleHeight + layoutGuide.bottom
var moveUp: CGFloat = CGFloat.minimum(topMovement, bottomMovement)
moveUp = CGFloat(Int(moveUp))
return moveUp
}
func setupActiveScrollViewConfiguration(superScrollView: UIScrollView?, textInputView: some IQTextInputView) {
// If there was a lastScrollView. // (Bug ID: #34)
guard let lastConfiguration: IQScrollViewConfiguration = lastScrollViewConfiguration else {
if let superScrollView: UIScrollView = superScrollView {
// If there was no lastScrollView and we found a current scrollView. then setting it as lastScrollView.
let configuration = IQScrollViewConfiguration(scrollView: superScrollView,
canRestoreContentOffset: true)
self.lastScrollViewConfiguration = configuration
showLog("""
Saving ScrollView New contentInset: \(configuration.startingContentInset)
and contentOffset: \(configuration.startingContentOffset)
""")
}
return
}
// If we can't find current superScrollView, then setting lastScrollView to it's original form.
if superScrollView == nil {
restoreScrollViewConfigurationIfChanged(configuration: lastConfiguration,
textInputView: textInputView)
self.lastScrollViewConfiguration = nil
} else if superScrollView != lastConfiguration.scrollView {
// If both scrollView's are different,
// then reset lastScrollView to it's original frame and setting current scrollView as last scrollView.
restoreScrollViewConfigurationIfChanged(configuration: lastConfiguration,
textInputView: textInputView)
if let superScrollView = superScrollView {
let configuration = IQScrollViewConfiguration(scrollView: superScrollView,
canRestoreContentOffset: true)
self.lastScrollViewConfiguration = configuration
showLog("""
Saving ScrollView New contentInset: \(configuration.startingContentInset)
and contentOffset: \(configuration.startingContentOffset)
""")
} else {
self.lastScrollViewConfiguration = nil
}
}
// Else the case where superScrollView == lastScrollView means we are on same scrollView
// after switching to different textInputView. So doing nothing, going ahead
}
func restoreScrollViewConfigurationIfChanged(configuration: IQScrollViewConfiguration,
textInputView: (some IQTextInputView)?) {
guard configuration.hasChanged else { return }
if configuration.scrollView.contentInset != configuration.startingContentInset {
showLog("Restoring contentInset to: \(configuration.startingContentInset)")
}
if configuration.scrollView.iq.restoreContentOffset,
!configuration.scrollView.contentOffset.equalTo(configuration.startingContentOffset) {
showLog("Restoring contentOffset to: \(configuration.startingContentOffset)")
}
activeConfiguration.animate(alongsideTransition: {
configuration.restore(for: textInputView)
})
}
// swiftlint:disable function_body_length
private func adjustScrollViewContentOffsets(moveUp: inout CGFloat, textInputView: some IQTextInputView,
lastScrollViewConfiguration: IQScrollViewConfiguration,
rootSuperview: UIView?,
layoutGuide: IQLayoutGuide,
textInputViewRectInRootSuperview: CGRect,
isScrollableTextInputView: Bool, window: UIWindow,
kbSize: CGSize, keyboardDistance: CGFloat,
rootBeginSafeAreaInsets: UIEdgeInsets) {
// Saving
var lastView: UIView = textInputView
var superScrollView: UIScrollView? = lastScrollViewConfiguration.scrollView
while let scrollView: UIScrollView = superScrollView {
var isContinue: Bool = false
if moveUp > 0 {
isContinue = moveUp > (-scrollView.contentOffset.y - scrollView.contentInset.top)
} else if let tableView: UITableView = scrollView.iq.superviewOf(type: UITableView.self) {
// Special treatment for UITableView due to their cell reusing logic
isContinue = scrollView.contentOffset.y > 0
Self.handleTableViewCase(moveUp: &moveUp, isContinue: isContinue, textInputView: textInputView,
tableView: tableView, rootSuperview: rootSuperview, layoutGuide: layoutGuide)
} else if let collectionView = scrollView.iq.superviewOf(type: UICollectionView.self) {
// Special treatment for UICollectionView due to their cell reusing logic
isContinue = scrollView.contentOffset.y > 0
Self.handleCollectionViewCase(moveUp: &moveUp, isContinue: isContinue,
textInputView: textInputView, collectionView: collectionView,
rootSuperview: rootSuperview, layoutGuide: layoutGuide)
} else {
isContinue = textInputViewRectInRootSuperview.minY < layoutGuide.top
if isContinue {
moveUp = CGFloat.minimum(0, textInputViewRectInRootSuperview.minY - layoutGuide.top)
}
}
// Looping in upper hierarchy until we don't found any scrollView
// in it's upper hierarchy till UIWindow object.
if isContinue {
var tempScrollView: UIScrollView? = scrollView.iq.superviewOf(type: UIScrollView.self)
var nextScrollView: UIScrollView?
while let view: UIScrollView = tempScrollView {
if view.isScrollEnabled, !view.iq.ignoreScrollingAdjustment {
nextScrollView = view
break
} else {
tempScrollView = view.iq.superviewOf(type: UIScrollView.self)
}
}
// Getting lastViewRect.
if let lastViewRect: CGRect = lastView.superview?.convert(lastView.frame, to: scrollView) {
// Calculating the expected Y offset from move and scrollView's contentOffset.
let minimumMovement: CGFloat = CGFloat.minimum(scrollView.contentOffset.y, -moveUp)
var suggestedOffsetY: CGFloat = scrollView.contentOffset.y - minimumMovement
// Rearranging the expected Y offset according to the view.
suggestedOffsetY = CGFloat.minimum(suggestedOffsetY, lastViewRect.minY)
updateSuggestedOffsetYAndMoveUp(suggestedOffsetY: &suggestedOffsetY, moveUp: &moveUp,
isScrollableTextInputView: isScrollableTextInputView,
nextScrollView: nextScrollView, textInputView: textInputView,
window: window, layoutGuide: layoutGuide,
scrollViewContentOffset: scrollView.contentOffset)
let newContentOffset: CGPoint = CGPoint(x: scrollView.contentOffset.x, y: suggestedOffsetY)
if !scrollView.contentOffset.equalTo(newContentOffset) {
updateScrollViewContentOffset(scrollView: scrollView, newContentOffset: newContentOffset,
moveUp: moveUp, textInputView: textInputView)
}
}
// Getting next lastView & superScrollView.
lastView = scrollView
superScrollView = nextScrollView
} else {
moveUp = 0
break
}
}
adjustScrollViewContentInset(lastScrollViewConfiguration: lastScrollViewConfiguration, window: window,
kbSize: kbSize, keyboardDistance: keyboardDistance,
rootBeginSafeAreaInsets: rootBeginSafeAreaInsets)
}
// swiftlint:enable function_body_length
private static func handleTableViewCase(moveUp: inout CGFloat, isContinue: Bool,
textInputView: some IQTextInputView, tableView: UITableView,
rootSuperview: UIView?, layoutGuide: IQLayoutGuide) {
guard isContinue,
let tableCell: UITableViewCell = textInputView.iq.superviewOf(type: UITableViewCell.self),
let indexPath: IndexPath = tableView.indexPath(for: tableCell),
let previousIndexPath: IndexPath = tableView.previousIndexPath(of: indexPath) else { return }
let previousCellRect: CGRect = tableView.rectForRow(at: previousIndexPath)
guard !previousCellRect.isEmpty else { return }
let previousCellRectInRootSuperview: CGRect = tableView.convert(previousCellRect,
to: rootSuperview)
moveUp = CGFloat.minimum(0, previousCellRectInRootSuperview.maxY - layoutGuide.top)
}
private static func handleCollectionViewCase(moveUp: inout CGFloat, isContinue: Bool,
textInputView: some IQTextInputView, collectionView: UICollectionView,
rootSuperview: UIView?,
layoutGuide: IQLayoutGuide) {
guard isContinue,
let collectionCell = textInputView.iq.superviewOf(type: UICollectionViewCell.self),
let indexPath: IndexPath = collectionView.indexPath(for: collectionCell),
let previousIndexPath: IndexPath = collectionView.previousIndexPath(of: indexPath),
let attributes = collectionView.layoutAttributesForItem(at: previousIndexPath) else { return }
let previousCellRect: CGRect = attributes.frame
guard !previousCellRect.isEmpty else { return }
let previousCellRectInRootSuperview: CGRect = collectionView.convert(previousCellRect,
to: rootSuperview)
moveUp = CGFloat.minimum(0, previousCellRectInRootSuperview.maxY - layoutGuide.top)
}
private func updateSuggestedOffsetYAndMoveUp(suggestedOffsetY: inout CGFloat, moveUp: inout CGFloat,
isScrollableTextInputView: Bool, nextScrollView: UIScrollView?,
textInputView: some IQTextInputView, window: UIWindow,
layoutGuide: IQLayoutGuide,
scrollViewContentOffset: CGPoint) {
// If is a UITextView type
// nextScrollView == nil
// If processing scrollView is last scrollView in upper hierarchy
// (there is no other scrollView in upper hierarchy.)
//
// suggestedOffsetY >= 0
// suggestedOffsetY must be >= 0 in order to keep distance from navigationBar (Bug ID: #92)
guard isScrollableTextInputView,
nextScrollView == nil,
suggestedOffsetY >= 0,
let superview: UIView = textInputView.superview else {
// Subtracting the Y offset from the move variable,
// because we are going to change scrollView's contentOffset.y to suggestedOffsetY.
moveUp -= (suggestedOffsetY-scrollViewContentOffset.y)
return
}
let currentTextInputViewRect: CGRect = superview.convert(textInputView.frame,
to: window)
// Calculating expected fix distance which needs to be managed from navigation bar
let expectedFixDistance: CGFloat = currentTextInputViewRect.minY - layoutGuide.top
// Now if expectedOffsetY (scrollView.contentOffset.y + expectedFixDistance)
// is lower than current suggestedOffsetY, which means we're in a position where
// navigationBar up and hide, then reducing suggestedOffsetY with expectedOffsetY
// (scrollView.contentOffset.y + expectedFixDistance)
let expectedOffsetY: CGFloat = scrollViewContentOffset.y + expectedFixDistance
suggestedOffsetY = CGFloat.minimum(suggestedOffsetY, expectedOffsetY)
// Setting move to 0 because now we don't want to move any view anymore
// (All will be managed by our contentInset logic.
moveUp = 0
}
func updateScrollViewContentOffset(scrollView: UIScrollView, newContentOffset: CGPoint,
moveUp: CGFloat, textInputView: some IQTextInputView) {
showLog("""
old contentOffset: \(scrollView.contentOffset)
new contentOffset: \(newContentOffset)
""")
showLog("Remaining Move: \(moveUp)")
// Getting problem while using `setContentOffset:animated:`, So I used animation API.
activeConfiguration.animate(alongsideTransition: {
// (Bug ID: #1365, #1508, #1541)
let stackView: UIStackView? = textInputView.iq.superviewOf(type: UIStackView.self,
belowView: scrollView)
// (Bug ID: #1901, #1996)
let animatedContentOffset: Bool = stackView != nil ||
scrollView is UICollectionView ||
scrollView is UITableView
if animatedContentOffset {
scrollView.setContentOffset(newContentOffset, animated: UIView.areAnimationsEnabled)
} else {
scrollView.contentOffset = newContentOffset
}
}, completion: {
if scrollView is UITableView || scrollView is UICollectionView {
// Skip reloading input views during interactive navigation gesture to prevent toolbar flash (Issue #2102)
if self.activeConfiguration.rootConfiguration?.isInteractiveGestureActive == false {
// This will update the next/previous states
textInputView.reloadInputViews()
}
}
})
}
func adjustScrollViewContentInset(lastScrollViewConfiguration: IQScrollViewConfiguration,
window: UIWindow, kbSize: CGSize, keyboardDistance: CGFloat,
rootBeginSafeAreaInsets: UIEdgeInsets) {
let lastScrollView = lastScrollViewConfiguration.scrollView
guard let lastScrollViewRect: CGRect = lastScrollView.superview?.convert(lastScrollView.frame, to: window),
!lastScrollView.iq.ignoreContentInsetAdjustment else { return }
// Updating contentInset
var bottomInset: CGFloat = (kbSize.height)-(window.frame.height-lastScrollViewRect.maxY)
let keyboardAndSafeArea: CGFloat = keyboardDistance + rootBeginSafeAreaInsets.bottom
var bottomScrollIndicatorInset: CGFloat = bottomInset - keyboardAndSafeArea
// Update the insets so that the scrollView doesn't shift incorrectly
// when the offset is near the bottom of the scroll view.
bottomInset = CGFloat.maximum(lastScrollViewConfiguration.startingContentInset.bottom, bottomInset)
let startingScrollInset: UIEdgeInsets = lastScrollViewConfiguration.startingScrollIndicatorInsets
bottomScrollIndicatorInset = CGFloat.maximum(startingScrollInset.bottom,
bottomScrollIndicatorInset)
bottomInset -= lastScrollView.safeAreaInsets.bottom
bottomScrollIndicatorInset -= lastScrollView.safeAreaInsets.bottom
var movedInsets: UIEdgeInsets = lastScrollView.contentInset
movedInsets.bottom = bottomInset
guard lastScrollView.contentInset != movedInsets else { return }
showLog("""
old ContentInset: \(lastScrollView.contentInset) new ContentInset: \(movedInsets)
""")
activeConfiguration.animate(alongsideTransition: {
lastScrollView.contentInset = movedInsets
lastScrollView.layoutIfNeeded() // (Bug ID: #1996)
var newScrollIndicatorInset: UIEdgeInsets = lastScrollView.verticalScrollIndicatorInsets
newScrollIndicatorInset.bottom = bottomScrollIndicatorInset
lastScrollView.scrollIndicatorInsets = newScrollIndicatorInset
})
}
private func adjustTextInputViewContentInset(window: UIWindow, originalKbSize: CGSize,
rootSuperview: UIView?,
layoutGuide: IQLayoutGuide,
textInputView: UIScrollView) {
let keyboardYPosition: CGFloat = window.frame.height - originalKbSize.height
var rootSuperViewFrameInWindow: CGRect = window.frame
if let rootSuperview: UIView = rootSuperview {
rootSuperViewFrameInWindow = rootSuperview.convert(rootSuperview.bounds, to: window)
}
let keyboardOverlapping: CGFloat = rootSuperViewFrameInWindow.maxY - keyboardYPosition
let availableHeight: CGFloat = rootSuperViewFrameInWindow.height-layoutGuide.top-keyboardOverlapping
let textInputViewHeight: CGFloat = CGFloat.minimum(textInputView.frame.height, availableHeight)
guard textInputView.frame.size.height-textInputView.contentInset.bottom>textInputViewHeight else { return }
// If frame is not change by library in past, then saving user textInputView properties (Bug ID: #92)
if startingTextViewConfiguration == nil {
startingTextViewConfiguration = IQScrollViewConfiguration(scrollView: textInputView,
canRestoreContentOffset: false)
}
var newContentInset: UIEdgeInsets = textInputView.contentInset
newContentInset.bottom = textInputView.frame.size.height-textInputViewHeight
newContentInset.bottom -= textInputView.safeAreaInsets.bottom
guard textInputView.contentInset != newContentInset else { return }
showLog("""
\(textInputView) Old textInputView.contentInset: \(textInputView.contentInset)
New textInputView.contentInset: \(newContentInset)
""")
activeConfiguration.animate(alongsideTransition: {
textInputView.contentInset = newContentInset
textInputView.layoutIfNeeded() // (Bug ID: #1996)
textInputView.scrollIndicatorInsets = newContentInset
})
}
func adjustRootController(moveUp: CGFloat, rootViewOrigin: CGPoint, originalKbSize: CGSize,
rootController: UIViewController, rootBeginOrigin: CGPoint) {
// +Positive or zero.
var rootViewOrigin: CGPoint = rootViewOrigin
if moveUp >= 0 {
rootViewOrigin.y = CGFloat.maximum(rootViewOrigin.y - moveUp, CGFloat.minimum(0, -originalKbSize.height))
if !rootController.view.frame.origin.equalTo(rootViewOrigin) {
showLog("Moving Upward")
activeConfiguration.animate(alongsideTransition: {
var rect: CGRect = rootController.view.frame
rect.origin = rootViewOrigin
rootController.view.frame = rect
// Animating content if needed (Bug ID: #204)
if self.layoutIfNeededOnUpdate {
// Animating content (Bug ID: #160)
rootController.view.setNeedsLayout()
rootController.view.layoutIfNeeded()
}
let classNameString: String = "\(type(of: rootController.self))"
self.showLog("Set \(classNameString) origin to: \(rootViewOrigin)")
})
}
movedDistance = rootBeginOrigin.y-rootViewOrigin.y
} else { // -Negative
let disturbDistance: CGFloat = rootViewOrigin.y-rootBeginOrigin.y
// disturbDistance Negative = frame disturbed.
// disturbDistance positive = frame not disturbed.
if disturbDistance <= 0 {
rootViewOrigin.y -= CGFloat.maximum(moveUp, disturbDistance)
if !rootController.view.frame.origin.equalTo(rootViewOrigin) {
showLog("Moving Downward")
// Setting adjusted rootViewRect
activeConfiguration.animate(alongsideTransition: {
var rect: CGRect = rootController.view.frame
rect.origin = rootViewOrigin
rootController.view.frame = rect
// Animating content if needed (Bug ID: #204)
if self.layoutIfNeededOnUpdate {
// Animating content (Bug ID: #160)
rootController.view.setNeedsLayout()
rootController.view.layoutIfNeeded()
}
let classNameString: String = "\(type(of: rootController.self))"
self.showLog("Set \(classNameString) origin to: \(rootViewOrigin)")
})
}
movedDistance = rootBeginOrigin.y-rootViewOrigin.y
}
}
}
func restoreScrollViewContentOffset(superScrollView: UIScrollView, textInputView: (some IQTextInputView)?) {
var superScrollView: UIScrollView? = superScrollView
while let scrollView: UIScrollView = superScrollView {
let width: CGFloat = CGFloat.maximum(scrollView.contentSize.width, scrollView.frame.width)
let height: CGFloat = CGFloat.maximum(scrollView.contentSize.height, scrollView.frame.height)
let contentSize: CGSize = CGSize(width: width, height: height)
let minimumY: CGFloat = contentSize.height - scrollView.frame.height
if minimumY < scrollView.contentOffset.y {
let newContentOffset: CGPoint = CGPoint(x: scrollView.contentOffset.x, y: minimumY)
if !scrollView.contentOffset.equalTo(newContentOffset) {
// (Bug ID: #1365, #1508, #1541)
let stackView: UIStackView?
if let textInputView: UIView = textInputView {
stackView = textInputView.iq.superviewOf(type: UIStackView.self,
belowView: scrollView)
} else {
stackView = nil
}
// (Bug ID: #1901, #1996)
let animatedContentOffset: Bool = stackView != nil ||
scrollView is UICollectionView ||
scrollView is UITableView
if animatedContentOffset {
scrollView.setContentOffset(newContentOffset, animated: UIView.areAnimationsEnabled)
} else {
scrollView.contentOffset = newContentOffset
}
showLog("Restoring contentOffset to: \(newContentOffset)")
}
}
superScrollView = scrollView.iq.superviewOf(type: UIScrollView.self)
}
}
}
// swiftlint:enable function_parameter_count

View File

@ -0,0 +1,238 @@
//
// IQKeyboardManager.swift
// https://github.com/hackiftekhar/IQKeyboardManager
// Copyright (c) 2013-24 Iftekhar Qurashi.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import UIKit
/**
IQKeyboardManager is a code-less drop-in universal library that automatically prevents issues
of the keyboard sliding up and covering UITextField/UITextView.
## Usage
Simply enable the manager in your AppDelegate:
```swift
IQKeyboardManager.shared.isEnabled = true
```
## Thread Safety
All public APIs must be called from the main thread. The class is marked with @MainActor
to enforce this at compile time.
## Example
```swift
import IQKeyboardManagerSwift
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
IQKeyboardManager.shared.isEnabled = true
return true
}
}
```
*/
@available(iOSApplicationExtension, unavailable)
@MainActor
@objcMembers public final class IQKeyboardManager: NSObject {
/**
Returns the default singleton instance.
*/
@MainActor
public static let shared: IQKeyboardManager = .init()
internal var activeConfiguration: IQActiveConfiguration = .init()
// MARK: UIKeyboard handling
/**
Enable/disable managing distance between keyboard and textInputView.
Default is YES(Enabled when class loads in `+(void)load` method).
*/
public var isEnabled: Bool = false {
didSet {
guard isEnabled != oldValue else { return }
// If not enable, enable it.
if isEnabled {
// If keyboard is currently showing.
if activeConfiguration.keyboardInfo.isVisible {
adjustPosition()
} else {
restorePosition()
}
showLog("Enabled")
} else { // If not disable, disable it.
restorePosition()
showLog("Disabled")
}
}
}
/**
Sets the default distance between the keyboard and the active text input view.
This distance is applied to all text inputs unless overridden by setting
`view.iq.distanceFromKeyboard` for specific views.
- Precondition: Value must be non-negative. Negative values will be logged as warnings.
- Default: `10.0` points
- Note: This is a global setting. Use `view.iq.distanceFromKeyboard` for per-view customization.
## Example
```swift
// Set global distance
IQKeyboardManager.shared.keyboardDistance = 20.0
// Override for specific text field
myTextField.iq.distanceFromKeyboard = 30.0
```
- SeeAlso: `UIView.iq.distanceFromKeyboard` for per-view customization
*/
public var keyboardDistance: CGFloat = 10.0 {
didSet {
if keyboardDistance < 0 {
showLog("⚠️ keyboardDistance shouldn't be negative.")
}
}
}
/*******************************************/
// MARK: UIAnimation handling
/**
If YES, then calls 'setNeedsLayout' and 'layoutIfNeeded' on any frame update of to viewController's view.
*/
public var layoutIfNeededOnUpdate: Bool = false
// MARK: Class Level disabling methods
/**
Classes that should have keyboard distance handling disabled.
When a view controller is of one of these types, keyboard distance handling
is disabled regardless of the `isEnabled` property.
- Precondition: All classes must be subclasses of `UIViewController`
- Default: `[UITableViewController.self, UIInputViewController.self, UIAlertController.self]`
- Note: This takes precedence over `enabledDistanceHandlingClasses`. If a class appears
in both arrays, it will be disabled.
## Example
```swift
// Disable for custom view controller
IQKeyboardManager.shared.disabledDistanceHandlingClasses.append(MyCustomViewController.self)
// Disable for multiple controllers
IQKeyboardManager.shared.disabledDistanceHandlingClasses += [
LoginViewController.self,
SignupViewController.self
]
```
- SeeAlso: `enabledDistanceHandlingClasses` for force-enabling specific classes
*/
public var disabledDistanceHandlingClasses: [UIViewController.Type] = [
UITableViewController.self,
UIInputViewController.self,
UIAlertController.self
]
/**
Enable distance handling within the scope of enabled distance handling viewControllers classes.
Within this scope, 'enabled' property is ignored. Class should be kind of UIViewController.
If same Class is added in disabledDistanceHandlingClasses list,
then enabledDistanceHandlingClasses will be ignored.
*/
public var enabledDistanceHandlingClasses: [UIViewController.Type] = []
/**************************************************************************************/
// MARK: Initialization/De-initialization
/* Singleton Object Initialization. */
private override init() {
super.init()
self.addActiveConfigurationObserver()
NotificationCenter.default.addObserver(self, selector: #selector(applicationDidBecomeActive(_:)),
name: UIApplication.didBecomeActiveNotification, object: nil)
}
deinit {
// Disable the keyboard manager.
isEnabled = false
NotificationCenter.default.removeObserver(self)
}
// MARK: Public Methods
/**
Manually triggers a position adjustment for the active text input view.
Call this method when you've made external changes to the view hierarchy that
might affect keyboard positioning (e.g., programmatically changing view frames,
adding/removing views, changing constraints).
This method is safe to call even when no text input is active or the keyboard
is hidden - it will simply return early.
## When to Call
- After programmatically modifying view frames
- After adding or removing views from the hierarchy
- After changing Auto Layout constraints that affect the active text field's position
- When orientation changes occur outside the normal notification flow
## Example
```swift
// After programmatic layout changes
myView.frame = newFrame
IQKeyboardManager.shared.reloadLayoutIfNeeded()
// After constraint updates
NSLayoutConstraint.activate(newConstraints)
view.layoutIfNeeded()
IQKeyboardManager.shared.reloadLayoutIfNeeded()
```
- Note: This method only has an effect when:
- `isEnabled` is `true`
- A text input view is currently active
- The keyboard is visible
- The root configuration is ready
- SeeAlso: `isEnabled` for enabling/disabling the manager
*/
public func reloadLayoutIfNeeded() {
guard privateIsEnabled(),
activeConfiguration.keyboardInfo.isVisible,
activeConfiguration.isReady else {
return
}
adjustPosition()
}
}

View File

@ -0,0 +1,118 @@
//
// UIScrollView+IQKeyboardManagerExtension.swift
// https://github.com/hackiftekhar/IQKeyboardManager
// Copyright (c) 2013-24 Iftekhar Qurashi.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import UIKit
import IQKeyboardCore
@available(iOSApplicationExtension, unavailable)
@MainActor
private struct AssociatedKeys {
static var ignoreScrollingAdjustment: Int = 0
static var ignoreContentInsetAdjustment: Int = 0
static var restoreContentOffset: Int = 0
}
@available(iOSApplicationExtension, unavailable)
@MainActor
public extension IQKeyboardExtension where Base: UIScrollView {
/**
If YES, then scrollview will ignore scrolling (simply not scroll it) for adjusting textInputView position.
Default is NO.
*/
var ignoreScrollingAdjustment: Bool {
get {
if let base = base {
return objc_getAssociatedObject(base, &AssociatedKeys.ignoreScrollingAdjustment) as? Bool ?? false
}
return false
}
set(newValue) {
if let base = base {
objc_setAssociatedObject(base, &AssociatedKeys.ignoreScrollingAdjustment,
newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
}
/**
If YES, then scrollview will ignore content inset adjustment (simply not updating it) when keyboard is shown.
Default is NO.
*/
var ignoreContentInsetAdjustment: Bool {
get {
if let base = base {
return objc_getAssociatedObject(base, &AssociatedKeys.ignoreContentInsetAdjustment) as? Bool ?? false
}
return false
}
set(newValue) {
if let base = base {
objc_setAssociatedObject(base, &AssociatedKeys.ignoreContentInsetAdjustment,
newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
}
/**
If we should restore scrollView contentOffset to it's initial position
*/
var restoreContentOffset: Bool {
get {
if let base = base {
return objc_getAssociatedObject(base, &AssociatedKeys.restoreContentOffset) as? Bool ?? false
}
return false
}
set(newValue) {
if let base = base {
objc_setAssociatedObject(base, &AssociatedKeys.restoreContentOffset,
newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
}
}
// swiftlint:disable unused_setter_value
@available(iOSApplicationExtension, unavailable)
@MainActor
@objc public extension UIScrollView {
@available(*, unavailable, renamed: "iq.ignoreScrollingAdjustment")
var shouldIgnoreScrollingAdjustment: Bool {
get { false }
set { }
}
@available(*, unavailable, renamed: "iq.ignoreContentInsetAdjustment")
var shouldIgnoreContentInsetAdjustment: Bool {
get { false }
set { }
}
@available(*, unavailable, renamed: "iq.restoreContentOffset")
var shouldRestoreScrollViewContentOffset: Bool {
get { false }
set { }
}
}
// swiftlint:enable unused_setter_value

View File

@ -0,0 +1,46 @@
//
// UIScrollView+IQKeyboardManagerExtensionObjc.swift
// https://github.com/hackiftekhar/IQKeyboardManager
// Copyright (c) 2013-24 Iftekhar Qurashi.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import UIKit
// MARK: For ObjectiveC Compatibility
// swiftlint:disable identifier_name
@objc public extension UIScrollView {
var iq_ignoreScrollingAdjustment: Bool {
get { iq.ignoreScrollingAdjustment }
set { iq.ignoreScrollingAdjustment = newValue }
}
var iq_ignoreContentInsetAdjustment: Bool {
get { iq.ignoreContentInsetAdjustment }
set { iq.ignoreContentInsetAdjustment = newValue }
}
var iq_restoreContentOffset: Bool {
get { iq.restoreContentOffset }
set { iq.restoreContentOffset = newValue }
}
}
// swiftlint:enable identifier_name

View File

@ -0,0 +1,158 @@
//
// UIView+IQKeyboardManagerExtension.swift
// https://github.com/hackiftekhar/IQKeyboardManager
// Copyright (c) 2013-24 Iftekhar Qurashi.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import UIKit
import IQKeyboardCore
@available(iOSApplicationExtension, unavailable)
@MainActor
private struct AssociatedKeys {
static var distanceFromKeyboard: Int = 0
static var enableMode: Int = 0
}
@available(iOSApplicationExtension, unavailable)
@MainActor
@objc public extension UIView {
nonisolated static let defaultKeyboardDistance: CGFloat = .greatestFiniteMagnitude
}
@available(iOSApplicationExtension, unavailable)
@available(*, unavailable, renamed: "UIView.defaultKeyboardDistance")
nonisolated public let kIQUseDefaultKeyboardDistance: CGFloat = .greatestFiniteMagnitude
/**
Extension providing keyboard management functionality to UIView instances.
This extension allows per-view configuration of keyboard behavior, including
custom distance and enable/disable modes.
## Usage
```swift
// Set custom distance for a text field
textField.iq.distanceFromKeyboard = 30.0
// Disable keyboard management for a specific text field
textField.iq.enableMode = .disabled
```
*/
@available(iOSApplicationExtension, unavailable)
@MainActor
public extension IQKeyboardExtension where Base: IQTextInputView {
/**
Custom distance from keyboard for this specific text input view.
If set to `UIView.defaultKeyboardDistance`, the global `keyboardDistance`
value from `IQKeyboardManager.shared.keyboardDistance` will be used.
Otherwise, this value takes precedence.
- Default: `UIView.defaultKeyboardDistance` (uses global setting)
- Note: Value cannot be negative. Negative values may cause unexpected behavior.
## Example
```swift
// Use global setting
textField.iq.distanceFromKeyboard = UIView.defaultKeyboardDistance
// Use custom distance
textField.iq.distanceFromKeyboard = 50.0
```
*/
var distanceFromKeyboard: CGFloat {
get {
if let base = base {
if let value = objc_getAssociatedObject(base, &AssociatedKeys.distanceFromKeyboard) as? CGFloat {
return value
}
}
return UIView.defaultKeyboardDistance
}
set(newValue) {
if let base = base {
objc_setAssociatedObject(base, &AssociatedKeys.distanceFromKeyboard,
newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
if newValue < 0 {
IQKeyboardManager.shared.showLog("Warning: distanceFromKeyboard shouldn't be negative.")
}
}
}
/**
Enable mode for this specific text input view.
Controls whether keyboard management is enabled, disabled, or uses the default
global setting for this view.
- `.default`: Use global `IQKeyboardManager.shared.isEnabled` setting
- `.enabled`: Force enable keyboard management for this view
- `.disabled`: Force disable keyboard management for this view
## Example
```swift
// Use global setting
textField.iq.enableMode = .default
// Always enable for this field
textField.iq.enableMode = .enabled
// Always disable for this field
textField.iq.enableMode = .disabled
```
- SeeAlso: `IQKeyboardManager.isEnabled` for global control
*/
var enableMode: IQEnableMode {
get {
if let base = base {
return objc_getAssociatedObject(base, &AssociatedKeys.enableMode) as? IQEnableMode ?? .default
}
return .default
}
set(newValue) {
if let base = base {
objc_setAssociatedObject(base, &AssociatedKeys.enableMode, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
}
}
// swiftlint:disable unused_setter_value
@available(iOSApplicationExtension, unavailable)
@MainActor
@objc public extension UIView {
@available(*, unavailable, renamed: "iq.distanceFromKeyboard")
var keyboardDistanceFromTextField: CGFloat {
get { 0 }
set { }
}
@available(*, unavailable, renamed: "iq.enableMode")
var enableMode: IQEnableMode {
get { .default }
set { }
}
}
// swiftlint:enable unused_setter_value

View File

@ -0,0 +1,55 @@
//
// UIView+IQKeyboardManagerExtensionObjc.swift
// https://github.com/hackiftekhar/IQKeyboardManager
// Copyright (c) 2013-24 Iftekhar Qurashi.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import UIKit
import IQKeyboardCore
// MARK: For ObjectiveC Compatibility
// swiftlint:disable identifier_name
@objc public extension UITextField {
var iq_distanceFromKeyboard: CGFloat {
get { iq.distanceFromKeyboard }
set { iq.distanceFromKeyboard = newValue }
}
var iq_enableMode: IQEnableMode {
get { iq.enableMode }
set { iq.enableMode = newValue }
}
}
@objc public extension UITextView {
var iq_distanceFromKeyboard: CGFloat {
get { iq.distanceFromKeyboard }
set { iq.distanceFromKeyboard = newValue }
}
var iq_enableMode: IQEnableMode {
get { iq.enableMode }
set { iq.enableMode = newValue }
}
}
// swiftlint:enable identifier_name

View File

@ -0,0 +1,45 @@
//
// UICollectionView+IndexPaths.swift
// https://github.com/hackiftekhar/IQKeyboardManager
// Copyright (c) 2013-24 Iftekhar Qurashi.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import UIKit
@available(iOSApplicationExtension, unavailable)
@MainActor
internal extension UICollectionView {
func previousIndexPath(of indexPath: IndexPath) -> IndexPath? {
var previousRow: Int = indexPath.row - 1
var previousSection: Int = indexPath.section
// Fixing indexPath
if previousRow < 0 {
previousSection -= 1
if previousSection >= 0 {
previousRow = self.numberOfItems(inSection: previousSection) - 1
}
}
guard previousRow >= 0, previousSection >= 0 else { return nil }
return IndexPath(item: previousRow, section: previousSection)
}
}

View File

@ -0,0 +1,46 @@
//
// UITableView+IndexPaths.swift
// https://github.com/hackiftekhar/IQKeyboardManager
// Copyright (c) 2013-24 Iftekhar Qurashi.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import UIKit
@available(iOSApplicationExtension, unavailable)
@MainActor
internal extension UITableView {
func previousIndexPath(of indexPath: IndexPath) -> IndexPath? {
var previousRow: Int = indexPath.row - 1
var previousSection: Int = indexPath.section
// Fixing indexPath
if previousRow < 0 {
previousSection -= 1
if previousSection >= 0 {
let rowCount = self.numberOfRows(inSection: previousSection)
previousRow = rowCount > 0 ? rowCount - 1 : -1
}
}
guard previousRow >= 0, previousSection >= 0 else { return nil }
return IndexPath(row: previousRow, section: previousSection)
}
}

View File

@ -0,0 +1,99 @@
//
// UIView+Parent.swift
// https://github.com/hackiftekhar/IQKeyboardManager
// Copyright (c) 2013-24 Iftekhar Qurashi.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import UIKit
import IQKeyboardCore
/**
UIView hierarchy category.
*/
@available(iOSApplicationExtension, unavailable)
@MainActor
public extension IQKeyboardExtension where Base: UIView {
/**
Returns the UIViewController object that is actually the parent of this object.
Most of the time it's the viewController object which actually contains it,
but result may be different if it's viewController is added as childViewController of another viewController.
*/
func parentContainerViewController() -> UIViewController? {
var matchController: UIViewController? = viewContainingController()
var parentContainerViewController: UIViewController?
if var navController: UINavigationController = matchController?.navigationController {
while let parentNav: UINavigationController = navController.navigationController {
navController = parentNav
}
var parentController: UIViewController = navController
while let parent: UIViewController = parentController.parent,
!(parent is UINavigationController) &&
!(parent is UITabBarController) &&
!(parent is UISplitViewController) {
parentController = parent
}
if navController == parentController {
parentContainerViewController = navController.topViewController
} else {
parentContainerViewController = parentController
}
} else if let tabController: UITabBarController = matchController?.tabBarController {
let selectedController = tabController.selectedViewController
if let navController: UINavigationController = selectedController as? UINavigationController {
parentContainerViewController = navController.topViewController
} else {
parentContainerViewController = tabController.selectedViewController
}
} else {
while let parent: UIViewController = matchController?.parent,
!(parent is UINavigationController) &&
!(parent is UITabBarController) &&
!(parent is UISplitViewController) {
matchController = parent
}
parentContainerViewController = matchController
}
if let controller: UIViewController = parentContainerViewController?.iq_parentContainerViewController() {
return controller
} else {
return parentContainerViewController
}
}
}
@available(iOSApplicationExtension, unavailable)
@MainActor
@objc public extension UIView {
@available(*, unavailable, renamed: "iq.parentContainerViewController()")
func parentContainerViewController() -> UIViewController? { nil }
}

View File

@ -0,0 +1,31 @@
//
// UIView+ParentObjc.swift
// https://github.com/hackiftekhar/IQKeyboardManager
// Copyright (c) 2013-24 Iftekhar Qurashi.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import UIKit
@objc public extension UIView {
func iq_parentContainerViewController() -> UIViewController? {
iq.parentContainerViewController()
}
}

View File

@ -0,0 +1,49 @@
//
// UIViewController+ParentContainer.swift
// https://github.com/hackiftekhar/IQKeyboardManager
// Copyright (c) 2013-24 Iftekhar Qurashi.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import UIKit
@available(iOSApplicationExtension, unavailable)
@MainActor
@objc extension UIViewController {
/**
This method is provided to override by viewController's
if the library lifts a viewController which you doesn't want to lift.
This may happen if you have implemented side menu feature
in your app and the library try to lift the side menu controller.
Overriding this method in side menu class to return correct controller should fix the problem.
*/
open func iq_parentContainerViewController() -> UIViewController? {
return self
}
}
@available(iOSApplicationExtension, unavailable)
@MainActor
@objc extension UIViewController {
@available(*, unavailable, renamed: "iq_parentContainerViewController()")
open func parentIQContainerViewController() -> UIViewController? {
return self
}
}

View File

@ -0,0 +1,137 @@
//
// IQKeyboardManager+ToolbarManager.swift
// https://github.com/hackiftekhar/IQKeyboardManager
// Copyright (c) 2013-24 Iftekhar Qurashi.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import UIKit
import IQKeyboardToolbarManager
@available(iOSApplicationExtension, unavailable)
// swiftlint:disable line_length
@available(*, deprecated,
message: "Please use `IQKeyboardToolbarManager` independently from https://github.com/hackiftekhar/IQKeyboardToolbarManager")
// swiftlint:enable line_length
@MainActor
@objc public extension IQKeyboardManager {
@MainActor
private struct AssociatedKeys {
static var toolbarManager: Int = 0
}
internal var toolbarManager: IQKeyboardToolbarManager {
IQKeyboardToolbarManager.shared
}
var enableToolbarDebugging: Bool {
get { toolbarManager.isDebuggingEnabled }
set { toolbarManager.isDebuggingEnabled = newValue }
}
/**
Automatic add the toolbar functionality. Default is YES.
*/
var enableAutoToolbar: Bool {
get { toolbarManager.isEnabled }
set { toolbarManager.isEnabled = newValue }
}
/**
Configurations related to the toolbar display over the keyboard.
*/
var toolbarConfiguration: IQKeyboardToolbarConfiguration {
toolbarManager.toolbarConfiguration
}
// MARK: UISound handling
/**
If YES, then it plays inputClick sound on next/previous/done click.
*/
var playInputClicks: Bool {
get { toolbarManager.playInputClicks }
set { toolbarManager.playInputClicks = newValue }
}
/**
Disable automatic toolbar creation within the scope of disabled toolbar viewControllers classes.
Within this scope, 'enableAutoToolbar' property is ignored. Class should be kind of UIViewController.
*/
var disabledToolbarClasses: [UIViewController.Type] {
get { toolbarManager.disabledToolbarClasses }
set { toolbarManager.disabledToolbarClasses = newValue }
}
/**
Enable automatic toolbar creation within the scope of enabled toolbar viewControllers classes.
Within this scope, 'enableAutoToolbar' property is ignored. Class should be kind of UIViewController.
If same Class is added in disabledToolbarClasses list, then enabledToolbarClasses will be ignore.
*/
var enabledToolbarClasses: [UIViewController.Type] {
get { toolbarManager.enabledToolbarClasses }
set { toolbarManager.enabledToolbarClasses = newValue }
}
/**
Allowed subclasses of UIView to add all inner textField,
this will allow to navigate between textField contains in different superview.
Class should be kind of UIView.
*/
var deepResponderAllowedContainerClasses: [UIView.Type] {
get { toolbarManager.deepResponderAllowedContainerClasses }
set { toolbarManager.deepResponderAllowedContainerClasses = newValue }
}
/** reloadInputViews to reload toolbar buttons enable/disable state on the fly Enhancement ID #434. */
func reloadInputViews() {
toolbarManager.reloadInputViews()
}
/**
Returns YES if can navigate to previous responder textInputView, otherwise NO.
*/
var canGoPrevious: Bool {
toolbarManager.canGoPrevious
}
/**
Returns YES if can navigate to next responder textInputViews, otherwise NO.
*/
var canGoNext: Bool {
toolbarManager.canGoNext
}
/**
Navigate to previous responder textInputViews
*/
@discardableResult
func goPrevious() -> Bool {
toolbarManager.goPrevious()
}
/**
Navigate to next responder textInputView.
*/
@discardableResult
func goNext() -> Bool {
toolbarManager.goNext()
}
}

View File

@ -0,0 +1,177 @@
//
// IQKeyboardManager+ToolbarManagerDeprecated.swift
// https://github.com/hackiftekhar/IQKeyboardManager
// Copyright (c) 2013-24 Iftekhar Qurashi.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import UIKit
import IQKeyboardToolbarManager
// swiftlint:disable unused_setter_value
// swiftlint:disable identifier_name
@available(iOSApplicationExtension, unavailable)
@MainActor
@objc public extension IQKeyboardManager {
@available(*, unavailable, renamed: "playInputClicks")
var shouldPlayInputClicks: Bool {
get { false }
set { }
}
}
@available(iOSApplicationExtension, unavailable)
@MainActor
@objc public extension IQKeyboardManager {
@available(*, unavailable, renamed: "toolbarConfiguration.manageBehavior")
var toolbarManageBehaviour: IQKeyboardToolbarManageBehavior {
get { .bySubviews }
set { }
}
@available(*, unavailable, renamed: "toolbarConfiguration.useTextInputViewTintColor")
var shouldToolbarUsesTextFieldTintColor: Bool {
get { false }
set { }
}
@available(*, unavailable, renamed: "toolbarConfiguration.tintColor")
var toolbarTintColor: UIColor? {
get { nil }
set { }
}
@available(*, unavailable, renamed: "toolbarConfiguration.barTintColor")
var toolbarBarTintColor: UIColor? {
get { nil }
set { }
}
@available(*, unavailable, renamed: "toolbarConfiguration.previousNextDisplayMode")
var previousNextDisplayMode: IQPreviousNextDisplayMode {
get { .default }
set { }
}
@available(*, unavailable, renamed: "deepResponderAllowedContainerClasses")
var toolbarPreviousNextAllowedClasses: [UIView.Type] {
get { [] }
set { }
}
}
@available(iOSApplicationExtension, unavailable)
@MainActor
@objc public extension IQKeyboardManager {
@available(*, unavailable, renamed: "toolbarConfiguration.previousBarButtonConfiguration.image",
message: "To change, please assign a new toolbarConfiguration.previousBarButtonConfiguration")
var toolbarPreviousBarButtonItemImage: UIImage? {
get { nil }
set { }
}
@available(*, unavailable, renamed: "toolbarConfiguration.previousBarButtonConfiguration.title",
message: "To change, please assign a new toolbarConfiguration.previousBarButtonConfiguration")
var toolbarPreviousBarButtonItemText: String? {
get { nil }
set { }
}
@available(*, unavailable, renamed: "toolbarConfiguration.previousBarButtonConfiguration.accessibilityLabel",
message: "To change, please assign a new toolbarConfiguration.previousBarButtonConfiguration")
var toolbarPreviousBarButtonItemAccessibilityLabel: String? {
get { nil }
set { }
}
@available(*, unavailable, renamed: "toolbarConfiguration.nextBarButtonConfiguration.image",
message: "To change, please assign a new toolbarConfiguration.nextBarButtonConfiguration")
var toolbarNextBarButtonItemImage: UIImage? {
get { nil }
set { }
}
@available(*, unavailable, renamed: "toolbarConfiguration.nextBarButtonConfiguration.title",
message: "To change, please assign a new toolbarConfiguration.nextBarButtonConfiguration")
var toolbarNextBarButtonItemText: String? {
get { nil }
set { }
}
@available(*, unavailable, renamed: "toolbarConfiguration.nextBarButtonConfiguration.accessibilityLabel",
message: "To change, please assign a new toolbarConfiguration.nextBarButtonConfiguration")
var toolbarNextBarButtonItemAccessibilityLabel: String? {
get { nil }
set { }
}
@available(*, unavailable, renamed: "toolbarConfiguration.doneBarButtonConfiguration.image",
message: "To change, please assign a new toolbarConfiguration.doneBarButtonConfiguration")
var toolbarDoneBarButtonItemImage: UIImage? {
get { nil }
set { }
}
@available(*, unavailable, renamed: "toolbarConfiguration.doneBarButtonConfiguration.title",
message: "To change, please assign a new toolbarConfiguration.doneBarButtonConfiguration")
var toolbarDoneBarButtonItemText: String? {
get { nil }
set { }
}
@available(*, unavailable, renamed: "toolbarConfiguration.doneBarButtonConfiguration.accessibilityLabel",
message: "To change, please assign a new toolbarConfiguration.doneBarButtonConfiguration")
var toolbarDoneBarButtonItemAccessibilityLabel: String? {
get { nil }
set { }
}
}
@available(iOSApplicationExtension, unavailable)
@MainActor
@objc public extension IQKeyboardManager {
@available(*, unavailable, renamed: "toolbarConfiguration.placeholderConfiguration.accessibilityLabel")
var toolbarTitleBarButtonItemAccessibilityLabel: String? {
get { nil }
set { }
}
@available(*, unavailable, renamed: "toolbarConfiguration.placeholderConfiguration.showPlaceholder")
var shouldShowToolbarPlaceholder: Bool {
get { false }
set { }
}
@available(*, unavailable, renamed: "toolbarConfiguration.placeholderConfiguration.font")
var placeholderFont: UIFont? {
get { nil }
set { }
}
@available(*, unavailable, renamed: "toolbarConfiguration.placeholderConfiguration.color")
var placeholderColor: UIColor? {
get { nil }
set { }
}
@available(*, unavailable, renamed: "toolbarConfiguration.placeholderConfiguration.buttonColor")
var placeholderButtonColor: UIColor? {
get { nil }
set { }
}
}
// swiftlint:enable unused_setter_value
// swiftlint:enable identifier_name

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyAccessedAPITypes</key>
<array/>
<key>NSPrivacyTrackingDomains</key>
<array/>
<key>NSPrivacyCollectedDataTypes</key>
<array/>
<key>NSPrivacyTracking</key>
<false/>
</dict>
</plist>

View File

@ -0,0 +1,100 @@
//
// IQKeyboardManager+Resign.swift
// https://github.com/hackiftekhar/IQKeyboardManager
// Copyright (c) 2013-24 Iftekhar Qurashi.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import UIKit
@available(iOSApplicationExtension, unavailable)
@MainActor
@objc public extension IQKeyboardManager {
@MainActor
private struct AssociatedKeys {
static var resignHandler: Int = 0
}
internal var resignHandler: IQKeyboardResignHandler {
if let object = objc_getAssociatedObject(self, &AssociatedKeys.resignHandler)
as? IQKeyboardResignHandler {
return object
}
let object: IQKeyboardResignHandler = .init()
objc_setAssociatedObject(self, &AssociatedKeys.resignHandler,
object, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
return object
}
/**
Resigns Keyboard on touching outside TextInputView. Default is NO.
*/
var resignOnTouchOutside: Bool {
get { resignHandler.resignOnTouchOutside }
set { resignHandler.resignOnTouchOutside = newValue }
}
/** TapGesture to resign keyboard on view's touch.
It's a readonly property and exposed only for adding/removing dependencies
if your added gesture does have collision with this one
*/
var resignGesture: UITapGestureRecognizer {
get { resignHandler.resignGesture }
set { resignHandler.resignGesture = newValue }
}
/**
Disabled classes to ignore resignOnTouchOutside' property, Class should be kind of UIViewController.
*/
var disabledTouchResignedClasses: [UIViewController.Type] {
get { resignHandler.disabledTouchResignedClasses }
set { resignHandler.disabledTouchResignedClasses = newValue }
}
/**
Enabled classes to forcefully enable 'resignOnTouchOutside' property.
Class should be kind of UIViewController
. If same Class is added in disabledTouchResignedClasses list, then enabledTouchResignedClasses will be ignored.
*/
var enabledTouchResignedClasses: [UIViewController.Type] {
get { resignHandler.enabledTouchResignedClasses }
set { resignHandler.enabledTouchResignedClasses = newValue }
}
/**
if resignOnTouchOutside is enabled then you can customize the behavior
to not recognize gesture touches on some specific view subclasses.
Class should be kind of UIView. Default is [UIControl, UINavigationBar]
*/
var touchResignedGestureIgnoreClasses: [UIView.Type] {
get { resignHandler.touchResignedGestureIgnoreClasses }
set { resignHandler.touchResignedGestureIgnoreClasses = newValue }
}
/**
Resigns currently first responder field.
*/
@discardableResult
func resignFirstResponder() -> Bool {
resignHandler.resignFirstResponder()
}
}

View File

@ -0,0 +1,37 @@
//
// IQKeyboardManager+Resign_Deprecated.swift
// https://github.com/hackiftekhar/IQKeyboardManager
// Copyright (c) 2013-24 Iftekhar Qurashi.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import UIKit
// swiftlint:disable unused_setter_value
@available(iOSApplicationExtension, unavailable)
@MainActor
@objc public extension IQKeyboardManager {
@available(*, unavailable, renamed: "resignOnTouchOutside")
var shouldResignOnTouchOutside: Bool {
get { false }
set { }
}
}
// swiftlint:enable unused_setter_value

View File

@ -0,0 +1,105 @@
//
// IQKeyboardResignHandler+Internal.swift
// https://github.com/hackiftekhar/IQKeyboardManager
// Copyright (c) 2013-24 Iftekhar Qurashi.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import UIKit
import IQKeyboardCore
@available(iOSApplicationExtension, unavailable)
@MainActor
internal extension IQKeyboardResignHandler {
func removeTextInputViewObserver() {
textInputViewObserver.unsubscribe(identifier: "IQKeyboardResignHandler")
}
func addTextInputViewObserver() {
textInputViewObserver.subscribe(identifier: "IQKeyboardResignHandler",
changeHandler: { [weak self] event, textInputView in
guard let self = self else { return }
switch event {
case .beginEditing:
self.resignGesture.isEnabled = self.privateResignOnTouchOutside()
textInputView.window?.addGestureRecognizer(self.resignGesture)
case .endEditing:
textInputView.window?.removeGestureRecognizer(self.resignGesture)
}
})
}
func privateResignOnTouchOutside() -> Bool {
guard let textInputView: any IQTextInputView = textInputViewObserver.textInputView else {
return resignOnTouchOutside
}
switch textInputView.internalResignOnTouchOutsideMode {
case .default:
guard var controller = (textInputView as UIView).iq.viewContainingController() else {
return resignOnTouchOutside
}
// If it is searchBar textField embedded in Navigation Bar
if textInputView is UISearchTextField {
if let navController: UINavigationController = controller as? UINavigationController,
let topController: UIViewController = navController.topViewController,
controller.navigationItem.searchController?.searchBar.searchTextField == textInputView {
controller = topController
}
}
// If viewController is in enabledTouchResignedClasses, then assuming resignOnTouchOutside is enabled.
let isWithEnabledClass: Bool = enabledTouchResignedClasses.contains(where: { controller.isKind(of: $0) })
var isEnabled: Bool = resignOnTouchOutside || isWithEnabledClass
if isEnabled {
// If viewController is in disabledTouchResignedClasses,
// then assuming resignOnTouchOutside is disable.
if disabledTouchResignedClasses.contains(where: { controller.isKind(of: $0) }) {
isEnabled = false
} else {
let classNameString: String = "\(type(of: controller.self))"
// _UIAlertControllerTextFieldViewController
if classNameString.contains("UIAlertController"),
classNameString.hasSuffix("TextFieldViewController") {
isEnabled = false
}
}
}
return isEnabled
case .enabled:
return true
case .disabled:
return false
}
}
}
@available(iOSApplicationExtension, unavailable)
@MainActor
fileprivate extension IQTextInputView {
var internalResignOnTouchOutsideMode: IQEnableMode {
iq.resignOnTouchOutsideMode
}
}

View File

@ -0,0 +1,154 @@
//
// IQKeyboardResignHandler.swift
// https://github.com/hackiftekhar/IQKeyboardManager
// Copyright (c) 2013-24 Iftekhar Qurashi.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import UIKit
import IQTextInputViewNotification
import IQKeyboardCore
@available(iOSApplicationExtension, unavailable)
@MainActor
@objcMembers internal final class IQKeyboardResignHandler: NSObject {
let textInputViewObserver: IQTextInputViewNotification = .init()
/**
Resigns Keyboard on touching outside of TextInputView. Default is NO.
*/
public var resignOnTouchOutside: Bool = false {
didSet {
resignGesture.isEnabled = privateResignOnTouchOutside()
IQKeyboardManager.shared.showLog("resignOnTouchOutside: \(resignOnTouchOutside ? "Yes" : "No")")
}
}
/** TapGesture to resign keyboard on view's touch.
It's a readonly property and exposed only for adding/removing dependencies
if your added gesture does have collision with this one
*/
public var resignGesture: UITapGestureRecognizer = .init()
/**
Disabled classes to ignore resignOnTouchOutside' property, Class should be kind of UIViewController.
*/
public var disabledTouchResignedClasses: [UIViewController.Type] = [
UIAlertController.self,
UIInputViewController.self
]
/**
Enabled classes to forcefully enable 'resignOnTouchOutside' property.
Class should be kind of UIViewController
. If same Class is added in disabledTouchResignedClasses list, then enabledTouchResignedClasses will be ignored.
*/
public var enabledTouchResignedClasses: [UIViewController.Type] = []
/**
if resignOnTouchOutside is enabled then you can customize the behavior
to not recognize gesture touches on some specific view subclasses.
Class should be kind of UIView. Default is [UIControl, UINavigationBar]
*/
public var touchResignedGestureIgnoreClasses: [UIView.Type] = [
UIControl.self,
UINavigationBar.self
]
/**
Resigns currently first responder field.
*/
@discardableResult
public func resignFirstResponder() -> Bool {
guard let textInputView: any IQTextInputView = textInputViewObserver.textInputView else {
return false
}
// Resigning first responder
guard textInputView.resignFirstResponder() else {
IQKeyboardManager.shared.showLog("Warning: Refuses to resign first responder: \(textInputView)")
// If it refuses then becoming it as first responder again. (Bug ID: #96)
// If it refuses to resign then becoming it first responder again for getting notifications callback.
textInputView.becomeFirstResponder()
return false
}
return true
}
public override init() {
super.init()
resignGesture.addTarget(self, action: #selector(self.tapRecognized(_:)))
resignGesture.cancelsTouchesInView = false
resignGesture.delegate = self
resignGesture.isEnabled = false
addTextInputViewObserver()
}
}
@available(iOSApplicationExtension, unavailable)
@MainActor
@objc extension IQKeyboardResignHandler: UIGestureRecognizerDelegate {
/** Resigning on tap gesture. (Enhancement ID: #14)*/
private func tapRecognized(_ gesture: UITapGestureRecognizer) {
if gesture.state == .ended {
// Resigning currently responder textInputView.
resignFirstResponder()
}
}
/** Note: returning YES is guaranteed to allow simultaneous recognition.
returning NO is not guaranteed to prevent simultaneous recognition,
as the other gesture's delegate may return YES.
*/
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
shouldRecognizeSimultaneouslyWith
otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return false
}
/**
To not detect touch events in a subclass of UIControl,
these may have added their own selector for specific work
*/
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
shouldReceive touch: UITouch) -> Bool {
// (Bug ID: #145)
// Should not recognize gesture if the clicked view is either UIControl or UINavigationBar(<Back button etc...)
for ignoreClass in touchResignedGestureIgnoreClasses where touch.view?.isKind(of: ignoreClass) ?? false {
return false
}
// (Issue #2109) Ignore Apple Pencil touches to prevent conflicts with floating keyboard on iPad
if touch.type == .pencil {
return false
}
return true
}
}

View File

@ -0,0 +1,69 @@
//
// UIView+Resign.swift
// https://github.com/hackiftekhar/IQKeyboardManager
// Copyright (c) 2013-24 Iftekhar Qurashi.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import UIKit
import IQKeyboardCore
@available(iOSApplicationExtension, unavailable)
@MainActor
private struct AssociatedKeys {
static var resignOnTouchOutsideMode: Int = 0
}
/**
TextInputView category for managing touch resign
*/
@available(iOSApplicationExtension, unavailable)
@MainActor
public extension IQKeyboardExtension where Base: IQTextInputView {
/**
Override resigns Keyboard on touching outside of TextInputView behavior for this particular textInputView.
*/
var resignOnTouchOutsideMode: IQEnableMode {
get {
guard let base = base else {
return .default
}
return objc_getAssociatedObject(base, &AssociatedKeys.resignOnTouchOutsideMode) as? IQEnableMode ?? .default
}
set(newValue) {
if let base = base {
objc_setAssociatedObject(base, &AssociatedKeys.resignOnTouchOutsideMode,
newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
}
}
// swiftlint:disable unused_setter_value
@available(iOSApplicationExtension, unavailable)
@MainActor
@objc public extension UIView {
@available(*, unavailable, renamed: "iq.resignOnTouchOutsideMode")
var shouldResignOnTouchOutsideMode: IQEnableMode {
get { .default }
set { }
}
}
// swiftlint:enable unused_setter_value

View File

@ -0,0 +1,45 @@
//
// UIView+ResignObjc.swift
// https://github.com/hackiftekhar/IQKeyboardManager
// Copyright (c) 2013-24 Iftekhar Qurashi.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import UIKit
import IQKeyboardCore
// MARK: For ObjectiveC Compatibility
// swiftlint:disable identifier_name
@objc public extension UITextField {
var iq_resignOnTouchOutsideMode: IQEnableMode {
get { iq.resignOnTouchOutsideMode }
set { iq.resignOnTouchOutsideMode = newValue }
}
}
@objc public extension UITextView {
var iq_resignOnTouchOutsideMode: IQEnableMode {
get { iq.resignOnTouchOutsideMode }
set { iq.resignOnTouchOutsideMode = newValue }
}
}
// swiftlint:enable identifier_name

21
Pods/IQKeyboardManagerSwift/LICENSE.md generated Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2013-2023 Iftekhar Qurashi
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

254
Pods/IQKeyboardManagerSwift/README.md generated Normal file
View File

@ -0,0 +1,254 @@
<p align="center">
<img src="https://raw.githubusercontent.com/hackiftekhar/IQKeyboardManager/master/Screenshot/Social.png" alt="Icon"/>
</p>
[![LICENSE.md](https://img.shields.io/github/license/hackiftekhar/IQKeyboardManager.svg)](https://github.com/hackiftekhar/IQKeyboardManager/blob/master/LICENSE.md)
[![Build Status](https://travis-ci.org/hackiftekhar/IQKeyboardManager.svg)](https://travis-ci.org/hackiftekhar/IQKeyboardManager)
![Platform iOS](https://img.shields.io/badge/Platform-iOS-blue.svg?style=fla)
[![CocoaPods](https://img.shields.io/cocoapods/v/IQKeyboardManagerSwift.svg)](http://cocoadocs.org/docsets/IQKeyboardManagerSwift)
[![Github tag](https://img.shields.io/github/tag/hackiftekhar/iqkeyboardmanager.svg)](https://github.com/hackiftekhar/IQKeyboardManager/tags)
## IQKeyboardManager Objective-C version source code is moved to https://github.com/hackiftekhar/IQKeyboardManagerObjC
## Introduction
While developing iOS apps, we often run into issues where the iPhone keyboard slides up and covers the `UITextField/UITextView`. `IQKeyboardManager` allows you to prevent this issue of keyboard sliding up and covering `UITextField/UITextView` without needing you to write any code or make any additional setup. To use `IQKeyboardManager` you simply need to add source files to your project.
## Key Features
1. **One Line of Code** - Just enable and it works
2. **Works Automatically** - No manual setup required
3. **No More UIScrollView** - Automatically handles scroll views
4. **No More Subclasses** - Works with standard UIKit components
5. **No More Manual Work** - Handles all edge cases automatically
6. **Modular Architecture** - Include only what you need via subspecs
### What's Included
- ✅ Automatic keyboard avoidance for UITextField/UITextView
- ✅ Support for UIScrollView, UITableView, UICollectionView
- ✅ All interface orientations
- ✅ Configurable keyboard distance
- ✅ Class-level enable/disable control
### Optional Features (via Subspecs)
- 📦 Toolbar with Previous/Next/Done buttons
- 📦 Return key handling customization
- 📦 Tap-to-resign keyboard
- 📦 Keyboard appearance configuration
- 📦 UITextView with placeholder supportv
## Subspecs
Now IQKeyboardManagerSwift uses a modular architecture with subspecs.
By default, all subspecs are included, but you can include only what you need:
### Available Subspecs
- **Core** (always included): Basic keyboard distance management
- **Appearance**: Keyboard appearance configuration
- **IQKeyboardReturnManager**: Return key handling
- **IQKeyboardToolbarManager**: Toolbar functionality (Previous/Next/Done buttons)
- **IQTextView**: UITextView with placeholder support
- **Resign**: Tap-to-resign keyboard functionality
### Including Specific Subspecs
```ruby
# Include toolbar example
pod 'IQKeyboardManagerSwift/IQKeyboardToolbarManager'
```
## Screenshot
[![Screenshot 1](https://raw.githubusercontent.com/hackiftekhar/IQKeyboardManager/master/Screenshot/README_Screenshot1.png)](http://youtu.be/6nhLw6hju2A)
[![Screenshot 2](https://raw.githubusercontent.com/hackiftekhar/IQKeyboardManager/master/Screenshot/README_Screenshot2.png)](http://youtu.be/6nhLw6hju2A)
[![Screenshot 3](https://raw.githubusercontent.com/hackiftekhar/IQKeyboardManager/master/Screenshot/README_Screenshot3.png)](http://youtu.be/6nhLw6hju2A)
[![Screenshot 4](https://raw.githubusercontent.com/hackiftekhar/IQKeyboardManager/master/Screenshot/README_Screenshot4.png)](http://youtu.be/6nhLw6hju2A)
[![Screenshot 5](https://raw.githubusercontent.com/hackiftekhar/IQKeyboardManager/master/Screenshot/README_Screenshot5.png)](http://youtu.be/6nhLw6hju2A)
## GIF animation
[![IQKeyboardManager](https://raw.githubusercontent.com/hackiftekhar/IQKeyboardManager/v3.3.0/Screenshot/IQKeyboardManager.gif)](http://youtu.be/6nhLw6hju2A)
## Video
<a href="http://youtu.be/WAYc2Qj-OQg" target="_blank"><img src="http://img.youtube.com/vi/WAYc2Qj-OQg/0.jpg"
alt="IQKeyboardManager Demo Video" width="480" height="360" border="10" /></a>
## Tutorial video by @rebeloper ([#1135](https://github.com/hackiftekhar/IQKeyboardManager/issues/1135))
@rebeloper demonstrated two videos on how to implement **IQKeyboardManager** at it's core:
<a href="https://www.youtube.com/playlist?list=PL_csAAO9PQ8aTL87XnueOXi3RpWE2m_8v" target="_blank"><img src="https://raw.githubusercontent.com/hackiftekhar/IQKeyboardManager/master/Screenshot/ThirdPartyYoutubeTutorial.jpg"
alt="Youtube Tutorial Playlist"/></a>
https://www.youtube.com/playlist?list=PL_csAAO9PQ8aTL87XnueOXi3RpWE2m_8v
## Warning
- **If you're planning to build SDK/library/framework and want to handle UITextField/UITextView with IQKeyboardManager then you're totally going the wrong way.** I would never suggest to add **IQKeyboardManager** as **dependency/adding/shipping** with any third-party library. Instead of adding **IQKeyboardManager** you should implement your own solution to achieve same kind of results. **IQKeyboardManager** is totally designed for projects to help developers for their convenience, it's not designed for **adding/dependency/shipping** with any **third-party library**, because **doing this could block adoption by other developers for their projects as well (who are not using IQKeyboardManager and have implemented their custom solution to handle UITextField/UITextView in the project).**
- If **IQKeyboardManager** conflicts with other **third-party library**, then it's **developer responsibility** to **enable/disable IQKeyboardManager** when **presenting/dismissing** third-party library UI. Third-party libraries are not responsible to handle IQKeyboardManager.
## Requirements
| | Minimum iOS Target | Minimum Xcode Version |
|------------------------|--------------------|-----------------------|
| IQKeyboardManagerSwift | iOS 13.0 | Xcode 13 |
| Demo Project | | Xcode 15 |
#### Swift versions support
| Swift | Xcode | IQKeyboardManagerSwift |
|-------------------|-------|------------------------|
| 5.9, 5.8, 5.7 | 16 | >= 7.0.0 |
| 5.9, 5.8, 5.7, 5.6| 15 | >= 7.0.0 |
| 5.5, 5.4, 5.3, 5.2, 5.1, 5.0, 4.2| 11 | >= 6.5.7 |
| 5.1, 5.0, 4.2, 4.0, 3.2, 3.0| 11 | >= 6.5.0 |
| 5.0,4.2, 4.0, 3.2, 3.0| 10.2 | >= 6.2.1 |
| 4.2, 4.0, 3.2, 3.0| 10.0 | >= 6.0.4 |
| 4.0, 3.2, 3.0 | 9.0 | 5.0.0 |
Installation
==========================
#### CocoaPods
To install it, simply add the following line to your Podfile: ([#236](https://github.com/hackiftekhar/IQKeyboardManager/issues/236))
```ruby
pod 'IQKeyboardManagerSwift'
```
*Or you can choose the version you need based on Swift support table from [Requirements](README.md#requirements)*
```ruby
pod 'IQKeyboardManagerSwift', '8.0.0'
```
#### Carthage
To integrate `IQKeyboardManger` or `IQKeyboardManagerSwift` into your Xcode project using Carthage, add the following line to your `Cartfile`:
```ogdl
github "hackiftekhar/IQKeyboardManager"
```
Run `carthage update --use-xcframeworks` to build the frameworks and drag `IQKeyboardManagerSwift.xcframework` into your Xcode project based on your need. Make sure to add only one framework, not both.
#### Swift Package Manager (SPM)
To install `IQKeyboardManagerSwift` package via Xcode
* Go to File -> Swift Packages -> Add Package Dependency...
* Then search for https://github.com/hackiftekhar/IQKeyboardManager.git
* And choose the version you want
#### Source Code
***IQKeyboardManagerSwift:*** Source code installation is not supported (since 7.2.0) because now the library depends on some other independent libraries. Due to this you may face compilation issues.
#### Basic Usage
### Minimal Setup (Core Only)
In `AppDelegate.swift`, import and enable IQKeyboardManager:
```swift
import IQKeyboardManagerSwift
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Enable keyboard management
IQKeyboardManager.shared.isEnabled = true
return true
}
}
```
That's it! The keyboard will now automatically adjust to avoid covering text fields.
### With Toolbar (Requires IQKeyboardToolbarManager Subspec)
```swift
import IQKeyboardManagerSwift
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Enable keyboard management
IQKeyboardManager.shared.isEnabled = true
// Enable toolbar (@Deprecated: Please use IQKeyboardToolbarManager pod independently)
IQKeyboardManager.shared.enableAutoToolbar = true
return true
}
```
### With All Features
```swift
import IQKeyboardManagerSwift
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Core functionality
IQKeyboardManager.shared.isEnabled = true
IQKeyboardManager.shared.keyboardDistance = 20.0
// Toolbar (if using IQKeyboardToolbarManager subspec)
IQKeyboardManager.shared.enableAutoToolbar = true
// Tap to resign (if using Resign subspec)
IQKeyboardManager.shared.resignOnTouchOutside = true
// Appearance (if using Appearance subspec)
IQKeyboardManager.shared.keyboardConfiguration.overrideKeyboardAppearance = true
IQKeyboardManager.shared.keyboardConfiguration.keyboardAppearance = .dark
return true
}
```
Migration Guide
==========================
- [IQKeyboardManager 2.0.0 Migration Guide](https://github.com/hackiftekhar/IQKeyboardManager/blob/master/Documentation/MIGRATION%20GUIDE%201.0%20TO%202.0.md)
- [IQKeyboardManager 3.0.0 Migration Guide](https://github.com/hackiftekhar/IQKeyboardManager/blob/master/Documentation/MIGRATION%20GUIDE%202.0%20TO%203.0.md)
- [IQKeyboardManager 4.0.0 Migration Guide](https://github.com/hackiftekhar/IQKeyboardManager/blob/master/Documentation/MIGRATION%20GUIDE%203.0%20TO%204.0.md)
- [IQKeyboardManager 5.0.0 Migration Guide](https://github.com/hackiftekhar/IQKeyboardManager/blob/master/Documentation/MIGRATION%20GUIDE%204.0%20TO%205.0.md)
- [IQKeyboardManager 6.0.0 Migration Guide](https://github.com/hackiftekhar/IQKeyboardManager/blob/master/Documentation/MIGRATION%20GUIDE%205.0%20TO%206.0.md)
- [IQKeyboardManager 7.0.0 Migration Guide](https://github.com/hackiftekhar/IQKeyboardManager/blob/master/Documentation/MIGRATION%20GUIDE%206.0%20TO%207.0.md)
- [IQKeyboardManager 8.0.0 Migration Guide](https://github.com/hackiftekhar/IQKeyboardManager/blob/master/Documentation/MIGRATION%20GUIDE%207.0%20TO%208.0.md)
Other Links
==========================
- [Known Issues](https://github.com/hackiftekhar/IQKeyboardManager/wiki/Known-Issues)
- [Manual Management Tweaks](https://github.com/hackiftekhar/IQKeyboardManager/wiki/Manual-Management)
- [Properties and functions usage](https://github.com/hackiftekhar/IQKeyboardManager/wiki/Properties-&-Functions)
## Dependency Diagram
[![IQKeyboardManager Dependency Diagram](https://raw.githubusercontent.com/hackiftekhar/IQKeyboardManager/master/Screenshot/IQKeyboardManagerDependency.jpg)](https://raw.githubusercontent.com/hackiftekhar/IQKeyboardManager/master/Screenshot/IQKeyboardManagerDependency.jpg)
LICENSE
---
Distributed under the MIT License.
Contributions
---
Any contribution is more than welcome! You can contribute through pull requests and issues on GitHub.
Author
---
If you wish to contact me, email at: hack.iftekhar@gmail.com