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

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,243 @@
//
// IQKeyboardInfo.swift
// https://github.com/hackiftekhar/IQKeyboardNotification
// 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)
public struct IQKeyboardInfo: Equatable {
nonisolated public static func == (lhs: Self, rhs: Self) -> Bool {
lhs.event == rhs.event &&
lhs.endFrame.equalTo(rhs.endFrame)
}
@objc public enum Event: Int, CaseIterable {
case willShow
case didShow
case willChangeFrame
case didChangeFrame
case willHide
case didHide
@MainActor
public var notification: Notification.Name {
switch self {
case .willShow:
return UIResponder.keyboardWillShowNotification
case .didShow:
return UIResponder.keyboardDidShowNotification
case .willChangeFrame:
return UIResponder.keyboardWillChangeFrameNotification
case .didChangeFrame:
return UIResponder.keyboardDidChangeFrameNotification
case .willHide:
return UIResponder.keyboardWillHideNotification
case .didHide:
return UIResponder.keyboardDidHideNotification
}
}
}
public let event: Event
/// `keyboardIsLocalUserInfoKey`.
public let isLocal: Bool
/// `UIKeyboardFrameBeginUserInfoKey`.
public let beginFrame: CGRect
/// `UIKeyboardFrameEndUserInfoKey`.
public let endFrame: CGRect
/// `UIKeyboardAnimationDurationUserInfoKey`.
public let animationDuration: TimeInterval
/// `UIKeyboardAnimationCurveUserInfoKey`.
public let animationCurve: UIView.AnimationCurve
public var animationOptions: UIView.AnimationOptions {
return UIView.AnimationOptions(rawValue: UInt(animationCurve.rawValue << 16))
}
public var isVisible: Bool {
endFrame.height > 0
}
internal init(notification: Notification?, event: Event) {
self.event = event
let screenBounds: CGRect
if let screen: UIScreen = notification?.object as? UIScreen {
screenBounds = screen.bounds
} else {
screenBounds = UIScreen.main.bounds
}
if let info: [AnyHashable: Any] = notification?.userInfo {
if let value = info[UIResponder.keyboardIsLocalUserInfoKey] as? Bool {
isLocal = value
} else {
isLocal = true
}
// Getting keyboard animation.
if let curveValue: Int = info[UIResponder.keyboardAnimationCurveUserInfoKey] as? Int,
let curve: UIView.AnimationCurve = UIView.AnimationCurve(rawValue: curveValue) {
animationCurve = curve
} else {
animationCurve = .easeOut
}
// Getting keyboard animation duration
if let duration: TimeInterval = info[UIResponder.keyboardAnimationDurationUserInfoKey] as? TimeInterval,
duration != 0.0 {
animationDuration = duration
} else {
animationDuration = 0.25
}
if let beginKeyboardFrame: CGRect = info[UIResponder.keyboardFrameBeginUserInfoKey] as? CGRect {
beginFrame = Self.getKeyboardFrame(of: beginKeyboardFrame, inScreenBounds: screenBounds)
} else {
beginFrame = CGRect(x: 0, y: screenBounds.height, width: screenBounds.width, height: 0)
}
var endFrame: CGRect
if let endKeyboardFrame: CGRect = info[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect {
endFrame = Self.getKeyboardFrame(of: endKeyboardFrame, inScreenBounds: screenBounds)
} else {
endFrame = CGRect(x: 0, y: screenBounds.height, width: screenBounds.width, height: 0)
}
// With iOS 26, the endFrame returns height equal to the input accessory view height even when keyboard is dismissed.
// Unfortunately we have to assume this hack for iOS 26.
#if compiler(>=6.2) // Xcode 26
if #available(iOS 26.0, *) {
switch event {
case .willHide, .didHide:
endFrame.origin.y += endFrame.size.height
endFrame.size.height = 0
case .willShow, .didShow, .willChangeFrame, .didChangeFrame:
break
}
}
#endif
self.endFrame = endFrame
} else {
isLocal = true
animationCurve = .easeOut
animationDuration = 0.25
beginFrame = CGRect(x: 0, y: screenBounds.height, width: screenBounds.width, height: 0)
endFrame = CGRect(x: 0, y: screenBounds.height, width: screenBounds.width, height: 0)
}
}
@MainActor
public func animate(alongsideTransition transition: @escaping () -> Void, completion: (() -> Void)? = nil) {
// if let timing = UIView.AnimationCurve.RawValue(exactly: animationCurve.rawValue),
// let curve = UIView.AnimationCurve(rawValue: timing) {
// let animator = UIViewPropertyAnimator(duration: animationDuration, curve: curve) {
// transition()
// }
// animator.addCompletion { _ in
// completion?()
// }
// animator.isUserInteractionEnabled = true
// animator.startAnimation()
// } else {
var animationOptions: UIView.AnimationOptions = self.animationOptions
animationOptions.formUnion(.allowUserInteraction)
animationOptions.formUnion(.beginFromCurrentState)
UIView.animate(withDuration: animationDuration, delay: 0,
options: animationOptions,
animations: transition,
completion: { _ in
completion?()
})
// }
}
}
@available(iOSApplicationExtension, unavailable)
private extension IQKeyboardInfo {
static func getKeyboardFrame(of rect: CGRect, inScreenBounds screenBounds: CGRect) -> CGRect {
var finalFrame: CGRect = rect
// If this is floating keyboard
if finalFrame.width < screenBounds.width,
finalFrame.maxY < screenBounds.height {
finalFrame.size = CGSize(width: finalFrame.size.width, height: 0)
} else {
// (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 keyboardHeight = CGFloat.maximum(screenBounds.height - finalFrame.minY, 0)
finalFrame.size = CGSize(width: finalFrame.size.width, height: keyboardHeight)
}
return finalFrame
}
}
@available(iOSApplicationExtension, unavailable)
@objcMembers public class IQKeyboardInfoObjC: NSObject {
private let wrappedValue: IQKeyboardInfo
public var event: IQKeyboardInfo.Event { wrappedValue.event }
public var isLocal: Bool { wrappedValue.isLocal }
public var beginFrame: CGRect { wrappedValue.beginFrame }
public var endFrame: CGRect { wrappedValue.endFrame }
public var animationDuration: TimeInterval { wrappedValue.animationDuration }
public var animationCurve: UIView.AnimationCurve { wrappedValue.animationCurve }
public var animationOptions: UIView.AnimationOptions { wrappedValue.animationOptions }
public var isVisible: Bool { wrappedValue.isVisible }
init(wrappedValue: IQKeyboardInfo){
self.wrappedValue = wrappedValue
}
@MainActor
public func animate(alongsideTransition transition: @escaping () -> Void, completion: (() -> Void)? = nil) {
wrappedValue.animate(alongsideTransition: transition, completion: completion)
}
}
// MARK: Deprecated
@available(iOSApplicationExtension, unavailable)
public extension IQKeyboardInfo {
@available(*, unavailable, renamed: "event")
var name: Event { event }
@available(*, unavailable, renamed: "isVisible")
var keyboardShowing: Bool { isVisible }
}

