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

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,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,286 @@
//
// IQKeyboardReturnManager+UITextFieldDelegate.swift
// https://github.com/hackiftekhar/IQKeyboardReturnManager
// 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: UITextFieldDelegate
@available(iOSApplicationExtension, unavailable)
@MainActor
@objc extension IQKeyboardReturnManager: UITextFieldDelegate {
}
@objc public extension IQKeyboardReturnManager {
func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
var returnValue: Bool = true
if delegate == nil,
let textFieldDelegate: any UITextFieldDelegate = textInputViewCachedInfo(textField)?.textFieldDelegate {
let selector = #selector((any UITextFieldDelegate).textFieldShouldBeginEditing(_:))
if textFieldDelegate.responds(to: selector) {
returnValue = textFieldDelegate.textFieldShouldBeginEditing?(textField) ?? false
}
}
if returnValue {
updateReturnKey(textInputView: textField)
}
return returnValue
}
func textFieldShouldEndEditing(_ textField: UITextField) -> Bool {
guard delegate == nil else { return true }
if let textFieldDelegate: any UITextFieldDelegate = textInputViewCachedInfo(textField)?.textFieldDelegate {
let selector = #selector((any UITextFieldDelegate).textFieldShouldEndEditing(_:))
if textFieldDelegate.responds(to: selector) {
return textFieldDelegate.textFieldShouldEndEditing?(textField) ?? false
}
}
return true
}
func textFieldDidBeginEditing(_ textField: UITextField) {
var aDelegate: (any UITextFieldDelegate)? = delegate
if aDelegate == nil {
if let model: IQTextInputViewInfoModel = textInputViewCachedInfo(textField) {
aDelegate = model.textFieldDelegate
}
}
aDelegate?.textFieldDidBeginEditing?(textField)
}
func textFieldDidEndEditing(_ textField: UITextField) {
var aDelegate: (any UITextFieldDelegate)? = delegate
if aDelegate == nil {
if let model: IQTextInputViewInfoModel = textInputViewCachedInfo(textField) {
aDelegate = model.textFieldDelegate
}
}
aDelegate?.textFieldDidEndEditing?(textField)
}
func textFieldDidEndEditing(_ textField: UITextField, reason: UITextField.DidEndEditingReason) {
var aDelegate: (any UITextFieldDelegate)? = delegate
if aDelegate == nil {
if let model: IQTextInputViewInfoModel = textInputViewCachedInfo(textField) {
aDelegate = model.textFieldDelegate
}
}
aDelegate?.textFieldDidEndEditing?(textField, reason: reason)
}
func textField(_ textField: UITextField,
shouldChangeCharactersIn range: NSRange,
replacementString string: String) -> Bool {
guard delegate == nil else { return true }
if let textFieldDelegate: any UITextFieldDelegate = textInputViewCachedInfo(textField)?.textFieldDelegate {
let selector: Selector = #selector((any UITextFieldDelegate).textField(_:shouldChangeCharactersIn:
replacementString:))
if textFieldDelegate.responds(to: selector) {
return textFieldDelegate.textField?(textField,
shouldChangeCharactersIn: range,
replacementString: string) ?? false
}
}
return true
}
func textFieldDidChangeSelection(_ textField: UITextField) {
var aDelegate: (any UITextFieldDelegate)? = delegate
if aDelegate == nil {
if let model: IQTextInputViewInfoModel = textInputViewCachedInfo(textField) {
aDelegate = model.textFieldDelegate
}
}
aDelegate?.textFieldDidChangeSelection?(textField)
}
func textFieldShouldClear(_ textField: UITextField) -> Bool {
guard delegate == nil else { return true }
if let textFieldDelegate: any UITextFieldDelegate = textInputViewCachedInfo(textField)?.textFieldDelegate {
let selector: Selector = #selector((any UITextFieldDelegate).textFieldShouldClear(_:))
if textFieldDelegate.responds(to: selector) {
return textFieldDelegate.textFieldShouldClear?(textField) ?? false
}
}
return true
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
guard delegate == nil else { return true }
var isReturn: Bool = true
if let textFieldDelegate: any UITextFieldDelegate = textInputViewCachedInfo(textField)?.textFieldDelegate {
let selector: Selector = #selector((any UITextFieldDelegate).textFieldShouldReturn(_:))
if textFieldDelegate.responds(to: selector) {
isReturn = textFieldDelegate.textFieldShouldReturn?(textField) ?? false
}
}
if isReturn {
goToNextResponderOrResign(from: textField)
return true
} else {
return goToNextResponderOrResign(from: textField)
}
}
}
#if compiler(>=5.7) // Xcode 14
@available(iOS 16.0, *)
@available(iOSApplicationExtension, unavailable)
@MainActor
@objc public extension IQKeyboardReturnManager {
func textField(_ textField: UITextField, editMenuForCharactersIn range: NSRange, suggestedActions: [UIMenuElement]) -> UIMenu? {
guard delegate == nil else { return nil }
if let textFieldDelegate: any UITextFieldDelegate = textInputViewCachedInfo(textField)?.textFieldDelegate {
let selector = #selector((any UITextFieldDelegate).textField(_:editMenuForCharactersIn:suggestedActions:))
if textFieldDelegate.responds(to: selector) {
return textFieldDelegate.textField?(textField, editMenuForCharactersIn: range, suggestedActions: suggestedActions)
}
}
return nil
}
func textField(_ textField: UITextField, willPresentEditMenuWith animator: any UIEditMenuInteractionAnimating) {
var aDelegate: (any UITextFieldDelegate)? = delegate
if aDelegate == nil {
if let model: IQTextInputViewInfoModel = textInputViewCachedInfo(textField) {
aDelegate = model.textFieldDelegate
}
}
aDelegate?.textField?(textField, willPresentEditMenuWith: animator)
}
func textField(_ textField: UITextField, willDismissEditMenuWith animator: any UIEditMenuInteractionAnimating) {
var aDelegate: (any UITextFieldDelegate)? = delegate
if aDelegate == nil {
if let model: IQTextInputViewInfoModel = textInputViewCachedInfo(textField) {
aDelegate = model.textFieldDelegate
}
}
aDelegate?.textField?(textField, willDismissEditMenuWith: animator)
}
}
#endif
#if compiler(>=6.0) // Xcode 16
@available(iOS 18.0, *)
@available(iOSApplicationExtension, unavailable)
@MainActor
@objc public extension IQKeyboardReturnManager {
#if compiler(>=6.1) // Xcode 16.3
@available(iOS 18.4, *)
func textField(_ textField: UITextField, insertInputSuggestion inputSuggestion: UIInputSuggestion) {
var aDelegate: (any UITextFieldDelegate)? = delegate
if aDelegate == nil {
if let model: IQTextInputViewInfoModel = textInputViewCachedInfo(textField) {
aDelegate = model.textFieldDelegate
}
}
aDelegate?.textField?(textField, insertInputSuggestion: inputSuggestion)
}
#endif
}
#endif
#if compiler(>=6.2) // Xcode 26
@available(iOS 26.0, *)
@available(iOSApplicationExtension, unavailable)
@MainActor
@objc public extension IQKeyboardReturnManager {
func textField(_ textField: UITextField, shouldChangeCharactersInRanges ranges: [NSValue], replacementString string: String) -> Bool {
guard delegate == nil else { return true }
if let textFieldDelegate: any UITextFieldDelegate = textInputViewCachedInfo(textField)?.textFieldDelegate {
let selector: Selector = #selector((any UITextFieldDelegate).textField(_:shouldChangeCharactersInRanges:
replacementString:))
if textFieldDelegate.responds(to: selector) {
return textFieldDelegate.textField?(textField,
shouldChangeCharactersInRanges: ranges,
replacementString: string) ?? false
}
}
return true
}
func textField(_ textField: UITextField, editMenuForCharactersInRanges ranges: [NSValue], suggestedActions: [UIMenuElement]) -> UIMenu? {
guard delegate == nil else { return nil }
if let textFieldDelegate: any UITextFieldDelegate = textInputViewCachedInfo(textField)?.textFieldDelegate {
let selector = #selector((any UITextFieldDelegate).textField(_:editMenuForCharactersInRanges:suggestedActions:))
if textFieldDelegate.responds(to: selector) {
return textFieldDelegate.textField?(textField, editMenuForCharactersInRanges: ranges, suggestedActions: suggestedActions)
}
}
return nil
}
}
#endif

