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

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
@@ -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()
}
}
@@ -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
@@ -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
}
}
@@ -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
}
}
@@ -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
@@ -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