View File

@ -0,0 +1,172 @@
//
// IQKeyboardNotification.swift
// https://github.com/hackiftekhar/IQKeyboardNotification
// 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 Combine
@available(iOSApplicationExtension, unavailable)
@MainActor
@objcMembers public final class IQKeyboardNotification: NSObject {
private var storage: Set<AnyCancellable> = []
private var eventObservers: [IQKeyboardInfo.Event: [AnyHashable: SizeCompletion]] = [:]
public private(set) var oldKeyboardInfo: IQKeyboardInfo
public private(set) var keyboardInfo: IQKeyboardInfo {
didSet {
guard keyboardInfo != oldValue else { return }
oldKeyboardInfo = oldValue
sendKeyboardInfo(info: keyboardInfo)
}
}
public var isVisible: Bool {
keyboardInfo.isVisible
}
public var frame: CGRect {
keyboardInfo.endFrame
}
public override init() {
keyboardInfo = IQKeyboardInfo(notification: nil, event: .didHide)
oldKeyboardInfo = keyboardInfo
super.init()
// Registering for keyboard notification.
for event in IQKeyboardInfo.Event.allCases {
NotificationCenter.default.publisher(for: event.notification)
.map({ IQKeyboardInfo(notification: $0, event: event) })
.sink(receiveValue: { [weak self] keyboardInfo in
self?.keyboardInfo = keyboardInfo
})
.store(in: &storage)
}
}
public func animate(alongsideTransition transition: @escaping () -> Void, completion: (() -> Void)? = nil) {
keyboardInfo.animate(alongsideTransition: transition, completion: completion)
}
}
@available(iOSApplicationExtension, unavailable)
@MainActor
public extension IQKeyboardNotification {
typealias SizeCompletion = (_ event: IQKeyboardInfo.Event, _ endFrame: CGRect) -> Void
func subscribe(for events: [IQKeyboardInfo.Event],
identifier: AnyHashable, changeHandler: @escaping SizeCompletion) {
for event in events {
var existingObservers: [AnyHashable: SizeCompletion] = eventObservers[event] ?? [:]
existingObservers[identifier] = changeHandler
eventObservers[event] = existingObservers
}
// If current event is the one user is subscribed to, then call changeHandler immediately for the first time.
if events.contains(keyboardInfo.event) {
changeHandler(keyboardInfo.event, keyboardInfo.endFrame)
}
}
func unsubscribe(for events: [IQKeyboardInfo.Event], identifier: AnyHashable) {
for event in events {
var existingObservers: [AnyHashable: SizeCompletion] = eventObservers[event] ?? [:]
existingObservers[identifier] = nil
eventObservers[event] = existingObservers
}
}
func isSubscribed(for event: IQKeyboardInfo.Event? = nil, identifier: AnyHashable) -> Bool {
if let event = event {
guard let observers = eventObservers[event], !observers.isEmpty else { return false }
return observers[identifier] != nil
} else {
for event in IQKeyboardInfo.Event.allCases {
let observers = eventObservers[event] ?? [:]
if observers[identifier] != nil {
return true
}
}
return false
}
}
private func sendKeyboardInfo(info: IQKeyboardInfo) {
guard let observers = eventObservers[info.event], !observers.isEmpty else { return }
let endFrame: CGRect = info.endFrame
for block in observers.values {
block(info.event, endFrame)
}
}
}
@available(iOSApplicationExtension, unavailable)
@MainActor
@objc public extension IQKeyboardNotification {
var oldKeyboardInfoObjc: IQKeyboardInfoObjC { IQKeyboardInfoObjC(wrappedValue: oldKeyboardInfo) }
var keyboardInfoObjc: IQKeyboardInfoObjC { IQKeyboardInfoObjC(wrappedValue: keyboardInfo) }
func subscribe(identifier: AnyHashable, changeHandler: @escaping SizeCompletion) {
subscribe(for: IQKeyboardInfo.Event.allCases, identifier: identifier, changeHandler: changeHandler)
}
func unsubscribe(identifier: AnyHashable) {
unsubscribe(for: IQKeyboardInfo.Event.allCases, identifier: identifier)
}
func isSubscribed(identifier: AnyHashable) -> Bool {
isSubscribed(for: nil, identifier: identifier)
}
}
// MARK: Deprecated
@available(iOSApplicationExtension, unavailable)
@objc public extension IQKeyboardNotification {
// MARK: Deprecated
@available(*, unavailable, renamed: "isVisible")
var keyboardShowing: Bool { isVisible }
@available(*, unavailable, renamed: "subscribe(identifier:changeHandler:)")
func registerSizeChange(identifier: AnyHashable, changeHandler: @escaping SizeCompletion) {
subscribe(identifier: identifier, changeHandler: changeHandler)
}
@available(*, unavailable, renamed: "unsubscribe(identifier:)")
func unregisterSizeChange(identifier: AnyHashable) {
unsubscribe(identifier: identifier)
}
}