View File

@ -0,0 +1,532 @@
//
// IQKeyboardReturnManager+UITextViewDelegate.swift
// https://github.com/hackiftekhar/IQKeyboardReturnManager
// 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: UITextViewDelegate
@available(iOSApplicationExtension, unavailable)
@MainActor
@objc extension IQKeyboardReturnManager: UITextViewDelegate {
}
@objc public extension IQKeyboardReturnManager {
func textViewShouldBeginEditing(_ textView: UITextView) -> Bool {
var returnValue: Bool = true
if delegate == nil,
let textViewDelegate: any UITextViewDelegate = textInputViewCachedInfo(textView)?.textViewDelegate {
if textViewDelegate.responds(to: #selector((any UITextViewDelegate).textViewShouldBeginEditing(_:))) {
returnValue = textViewDelegate.textViewShouldBeginEditing?(textView) ?? false
}
}
if returnValue {
updateReturnKey(textInputView: textView)
}
return returnValue
}
func textViewShouldEndEditing(_ textView: UITextView) -> Bool {
guard delegate == nil else { return true }
if let textViewDelegate: any UITextViewDelegate = textInputViewCachedInfo(textView)?.textViewDelegate {
if textViewDelegate.responds(to: #selector((any UITextViewDelegate).textViewShouldEndEditing(_:))) {
return textViewDelegate.textViewShouldEndEditing?(textView) ?? false
}
}
return true
}
func textViewDidBeginEditing(_ textView: UITextView) {
var aDelegate: (any UITextViewDelegate)? = delegate
if aDelegate == nil {
if let model: IQTextInputViewInfoModel = textInputViewCachedInfo(textView) {
aDelegate = model.textViewDelegate
}
}
aDelegate?.textViewDidBeginEditing?(textView)
}
func textViewDidEndEditing(_ textView: UITextView) {
var aDelegate: (any UITextViewDelegate)? = delegate
if aDelegate == nil {
if let model: IQTextInputViewInfoModel = textInputViewCachedInfo(textView) {
aDelegate = model.textViewDelegate
}
}
aDelegate?.textViewDidEndEditing?(textView)
}
func textView(_ textView: UITextView,
shouldChangeTextIn range: NSRange,
replacementText text: String) -> Bool {
var shouldChange = true
if delegate == nil {
if let textViewDelegate: any UITextViewDelegate = textInputViewCachedInfo(textView)?.textViewDelegate {
let selector = #selector((any UITextViewDelegate).textView(_:shouldChangeTextIn:replacementText:))
if textViewDelegate.responds(to: selector) {
shouldChange = (textViewDelegate.textView?(textView,
shouldChangeTextIn: range,
replacementText: text)) ?? true
}
}
}
if self.dismissTextViewOnReturn, text == "\n" {
goToNextResponderOrResign(from: textView)
return false
}
return shouldChange
}
func textViewDidChange(_ textView: UITextView) {
var aDelegate: (any UITextViewDelegate)? = delegate
if aDelegate == nil {
if let model: IQTextInputViewInfoModel = textInputViewCachedInfo(textView) {
aDelegate = model.textViewDelegate
}
}
aDelegate?.textViewDidChange?(textView)
}
func textViewDidChangeSelection(_ textView: UITextView) {
var aDelegate: (any UITextViewDelegate)? = delegate
if aDelegate == nil {
if let model: IQTextInputViewInfoModel = textInputViewCachedInfo(textView) {
aDelegate = model.textViewDelegate
}
}
aDelegate?.textViewDidChangeSelection?(textView)
}
@available(iOS, deprecated: 17.0)
func textView(_ aTextView: UITextView,
shouldInteractWith URL: URL,
in characterRange: NSRange,
interaction: UITextItemInteraction) -> Bool {
guard delegate == nil else { return true }
if let textViewDelegate: any UITextViewDelegate = textInputViewCachedInfo(aTextView)?.textViewDelegate {
let selector: Selector = #selector(textView as (UITextView, URL, NSRange, UITextItemInteraction) -> Bool)
if textViewDelegate.responds(to: selector) {
return textViewDelegate.textView?(aTextView,
shouldInteractWith: URL,
in: characterRange,
interaction: interaction) ?? false
}
}
return true
}
@available(iOS, deprecated: 17.0)
func textView(_ aTextView: UITextView,
shouldInteractWith textAttachment: NSTextAttachment,
in characterRange: NSRange,
interaction: UITextItemInteraction) -> Bool {
guard delegate == nil else { return true }
if let textViewDelegate: any UITextViewDelegate = textInputViewCachedInfo(aTextView)?.textViewDelegate {
let selector: Selector = #selector(textView as
(UITextView, NSTextAttachment, NSRange, UITextItemInteraction)
-> Bool)
if textViewDelegate.responds(to: selector) {
return textViewDelegate.textView?(aTextView,
shouldInteractWith: textAttachment,
in: characterRange,
interaction: interaction) ?? false
}
}
return true
}
@available(iOS, deprecated: 10.0)
func textView(_ aTextView: UITextView,
shouldInteractWith URL: URL,
in characterRange: NSRange) -> Bool {
guard delegate == nil else { return true }
if let textViewDelegate: any UITextViewDelegate = textInputViewCachedInfo(aTextView)?.textViewDelegate {
let selector: Selector = #selector(textView as (UITextView, URL, NSRange) -> Bool)
if textViewDelegate.responds(to: selector) {
return textViewDelegate.textView?(aTextView,
shouldInteractWith: URL,
in: characterRange) ?? false
}
}
return true
}
@available(iOS, deprecated: 10.0)
func textView(_ aTextView: UITextView,
shouldInteractWith textAttachment: NSTextAttachment,
in characterRange: NSRange) -> Bool {
guard delegate == nil else { return true }
if let textViewDelegate: any UITextViewDelegate = textInputViewCachedInfo(aTextView)?.textViewDelegate {
let selector: Selector = #selector(textView as (UITextView, NSTextAttachment, NSRange) -> Bool)
if textViewDelegate.responds(to: selector) {
return textViewDelegate.textView?(aTextView,
shouldInteractWith: textAttachment,
in: characterRange) ?? false
}
}
return true
}
}
#if compiler(>=5.7) // Xcode 14
@available(iOS 16.0, *)
@available(iOSApplicationExtension, unavailable)
@MainActor
@objc public extension IQKeyboardReturnManager {
func textView(_ aTextView: UITextView,
editMenuForTextIn range: NSRange,
suggestedActions: [UIMenuElement]) -> UIMenu? {
guard delegate == nil else { return nil }
if let textViewDelegate: any UITextViewDelegate = textInputViewCachedInfo(aTextView)?.textViewDelegate {
let selector = #selector((any UITextViewDelegate).textView(_:editMenuForTextIn:suggestedActions:))
if textViewDelegate.responds(to: selector) {
return textViewDelegate.textView?(aTextView,
editMenuForTextIn: range,
suggestedActions: suggestedActions)
}
}
return nil
}
func textView(_ aTextView: UITextView,
willPresentEditMenuWith animator: any UIEditMenuInteractionAnimating) {
var aDelegate: (any UITextViewDelegate)? = delegate
if aDelegate == nil {
if let model: IQTextInputViewInfoModel = textInputViewCachedInfo(aTextView) {
aDelegate = model.textViewDelegate
}
}
aDelegate?.textView?(aTextView, willPresentEditMenuWith: animator)
}
func textView(_ aTextView: UITextView,
willDismissEditMenuWith animator: any UIEditMenuInteractionAnimating) {
var aDelegate: (any UITextViewDelegate)? = delegate
if aDelegate == nil {
if let model: IQTextInputViewInfoModel = textInputViewCachedInfo(aTextView) {
aDelegate = model.textViewDelegate
}
}
aDelegate?.textView?(aTextView, willDismissEditMenuWith: animator)
}
}
#endif
#if compiler(>=5.9) // Xcode 15
@available(iOS 17.0, *)
@available(iOSApplicationExtension, unavailable)
@MainActor
@objc public extension IQKeyboardReturnManager {
func textView(_ aTextView: UITextView,
primaryActionFor textItem: UITextItem,
defaultAction: UIAction) -> UIAction? {
guard delegate == nil else { return nil }
if let textViewDelegate = textInputViewCachedInfo(aTextView)?.textViewDelegate {
let selector = #selector((any UITextViewDelegate).textView(_:primaryActionFor:defaultAction:))
if textViewDelegate.responds(to: selector) {
return textViewDelegate.textView?(aTextView,
primaryActionFor: textItem,
defaultAction: defaultAction)
}
}
return nil
}
func textView(_ aTextView: UITextView,
menuConfigurationFor textItem: UITextItem,
defaultMenu: UIMenu) -> UITextItem.MenuConfiguration? {
guard delegate == nil else { return nil }
if let textViewDelegate = textInputViewCachedInfo(aTextView)?.textViewDelegate {
let selector = #selector((any UITextViewDelegate).textView(_:menuConfigurationFor:defaultMenu:))
if textViewDelegate.responds(to: selector) {
return textViewDelegate.textView?(aTextView,
menuConfigurationFor: textItem,
defaultMenu: defaultMenu)
}
}
return nil
}
func textView(_ textView: UITextView,
textItemMenuWillDisplayFor textItem: UITextItem,
animator: any UIContextMenuInteractionAnimating) {
var aDelegate: (any UITextViewDelegate)? = delegate
if aDelegate == nil {
if let model = textInputViewCachedInfo(textView) {
aDelegate = model.textViewDelegate
}
}
aDelegate?.textView?(textView, textItemMenuWillDisplayFor: textItem, animator: animator)
}
func textView(_ textView: UITextView,
textItemMenuWillEndFor textItem: UITextItem,
animator: any UIContextMenuInteractionAnimating) {
var aDelegate: (any UITextViewDelegate)? = delegate
if aDelegate == nil {
if let model = textInputViewCachedInfo(textView) {
aDelegate = model.textViewDelegate
}
}
aDelegate?.textView?(textView, textItemMenuWillEndFor: textItem, animator: animator)
}
}
#endif
#if compiler(>=6.0) // Xcode 16
@available(iOS 18.0, *)
@available(iOSApplicationExtension, unavailable)
@MainActor
@objc public extension IQKeyboardReturnManager {
func textViewWritingToolsWillBegin(_ textView: UITextView) {
var aDelegate: (any UITextViewDelegate)? = delegate
if aDelegate == nil {
if let model: IQTextInputViewInfoModel = textInputViewCachedInfo(textView) {
aDelegate = model.textViewDelegate
}
}
aDelegate?.textViewWritingToolsWillBegin?(textView)
}
func textViewWritingToolsDidEnd(_ textView: UITextView) {
var aDelegate: (any UITextViewDelegate)? = delegate
if aDelegate == nil {
if let model: IQTextInputViewInfoModel = textInputViewCachedInfo(textView) {
aDelegate = model.textViewDelegate
}
}
aDelegate?.textViewWritingToolsDidEnd?(textView)
}
func textView(_ textView: UITextView,
writingToolsIgnoredRangesInEnclosingRange enclosingRange: NSRange) -> [NSValue] {
guard delegate == nil else { return [] }
if let textViewDelegate = textInputViewCachedInfo(textView)?.textViewDelegate {
let selector = #selector((any UITextViewDelegate).textView(_:writingToolsIgnoredRangesInEnclosingRange:))
if textViewDelegate.responds(to: selector) {
return textViewDelegate.textView?(textView,
writingToolsIgnoredRangesInEnclosingRange: enclosingRange) ?? []
}
}
return []
}
func textView(_ textView: UITextView, willBeginFormattingWith viewController: UITextFormattingViewController) {
var aDelegate: (any UITextViewDelegate)? = delegate
if aDelegate == nil {
if let model: IQTextInputViewInfoModel = textInputViewCachedInfo(textView) {
aDelegate = model.textViewDelegate
}
}
aDelegate?.textView?(textView, willBeginFormattingWith: viewController)
}
func textView(_ textView: UITextView, didBeginFormattingWith viewController: UITextFormattingViewController) {
var aDelegate: (any UITextViewDelegate)? = delegate
if aDelegate == nil {
if let model: IQTextInputViewInfoModel = textInputViewCachedInfo(textView) {
aDelegate = model.textViewDelegate
}
}
aDelegate?.textView?(textView, didBeginFormattingWith: viewController)
}
func textView(_ textView: UITextView, willEndFormattingWith viewController: UITextFormattingViewController) {
var aDelegate: (any UITextViewDelegate)? = delegate
if aDelegate == nil {
if let model: IQTextInputViewInfoModel = textInputViewCachedInfo(textView) {
aDelegate = model.textViewDelegate
}
}
aDelegate?.textView?(textView, willEndFormattingWith: viewController)
}
func textView(_ textView: UITextView, didEndFormattingWith viewController: UITextFormattingViewController) {
var aDelegate: (any UITextViewDelegate)? = delegate
if aDelegate == nil {
if let model: IQTextInputViewInfoModel = textInputViewCachedInfo(textView) {
aDelegate = model.textViewDelegate
}
}
aDelegate?.textView?(textView, didEndFormattingWith: viewController)
}
#if compiler(>=6.1) // Xcode 16.3
@available(iOS 18.4, *)
func textView(_ textView: UITextView, insertInputSuggestion inputSuggestion: UIInputSuggestion) {
var aDelegate: (any UITextViewDelegate)? = delegate
if aDelegate == nil {
if let model: IQTextInputViewInfoModel = textInputViewCachedInfo(textView) {
aDelegate = model.textViewDelegate
}
}
aDelegate?.textView?(textView, insertInputSuggestion: inputSuggestion)
}
#endif
}
#endif
#if compiler(>=6.2) // Xcode 26
@available(iOS 26.0, *)
@available(iOSApplicationExtension, unavailable)
@MainActor
@objc public extension IQKeyboardReturnManager {
func textView(_ textView: UITextView, shouldChangeTextInRanges ranges: [NSValue], replacementText text: String) -> Bool {
var shouldChange = true
if delegate == nil {
if let textViewDelegate: any UITextViewDelegate = textInputViewCachedInfo(textView)?.textViewDelegate {
let selector = #selector((any UITextViewDelegate).textView(_:shouldChangeTextInRanges:replacementText:))
if textViewDelegate.responds(to: selector) {
shouldChange = (textViewDelegate.textView?(textView,
shouldChangeTextInRanges: ranges,
replacementText: text)) ?? true
}
}
}
if self.dismissTextViewOnReturn, text == "\n" {
goToNextResponderOrResign(from: textView)
return false
}
return shouldChange
}
func textView(_ textView: UITextView, editMenuForTextInRanges ranges: [NSValue], suggestedActions: [UIMenuElement]) -> UIMenu? {
guard delegate == nil else { return nil }
if let textViewDelegate: any UITextViewDelegate = textInputViewCachedInfo(textView)?.textViewDelegate {
let selector = #selector((any UITextViewDelegate).textView(_:editMenuForTextInRanges:suggestedActions:))
if textViewDelegate.responds(to: selector) {
return textViewDelegate.textView?(textView,
editMenuForTextInRanges: ranges,
suggestedActions: suggestedActions)
}
}
return nil
}
}
#endif

View File

@ -0,0 +1,278 @@
//
// IQKeyboardReturnManager.swift
// https://github.com/hackiftekhar/IQKeyboardReturnManager
// 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
/**
Manages the return key to work like next/done in a view hierarchy.
*/
@available(iOSApplicationExtension, unavailable)
@MainActor
@objcMembers public final class IQKeyboardReturnManager: NSObject {
// MARK: Private variables
private var textInputViewInfoCache: [IQTextInputViewInfoModel] = []
// MARK: Settings
/**
Delegate of textInputView
*/
public weak var delegate: (any UITextFieldDelegate & UITextViewDelegate)?
/**
Set the last textInputView return key type. Default is UIReturnKeyDefault.
*/
public var lastTextInputViewReturnKeyType: UIReturnKeyType = .default {
didSet {
if let activeModel = textInputViewInfoCache.first(where: {
guard let textInputView = $0.textInputView else {
return false
}
return textInputView.isFirstResponder
}), let view: any IQTextInputView = activeModel.textInputView {
updateReturnKey(textInputView: view)
}
}
}
public var dismissTextViewOnReturn: Bool = false
// MARK: Initialization/De-initialization
public override init() {
super.init()
}
@available(*, unavailable, message: "Please use addResponderSubviews(of:recursive:)")
public init(controller: UIViewController) {
super.init()
addResponderSubviews(of: controller.view, recursive: true)
}
deinit {
// for model in textInputViewInfoCache {
// model.restore()
// }
textInputViewInfoCache.removeAll()
}
}
// MARK: Registering/Unregistering textInputView
@available(iOSApplicationExtension, unavailable)
@MainActor
@objc public extension IQKeyboardReturnManager {
/**
Should pass TextInputView instance. Assign textInputView delegate to self, change it's returnKeyType.
@param textInputView TextInputView object to register.
*/
func add(textInputView: any IQTextInputView) {
let model = IQTextInputViewInfoModel(textInputView: textInputView)
textInputViewInfoCache.append(model)
if let view: UITextField = textInputView as? UITextField {
view.delegate = self
} else if let view: UITextView = textInputView as? UITextView {
view.delegate = self
}
}
/**
Should pass TextInputView instance. Restore it's textInputView delegate and it's returnKeyType.
@param textInputView TextInputView object to unregister.
*/
func remove(textInputView: any IQTextInputView) {
guard let index: Int = textInputViewCachedInfoIndex(textInputView) else { return }
let model = textInputViewInfoCache.remove(at: index)
model.restore()
}
/**
Add all the TextInputView responderView's.
@param view object to register all it's responder subviews.
*/
func addResponderSubviews(of view: UIView, recursive: Bool) {
let textInputViews: [any IQTextInputView] = view.responderSubviews(recursive: recursive)
for view in textInputViews {
add(textInputView: view)
}
}
/**
Remove all the TextInputView responderView's.
@param view object to unregister all it's responder subviews.
*/
func removeResponderSubviews(of view: UIView, recursive: Bool) {
let textInputViews: [any IQTextInputView] = view.responderSubviews(recursive: recursive)
for view in textInputViews {
remove(textInputView: view)
}
}
}
@available(iOSApplicationExtension, unavailable)
@MainActor
@objc public extension IQKeyboardReturnManager {
@discardableResult
func goToNextResponderOrResign(from textInputView: any IQTextInputView) -> Bool {
guard let textInfoCache: IQTextInputViewInfoModel = nextResponderFromTextInputView(textInputView),
let textInputView = textInfoCache.textInputView else {
textInputView.resignFirstResponder()
return true
}
textInputView.becomeFirstResponder()
return false
}
}
// MARK: Internal Functions
@available(iOSApplicationExtension, unavailable)
@MainActor
internal extension IQKeyboardReturnManager {
func nextResponderFromTextInputView(_ textInputView: some IQTextInputView) -> IQTextInputViewInfoModel? {
guard let currentIndex: Int = textInputViewCachedInfoIndex(textInputView),
currentIndex < textInputViewInfoCache.count - 1 else { return nil }
let candidates = Array(textInputViewInfoCache[currentIndex+1..<textInputViewInfoCache.count])
return candidates.first {
guard let inputView = $0.textInputView,
inputView.isUserInteractionEnabled,
!inputView.isHidden, inputView.alpha != 0.0
else { return false }
return inputView.iqIsEnabled
}
}
func textInputViewCachedInfoIndex(_ textInputView: some IQTextInputView) -> Int? {
return textInputViewInfoCache.firstIndex {
guard let inputView = $0.textInputView else { return false }
return inputView == textInputView
}
}
func textInputViewCachedInfo(_ textInputView: some IQTextInputView) -> IQTextInputViewInfoModel? {
guard let index: Int = textInputViewCachedInfoIndex(textInputView) else { return nil }
return textInputViewInfoCache[index]
}
func updateReturnKey(textInputView: some IQTextInputView) {
let returnKey: UIReturnKeyType
if nextResponderFromTextInputView(textInputView) != nil {
returnKey = .next
} else {
returnKey = lastTextInputViewReturnKeyType
}
if textInputView.returnKeyType != returnKey {
// If it's the last textInputView in responder view, else next
textInputView.returnKeyType = returnKey
textInputView.reloadInputViews()
}
}
}
// MARK: Deprecated
// swiftlint:disable unused_setter_value
@available(iOSApplicationExtension, unavailable)
@MainActor
@objc public extension IQKeyboardReturnManager {
@available(*, unavailable, renamed: "lastTextInputViewReturnKeyType")
var lastTextFieldReturnKeyType: UIReturnKeyType {
get { .default }
set { }
}
@available(*, unavailable, renamed: "add(textInputView:)")
func addTextFieldView(_ textInputView: any IQTextInputView) { }
@available(*, unavailable, renamed: "remove(textInputView:)")
func removeTextFieldView(_ textInputView: any IQTextInputView) { }
@available(*, unavailable, renamed: "addResponderSubviews(of:recursive:)")
func addResponderFromView(_ view: UIView, recursive: Bool) { }
@available(*, unavailable, renamed: "removeResponderSubviews(of:recursive:)")
func removeResponderFromView(_ view: UIView, recursive: Bool = true) { }
}
// swiftlint:enable unused_setter_value
@available(iOSApplicationExtension, unavailable)
@MainActor
fileprivate extension UIView {
func responderSubviews(recursive: Bool) -> [any IQTextInputView] {
// Array of TextInputViews.
var textInputViews: [any IQTextInputView] = []
for view in subviews {
if let view = view as? IQTextInputView {
textInputViews.append(view)
}
// Sometimes there are hidden or disabled views and textInputView inside them still recorded,
// so we added some more validations here (Bug ID: #458)
// Uncommented else (Bug ID: #625)
else if recursive, !view.subviews.isEmpty {
let deepResponders = view.responderSubviews(recursive: recursive)
textInputViews.append(contentsOf: deepResponders)
}
}
// subviews are returning in opposite order. Sorting according the frames 'y'.
return textInputViews.sorted(by: { (view1: any IQTextInputView, view2: any IQTextInputView) -> Bool in
let frame1: CGRect = view1.convert(view1.bounds, to: self)
let frame2: CGRect = view2.convert(view2.bounds, to: self)
if frame1.minY != frame2.minY {
return frame1.minY < frame2.minY
} else {
return frame1.minX < frame2.minX
}
})
}
}