21
Pods/IQKeyboardNotification/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.

66
Pods/IQKeyboardNotification/README.md generated Normal file
View File

@ -0,0 +1,66 @@
# IQKeyboardNotification
Lightweight library to observe keyboard events with ease.
[![CI Status](https://img.shields.io/travis/hackiftekhar/IQKeyboardNotification.svg?style=flat)](https://travis-ci.org/hackiftekhar/IQKeyboardNotification)
[![Version](https://img.shields.io/cocoapods/v/IQKeyboardNotification.svg?style=flat)](https://cocoapods.org/pods/IQKeyboardNotification)
[![License](https://img.shields.io/cocoapods/l/IQKeyboardNotification.svg?style=flat)](https://cocoapods.org/pods/IQKeyboardNotification)
[![Platform](https://img.shields.io/cocoapods/p/IQKeyboardNotification.svg?style=flat)](https://cocoapods.org/pods/IQKeyboardNotification)
![Screenshot](https://raw.githubusercontent.com/hackiftekhar/IQKeyboardNotification/master/Screenshot/IQKeyboardNotificationScreenshot.png)
## Example
To run the example project, clone the repo, and run `pod install` from the Example directory first.
## Requirements
## Installation
IQKeyboardNotification is available through [CocoaPods](https://cocoapods.org). To install
it, simply add the following line to your Podfile:
```ruby
pod 'IQKeyboardNotification'
```
## Usage
To observe keyboard events, subscribe to the keyboard events:-
```swift
import IQKeyboardNotification
class ViewController: UIViewController {
private let keyboard: IQKeyboardNotification = .init()
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// Subscribe
keyboard.subscribe(identifier: "YOUR_UNIQUE_IDENTIFIER") { event, frame in
print(frame)
// Write your own logic here based on event and keyboard frame
}
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
// Unsubscribe
keyboard.unsubscribe(identifier: "YOUR_UNIQUE_IDENTIFIER")
}
}
```
## Author
Iftekhar Qurashi hack.iftekhar@gmail.com
## Flow
![Screenshot](https://raw.githubusercontent.com/hackiftekhar/IQKeyboardNotification/master/Screenshot/FlowDiagram.jpg)
## License
IQKeyboardNotification is available under the MIT license. See the LICENSE file for more info.