View File

@ -0,0 +1,56 @@
//
// IQTextInputViewInfoModel.swift
// https://github.com/hackiftekhar/IQKeyboardReturnManager
// 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 final class IQTextInputViewInfoModel: NSObject {
weak var textFieldDelegate: (any UITextFieldDelegate)?
weak var textViewDelegate: (any UITextViewDelegate)?
weak var textInputView: (any IQTextInputView)?
let originalReturnKeyType: UIReturnKeyType
init(textInputView: any IQTextInputView) {
self.textInputView = textInputView
self.originalReturnKeyType = textInputView.returnKeyType
if let textInputView = textInputView as? UITextField {
self.textFieldDelegate = textInputView.delegate
} else if let textInputView = textInputView as? UITextView {
self.textViewDelegate = textInputView.delegate
}
super.init()
}
func restore() {
textInputView?.returnKeyType = originalReturnKeyType
if let textInputView = textInputView as? UITextField {
textInputView.delegate = textFieldDelegate
} else if let textInputView = textInputView as? UITextView {
textInputView.delegate = textViewDelegate
}
}
}

21
Pods/IQKeyboardReturnManager/LICENSE generated Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Mohd 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.

68
Pods/IQKeyboardReturnManager/README.md generated Normal file
View File

@ -0,0 +1,68 @@
# IQKeyboardReturnManager
[![CI Status](https://img.shields.io/travis/hackiftekhar/IQKeyboardReturnManager.svg?style=flat)](https://travis-ci.org/hackiftekhar/IQKeyboardReturnManager)
[![Version](https://img.shields.io/cocoapods/v/IQKeyboardReturnManager.svg?style=flat)](https://cocoapods.org/pods/IQKeyboardReturnManager)
[![License](https://img.shields.io/cocoapods/l/IQKeyboardReturnManager.svg?style=flat)](https://cocoapods.org/pods/IQKeyboardReturnManager)
[![Platform](https://img.shields.io/cocoapods/p/IQKeyboardReturnManager.svg?style=flat)](https://cocoapods.org/pods/IQKeyboardReturnManager)
![Screenshot](https://raw.githubusercontent.com/hackiftekhar/IQKeyboardReturnManager/master/Screenshot/IQKeyboardReturnManagerScreenshot.png)
## Example
To run the example project, clone the repo, and run `pod install` from the Example directory first.
## Requirements
## Installation
IQKeyboardReturnManager is available through [CocoaPods](https://cocoapods.org). To install
it, simply add the following line to your Podfile:
```ruby
pod 'IQKeyboardReturnManager'
```
## Usage
To handle keyboard return key automatically:-
```swift
import IQKeyboardReturnManager
class ViewController: UIViewController {
let returnManager: IQKeyboardReturnManager = .init()
override func viewDidLoad() {
super.viewDidLoad()
// This will add all textInputView of the controller and start observing for textFieldShouldReturn events
returnManager.addResponderSubviews(of: self.view, recursive: true)
// If you would like to dismiss the UITextView on tapping on return then add this
returnManager.dismissTextViewOnReturn = true
// If you would like to change last textInputView return key type to done or something else, then add this
returnManager.lastTextInputViewReturnKeyType = .done
// If you would like to customize the navigation between textField by your own order then add them manually
returnManager.add(textInputView: textField1)
returnManager.add(textInputView: textField2)
returnManager.add(textInputView: textField3)
returnManager.add(textInputView: textField4)
}
}
// IQKeyboardReturnManager will forward all delegate callbacks to you, so you can customize the decisions
extension ViewController: UITextFieldDelegate {
@objc public func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {...}
}
```
## Author
Iftekhar Qurashi hack.iftekhar@gmail.com
## License
IQKeyboardReturnManager is available under the MIT license. See the LICENSE file for more info.