Compare commits

38 Commits

Author SHA1 Message Date
413a9603a4 fix: 修复首页位置上报图标渲染 2026-07-29 15:14:05 +08:00
f87b13cc05 feat: 支持模拟器定位与地图 fallback 2026-07-24 10:04:55 +08:00
0e0415eec4 feat: 添加获客员分成记录并重构列表 2026-07-23 17:43:13 +08:00
7ccbc5568d fix: 优化旅拍相册入口与二维码提示布局 2026-07-23 15:40:51 +08:00
787a166263 feat: 品牌化那拉提景区收款页 2026-07-23 15:39:34 +08:00
74f7370aab docs: 补充 type=13 新获客线索推送样本与字段说明。
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-23 10:56:40 +08:00
1b2637f0ee fix: 推送点击仅按 type 路由到收款页或消息中心。
对齐后端消息类型约定,去掉 route/uri 等兼容字段,并补充联调日志与单测。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-22 18:20:36 +08:00
caeeb9a1cf feat: 接入消息未读数、全部已读与首页红点角标同步。
固定消息中心为首页入口,并在推送到达、已读后刷新桌面角标。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-21 18:02:43 +08:00
e7f1d777dd feat: integrate JPush notifications 2026-07-20 15:56:06 +08:00
573c9a42ae Archive 时收集阿里云 NUI dSYM,并 bump build 号到 1010402。
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-17 09:17:27 +08:00
b019e1e494 为旅拍 OTG 上传增加 client_photo_id,并按服务端已登记 ID 同步本地上传状态。
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-16 19:13:19 +08:00
3d5ad8a614 fix: 固定首页常用应用图标尺寸 2026-07-16 19:02:00 +08:00
5f1c22c6a8 用内置有线传输帮助页替换飞书外链。
帮助文档改为应用内 WebView 加载本地 HTML,避免依赖外部平台内容。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-16 09:06:16 +08:00
ce6de76055 完善账号切换展示、首页菜单图标与有线传输设置 chip。
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-15 14:59:31 +08:00
5eef31b8da 完善景区排队设置页与全局按钮配置迁移。
重构排队设置与变更日志交互,补充 Overlay 与资源,并将多页面主操作按钮统一到 UIButton Configuration,同步更新相关单元测试。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-14 16:51:23 +08:00
6336feff27 完善景区排队设置页与全局按钮配置迁移。
重构排队设置与变更日志交互,补充 Overlay 与资源,并将多页面主操作按钮统一到 UIButton Configuration,同步更新相关单元测试。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-14 16:21:53 +08:00
efb3925257 完善首页权限申请流程与队列播报体验。
对齐 Android 无权限强制弹窗、角色下拉与申请页交互,补充 UIButton configuration 适配,并增强景区排队 TTS 与相关单元测试。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-14 13:46:45 +08:00
c083f1d4b3 升级 AppStore 缓存 Key 并在覆盖安装时强制重新登录。
统一账号作用域 Key 对齐 Android,新增缓存 schema 迁移清理旧数据,并同步更新相关模块与单元测试。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-14 09:35:55 +08:00
05804ba7d6 完善钱包提现状态展示与举报摄影师列表交互。
对齐 Android 提现进度与时间线映射,优化钱包与积分兑换页刷新逻辑,并补充相关单元测试与真机测试说明。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-13 16:51:32 +08:00
5138c1c11a 完善实名认证、钱包与打卡点模块 UI 与业务逻辑。
对齐 Android 实名认证流程与审核页、钱包提现/积分兑换界面,优化打卡点列表与详情交互,并补充相关资源、主题 token 与单元测试。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-13 16:28:10 +08:00
07b2b9d459 修复样片详情解析并优化列表上下架交互。
兼容 Android 媒体项 size 字段格式,增强灵活解码容错,并统一样片列表开关确认逻辑与安全区布局。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-13 15:14:40 +08:00
afcd412f11 完善样片管理列表、详情与上传页 UI 交互。
对齐 Android 样片模块布局与加载状态,补充共享 UI 组件与图片资源,并增强列表解析容错与单元测试覆盖。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-13 15:00:18 +08:00
cbf2db649e 完善素材管理 UI 交互并修复列表解析与时间选择。
MaterialItem 兼容 Android 缺失字段默认值,优化素材详情/列表/标签布局与上下架开关交互,修复个人空间时间选择器初始值。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-13 13:34:45 +08:00
7deeedec68 修复素材上传 OSS 路径并完善调试与表单交互。
对齐 Android material/{scenicId}/0/ 目录结构以修复视频元数据写入失败,补充 API 请求体调试日志、素材拍摄麦克风权限与上传控件点击穿透修复。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-13 11:19:58 +08:00
005349f8e6 模块化 AppStore 并完善素材管理与个人空间设置。
将会话、权限、位置、收款与排队存储拆分为独立模块,同步更新各 ViewModel 与单元测试,并补充素材管理 UI 与个人空间设置交互。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-13 11:01:08 +08:00
ceca780ab3 同步相册云盘界面并完善相关交互 2026-07-10 17:15:15 +08:00
ab5220e460 完善任务流程、键盘适配与页面交互 2026-07-10 15:56:15 +08:00
f88a85a807 修复任务提交与弹窗交互问题 2026-07-10 14:51:34 +08:00
df547a16dc 优化首页位置上报体验 2026-07-10 11:44:56 +08:00
c03a625409 Improve travel album list selection layout 2026-07-10 11:33:44 +08:00
b6d3207665 Use system navigation bar on OTG page 2026-07-10 11:20:14 +08:00
98dc810455 Update OTG transfer workflow 2026-07-10 11:02:00 +08:00
9d446b4bc3 Fix OTG batch selection cell refresh 2026-07-10 11:00:21 +08:00
f20ec7f06c feat: 添加云盘与消息中心功能 2026-07-09 22:37:43 +08:00
8e356973bd feat: update app workflows and permissions 2026-07-09 17:34:00 +08:00
43e6133c21 feat: update wallet punch point and report success 2026-07-09 14:27:18 +08:00
2970f1514b 完善风险地图定位与标记详情 2026-07-09 12:47:47 +08:00
42aca73588 feat: 更新素材管理和举报风险地图 2026-07-09 12:20:02 +08:00
654 changed files with 64753 additions and 6021 deletions

View File

@ -71,7 +71,7 @@ Swift · UIKit · SnapKit代码布局约束写在 `setupUI()` / `setupCons
## 单元测试
每增一功能必须附带单元测试且全部通过`Cmd + U` / `xcodebuild test`),与功能同次提交
每增一功能必须附带单元测试且全部通过,与功能同次提交。单元测试必须在已连接的 iPhone 真机上运行,不使用模拟器;支持使用与开发机处于同一 Wi-Fi 下并已启用无线连接的真机。可在 Xcode 中选择真机后执行 `Cmd + U`,或使用 `xcodebuild test -destination 'platform=iOS,id=<设备 UDID>'`
| 层级 | 优先级 |
|------|--------|

View File

@ -0,0 +1,169 @@
# AppStore 缓存 Key 升级对照
## 结论
- 旧线上工程与重构版工程的主应用 Bundle ID 均为 `com.yuanzhixiang.suixinkan`。通过 App Store 覆盖升级时,系统会保留原应用容器,因此旧版 `UserDefaults` 会继续存在。
- 本次升级策略为:**强制重新登录、旧偏好全部重置、不迁移旧缓存**。
- 新 Key 只对齐 Android 的业务作用域iOS 统一使用 `_` 作为分隔符,不复制 Android MMKV 历史实现中无分隔符拼接的写法。
- 本文中的“旧线上工程”为 `/Users/hanqiu/Desktop/android_project/suixinkan_ios`,“重构版工程”为 `/Users/hanqiu/Desktop/suixinkan`
## Key 占位符
| 占位符 | 含义 |
|---|---|
| `O` | 旧版将 `[accountType, userId, storeId, scenicId]` 中的非空值用 `_` 拼接,再清洗非字母数字字符后得到的 accountKey |
| `P` | 本次实施前重构版使用的 `<userId>_<accountType>` 账号前缀;用户 ID 为空时可能退化为 `guest` |
| `A` | 本次实施后统一使用的 `<accountType>_<userId>` 账号前缀 |
| `S` | 景区 IDscenicId |
| `T` | 排队点位 IDspotId |
| `R` | 角色编码roleCode |
> `suixinkan.session.v1` 是旧工程中真实存在的单个 `UserDefaults` Key其值为 JSON Data。表格中的 `suixinkan.session.v1[token]` 等写法仅表示该 JSON 内的字段,不是独立的 `UserDefaults` Key。
## 新旧工程统一业务 Key 对照
| 业务数据 | 旧线上工程 Key | 重构版实施前 Key | 统一后的 Key | 升级处理 |
|---|---|---|---|---|
| 登录会话 | `suixinkan.session.v1` JSON | 拆分为多个 `key_in_*` | 保持拆分结构 | 删除旧会话,强制登录 |
| Token | `suixinkan.session.v1[token]` | `key_in_token` | `key_in_token` | 首次升级清空 |
| 用户 ID | `suixinkan.session.v1[userId]` | `key_in_user_id` | `key_in_user_id` | 首次升级清空 |
| 用户名 | `suixinkan.session.v1[userName]` | `key_in_user_name` | `key_in_user_name` | 首次升级清空 |
| 头像 | `suixinkan.session.v1[avatar]` | `key_in_avatar` | `key_in_avatar` | 首次升级清空 |
| 手机号 | `suixinkan.session.v1[phone]` | `key_in_phone` | `key_in_phone` | 首次升级清空 |
| 账号类型 | `suixinkan.session.v1[accountType]` | `key_in_account_type` | `key_in_account_type` | 首次升级清空 |
| 账号类型显示名称 | `suixinkan.session.v1[accountDisplayName]` | `key_in_account_display_name` | `key_in_account_display_name` | 首次升级清空 |
| 角色 ID | `suixinkan.session.v1[roleId]` | `P_key_in_role_id` | `A_key_in_role_id` | 不迁移;当前仅用于 legacy 权限匹配兜底 |
| 角色编码 | 无独立 Key | `key_in_role_code` | `A_key_in_role_code` | 改为账号作用域 |
| 角色名称 | `suixinkan.session.v1[roleName]` | `key_in_role_name` | `A_key_in_role_name` | 改为账号作用域 |
| 权限列表 | 无 | `P_key_role_permission_list` | `A_key_role_permission_list` | 不迁移预发布数据 |
| 当前权限 | 无 | `P_key_in_permission` | `A_key_in_permission` | 不迁移预发布数据 |
| 角色景区列表 | 无 | `P_key_current_role_scenic_list` | `A_key_current_role_scenic_list` | 不迁移预发布数据 |
| 当前景区 ID | `suixinkan.session.v1[scenicId]` | `key_in_current_scenic_id` | `A_key_in_current_scenic_id` | 改为账号作用域 |
| 当前景区名称 | `suixinkan.session.v1[scenicName]` | `key_in_current_scenic_name` | `A_key_in_current_scenic_name` | 改为账号作用域 |
| 当前门店 ID | `suixinkan.session.v1[storeId]` | `key_in_current_store_id` | `A_scenic_S_scenic_store_id` | 改为账号+景区作用域 |
| 当前门店名称 | `suixinkan.session.v1[storeName]` | 无 | 不再持久化 | 删除旧值 |
| 常用菜单 | `home.common.menu.uris` | `P_role_R_common_uris` | `A_role_R_common_uris` | 不迁移旧菜单仅统一作用域Android 精确后缀不同 |
| 收款语音 | `payment.voice.broadcast.enabled` | `P_key_is_open_receive_voice` | `A_key_is_open_receive_voice` | 重置为默认值 |
| 在线状态 | 无 | `P_key_online_status` | `A_key_online_status` | 改为账号作用域 |
| 上次位置上报时间 | 无 | `P_key_last_location_report_time` | `A_key_last_location_report_time` | 改为账号作用域 |
| 定位提醒分钟数 | 无 | `P_key_location_reminder_minutes` | `A_key_location_reminder_minutes` | 改为账号作用域 |
| 当前排队点位 ID | `account_O_scenic_S_scenic_queue_selected_spot_id` | `P__scenic_queue_punch_spot_id` | `A_scenic_S_scenic_queue_punch_spot_id` | 不迁移 |
| 当前排队点位名称 | `account_O_scenic_S_scenic_queue_selected_spot_name` | `P__scenic_queue_punch_spot_name` | `A_scenic_S_spot_T_scenic_queue_punch_spot_name` | 不迁移 |
| 排队播报备注 | `account_O_scenic_S_scenic_queue_custom_tts_text_spot_T` | `P__scenic_queue_remark` | `A_scenic_S_spot_T_scenic_queue_remark` | 不迁移 |
| 排队设置快照 | `account_O_scenic_S_scenic_queue_settings_snapshot_spot_T` | `P__scenic_queue_settings_snapshot` | `A_scenic_S_spot_T_scenic_queue_settings_snapshot` | 不迁移 |
| 排队预设语音 | `account_O_scenic_S_scenic_queue_preset_voices_spot_T` | `P__scenic_queue_preset_voices_v1` | `A_scenic_S_spot_T_scenic_queue_preset_voices_v1` | 不迁移 |
| 离线 TTS | 无 | `P__scenic_queue_offline_tts_v1` | `A_scenic_S_scenic_queue_offline_tts_v1` | 默认值对齐 Android 为 `true` |
## 首次升级需要清理的旧 Key
已新增整型版本标记 `suixinkan.cache.schema.version`。当版本小于目标缓存版本时执行一次清理,全部清理完成后再写入目标版本,避免每次启动重复清空用户重新登录后产生的数据。
### 会话与推送
- `suixinkan.session.v1`
- `apns_device_token`
- `apns_uploaded_token`
同时清空重构版预发布阶段可能存在的登录与账号字段,以确保升级后强制重新登录:
- `key_in_token`
- `key_last_login_username`
- `key_in_user_id`
- `key_in_user_name`
- `key_in_real_name`
- `key_in_avatar`
- `key_in_phone`
- `key_in_account_type`
- `key_in_account_display_name`
- `key_in_role_id`
- `key_in_role_code`
- `key_in_role_name`
- `key_in_current_scenic_id`
- `key_in_current_scenic_name`
- `key_in_current_store_id`
`key_privacy_agreement_accepted` 是否清理应服从“旧偏好全部重置”的既定策略,因此本次也应清理。
同时清理早期重构实现可能写入的以下未分域业务字段:
- `key_role_permission_list`
- `key_in_permission`
- `key_current_role_scenic_list`
- `key_online_status`
- `key_last_location_report_time`
- `key_location_reminder_minutes`
- `key_is_open_receive_voice`
### 首页与收款
- `home.common.menu.uris`
- `home.common.menu.android.baseline.v2`
- `home.routing.unknown.records`
- `payment.voice.broadcast.enabled`
### 排队全局偏好
- `scenic_queue_tts_enabled`
- `scenic_queue_background_poll_enabled`
- `scenic_queue_selected_spot_id`
- `scenic_queue_selected_spot_name`
- `scenic_queue_custom_tts_text`
- `scenic_queue_photo_estimate_seconds`
- `scenic_queue_broadcast_interval_seconds`
- `scenic_queue_countdown_threshold_seconds`
- `scenic_queue_show_start_shooting_button`
- `scenic_queue_auto_call_ahead_count`
- `scenic_queue_quick_call_button_enabled`
- `scenic_queue_prepare_call_button_enabled`
- `scenic_queue_config_logs`
- `scenic_queue_settings_snapshot`
- `scenic_queue_preset_voices`
### 排队动态缓存
清理符合以下已知格式的旧 Key不应扩大为删除整个 `UserDefaults` domain
- `account_O_scenic_S_scenic_queue_selected_spot_id`
- `account_O_scenic_S_scenic_queue_selected_spot_name`
- `account_O_scenic_S_scenic_queue_custom_tts_text_spot_T`
- `account_O_scenic_S_scenic_queue_settings_snapshot_spot_T`
- `account_O_scenic_S_scenic_queue_preset_voices_spot_T`
- `scenic_S_spot_T_scenic_queue_settings_snapshot`
- `scenic_S_scenic_queue_settings_snapshot`
- `scenic_S_spot_T_scenic_queue_preset_voices`
### 重构版预发布缓存
清理使用旧 `P` 前缀的以下 Key 家族:
- `P_key_in_role_id`
- `P_key_role_permission_list`
- `P_key_in_permission`
- `P_key_current_role_scenic_list`
- `P_key_online_status`
- `P_key_last_location_report_time`
- `P_key_location_reminder_minutes`
- `P_key_is_open_receive_voice`
- `P_role_R_common_uris`
- `P__scenic_queue_punch_spot_id`
- `P__scenic_queue_punch_spot_name`
- `P__scenic_queue_remark`
- `P__scenic_queue_settings_snapshot`
- `P__scenic_queue_offline_tts_v1`
- `P__scenic_queue_preset_voices_v1`
其中 `P__scenic_queue_*` 的双下划线来自当前 Key 生成器和自带前导下划线的后缀叠加,属于需要淘汰的错误格式。
## 不应清理的内容
- 禁止调用 `removePersistentDomain(forName:)` 清空应用的整个 `UserDefaults` domain应只删除明确列出的 Key 和严格匹配的动态 Key。
- 不删除 `Documents/CloudDownloads`。该目录可能包含用户主动下载的文件,不属于普通缓存。
- 不删除 SDK 缓存、图片缓存或其他未列入本次升级范围的用户文件。
- 不做旧 `suixinkan.session.v1`、排队设置、常用菜单或收款偏好的解码与迁移。
## 发布前备注
- 旧线上版本为 `1.0.1 (2)`;当前覆盖升级版本已设置为 `1.1.4 (1010401)`
- 重构版当前未保留旧工程的 APNs entitlement、远程通知后台能力、回调和设备 Token 上传流程。该问题不是缓存 Key 修改,但若线上仍需要推送,发布前必须单独恢复并验证。清理 `apns_device_token``apns_uploaded_token` 后,恢复推送时应重新绑定设备 Token。
- `Application Support/suixinkan/cloud_transfer_tasks.json` 当前为全账号共享文件,存在账号切换后读取其他账号任务的风险。该问题不是 `UserDefaults` Key 迁移的一部分,应作为独立发布风险处理。
- 本方案已在重构版中实施:应用首次创建 `AppStore` 时按 `suixinkan.cache.schema.version` 执行一次定向清理,并切换到表格中的统一 Key不删除未列入范围的 `UserDefaults` 或用户文件。

20
Podfile
View File

@ -1,13 +1,24 @@
platform :ios, '16.0'
project 'suixinkan.xcodeproj',
'Debug' => :debug,
'SimulatorDebug' => :debug,
'Release' => :release
target 'suixinkan' do
use_frameworks!
pod 'AMapLocation-NO-IDFA'
pod 'AMap3DMap-NO-IDFA'
pod 'AMapSearch-NO-IDFA'
# 高德 SDK 未提供 arm64-simulator slice仅在真机配置中接入。
pod 'AMapFoundation-NO-IDFA', :configurations => ['Debug', 'Release']
pod 'AMapLocation-NO-IDFA', :configurations => ['Debug', 'Release']
pod 'AMap3DMap-NO-IDFA', :configurations => ['Debug', 'Release']
pod 'AMapSearch-NO-IDFA', :configurations => ['Debug', 'Release']
pod 'UMCommon'
pod 'UMDevice'
pod 'UMAPM'
# 极光推送:固定无 IDFA 的 JCore避免引入 ATT 授权。
pod 'JCore', '5.4.2-noidfa'
pod 'JPush', '6.1.0'
pod 'IQKeyboardManagerSwift'
# 微信 Open SDK仅接入分享能力不调用支付 API。
pod 'WechatOpenSDK-XCFramework', '~> 2.0.4'
@ -20,6 +31,9 @@ post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '16.0'
if target.name == 'Pods-suixinkanTests' && config.name == 'SimulatorDebug'
config.build_settings['EXCLUDED_ARCHS[sdk=iphonesimulator*]'] = ''
end
end
end
end

View File

@ -6,6 +6,49 @@ PODS:
- AMapFoundation-NO-IDFA (>= 1.9.0)
- AMapSearch-NO-IDFA (9.8.0):
- AMapFoundation-NO-IDFA (>= 1.9.0)
- IQKeyboardCore (1.0.9)
- IQKeyboardManagerSwift (8.0.3):
- IQKeyboardManagerSwift/Appearance (= 8.0.3)
- IQKeyboardManagerSwift/Core (= 8.0.3)
- IQKeyboardManagerSwift/IQKeyboardReturnManager (= 8.0.3)
- IQKeyboardManagerSwift/IQKeyboardToolbarManager (= 8.0.3)
- IQKeyboardManagerSwift/IQTextView (= 8.0.3)
- IQKeyboardManagerSwift/Resign (= 8.0.3)
- IQKeyboardManagerSwift/Appearance (8.0.3):
- IQKeyboardManagerSwift/Core
- IQTextInputViewNotification
- IQKeyboardManagerSwift/Core (8.0.3):
- IQKeyboardNotification
- IQTextInputViewNotification
- IQKeyboardManagerSwift/IQKeyboardReturnManager (8.0.3):
- IQKeyboardReturnManager
- IQKeyboardManagerSwift/IQKeyboardToolbarManager (8.0.3):
- IQKeyboardManagerSwift/Core
- IQKeyboardToolbarManager
- IQKeyboardManagerSwift/IQTextView (8.0.3):
- IQTextView
- IQKeyboardManagerSwift/Resign (8.0.3):
- IQKeyboardManagerSwift/Core
- IQTextInputViewNotification
- IQKeyboardNotification (1.0.7)
- IQKeyboardReturnManager (1.0.6):
- IQKeyboardCore
- IQKeyboardToolbar (1.1.5):
- IQKeyboardToolbar/Core (= 1.1.5)
- IQKeyboardToolbar/Core (1.1.5):
- IQKeyboardCore
- IQKeyboardToolbar/Placeholderable
- IQKeyboardToolbar/Placeholderable (1.1.5)
- IQKeyboardToolbarManager (1.1.6):
- IQKeyboardToolbar
- IQTextInputViewNotification
- IQTextInputViewNotification (1.0.9):
- IQKeyboardCore
- IQTextView (1.0.5):
- IQKeyboardToolbar/Placeholderable
- JCore (5.4.2-noidfa)
- JPush (6.1.0):
- JCore (> 5.1.1)
- UMAPM (2.0.7):
- UMCommon
- UMCommon (7.5.11):
@ -15,8 +58,12 @@ PODS:
DEPENDENCIES:
- AMap3DMap-NO-IDFA
- AMapFoundation-NO-IDFA
- AMapLocation-NO-IDFA
- AMapSearch-NO-IDFA
- IQKeyboardManagerSwift
- JCore (= 5.4.2-noidfa)
- JPush (= 6.1.0)
- UMAPM
- UMCommon
- UMDevice
@ -28,6 +75,16 @@ SPEC REPOS:
- AMapFoundation-NO-IDFA
- AMapLocation-NO-IDFA
- AMapSearch-NO-IDFA
- IQKeyboardCore
- IQKeyboardManagerSwift
- IQKeyboardNotification
- IQKeyboardReturnManager
- IQKeyboardToolbar
- IQKeyboardToolbarManager
- IQTextInputViewNotification
- IQTextView
- JCore
- JPush
- UMAPM
- UMCommon
- UMDevice
@ -38,11 +95,21 @@ SPEC CHECKSUMS:
AMapFoundation-NO-IDFA: a2e3c895398d7ee757278e1a0a8f9359da4b146e
AMapLocation-NO-IDFA: 7cd8fc837ea41edfbf4d937cd20572e277b77d18
AMapSearch-NO-IDFA: c0afd2a69a076d4228becda4401dbe4a279a03ef
IQKeyboardCore: 250a88dfc713b2ecbe1dce250811d054fc016959
IQKeyboardManagerSwift: 7f6a9a1e2285732d3ff5f485ff921215253ea77e
IQKeyboardNotification: 3b9bc812a5af2cf97df08a363d9daa9fc0dc1848
IQKeyboardReturnManager: fcbf51fc68d7536fc1fbcca5231c4e82576b12ac
IQKeyboardToolbar: b0f26e43b9e806e961fde2344fe379ae193236b9
IQKeyboardToolbarManager: 242e858cc74e06e00aa989bea1355b24d4397854
IQTextInputViewNotification: 3b9fb27a16e7ee8958cc9092cfb07a1a9e1fd559
IQTextView: ae13b4922f22e6f027f62c557d9f4f236b19d5c7
JCore: 696e0ac22f417bb0ac4729fc41bc2941aef14e2b
JPush: 891c497687be788b094bde368d51289355ee1112
UMAPM: ba9fbebe8dd2048b251c332ab0afc86e1ba5ea63
UMCommon: d652b3b372a801b36db203f5b546ab3e15676898
UMDevice: 20b7b3c37a36b2c5c7ca8b5a54386ff9ed2b3b19
WechatOpenSDK-XCFramework: ff342ae616bb86df3d236aca38059dfd4bc4a949
PODFILE CHECKSUM: f48a71990c19e75f00e16701179b4f160e74986f
PODFILE CHECKSUM: 5abf70ade4a9c4b66b64877fdea91f1d086bcaf5
COCOAPODS: 1.16.2

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,41 @@
//
// IQKeyboardConstants.swift
// https://github.com/hackiftekhar/IQKeyboardCore
// 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 Foundation
/**
`IQEnableModeDefault`
Pick default settings.
`IQEnableModeEnabled`
setting is enabled.
`IQEnableModeDisabled`
setting is disabled.
*/
@available(iOSApplicationExtension, unavailable)
@objc public enum IQEnableMode: Int {
case `default`
case enabled
case disabled
}

View File

@ -0,0 +1,65 @@
//
// IQKeyboardExtended.swift
// https://github.com/hackiftekhar/IQKeyboardCore
// 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
/// Wrapper for IQKeyboardManager compatible types. This type provides an extension point for
/// convenience methods in IQKeyboardManager.
@available(iOSApplicationExtension, unavailable)
public struct IQKeyboardExtension<Base: AnyObject> {
public private(set) weak var base: Base?
fileprivate init(_ base: Base) {
self.base = base
}
}
// swiftlint:disable identifier_name
/// Represents an object type that is compatible with IQKeyboardManager. You can use `iq` property to get a
/// value in the namespace of IQKeyboardManager.
@available(iOSApplicationExtension, unavailable)
public protocol IQKeyboardExtended {
/// Type being extended.
associatedtype Base: AnyObject
/// Instance IQKeyboardManager extension point.
@MainActor
var iq: IQKeyboardExtension<Base> { get set }
}
// swiftlint:disable unused_setter_value
@available(iOSApplicationExtension, unavailable)
public extension IQKeyboardExtended where Self: UIView {
/// Instance IQKeyboardManager extension point.
@MainActor
var iq: IQKeyboardExtension<Self> {
get { IQKeyboardExtension(self) }
set {}
}
}
// swiftlint:enable unused_setter_value
// swiftlint:enable identifier_name
@available(iOSApplicationExtension, unavailable)
@MainActor
extension UIView: IQKeyboardExtended {}

View File

@ -0,0 +1,99 @@
//
// IQTextInputView.swift
// https://github.com/hackiftekhar/IQKeyboardCore
// 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 protocol IQTextInputView where Self: UIView, Self: UITextInputTraits {
#if compiler(>=5.7) // Xcode 14
@available(iOS 16.0, *)
@objc var iqIsFindInteractionEnabled: Bool { get }
@available(iOS 16.0, *)
@objc var iqFindInteraction: UIFindInteraction? { get }
#endif
@objc var returnKeyType: UIReturnKeyType { get set }
@objc var keyboardAppearance: UIKeyboardAppearance { get set }
@objc var iqIsEnabled: Bool { get }
@objc var inputAccessoryView: UIView? { get set }
}
@available(iOSApplicationExtension, unavailable)
@MainActor
@objc extension UITextField: IQTextInputView {
#if compiler(>=5.7) // Xcode 14
@available(iOS 16.0, *)
public var iqIsFindInteractionEnabled: Bool { false }
@available(iOS 16.0, *)
public var iqFindInteraction: UIFindInteraction? { nil }
#endif
public var iqIsEnabled: Bool { isEnabled }
}
@available(iOSApplicationExtension, unavailable)
@MainActor
@objc extension UITextView: IQTextInputView {
#if compiler(>=5.7) // Xcode 14
@available(iOS 16.0, *)
public var iqIsFindInteractionEnabled: Bool { isFindInteractionEnabled }
@available(iOS 16.0, *)
public var iqFindInteraction: UIFindInteraction? { findInteraction }
#endif
public var iqIsEnabled: Bool { isEditable }
}
@available(iOSApplicationExtension, unavailable)
@MainActor
@objc extension UISearchBar: IQTextInputView {
#if compiler(>=5.7) // Xcode 14
@available(iOS 16.0, *)
public var iqIsFindInteractionEnabled: Bool { false }
@available(iOS 16.0, *)
public var iqFindInteraction: UIFindInteraction? { nil }
#endif
public var iqIsEnabled: Bool {
#if compiler(>=5.8) // Xcode 14.4
if #available(iOS 16.4, *) {
return isEnabled
} else {
return searchTextField.isEnabled
}
#else
return searchTextField.isEnabled
#endif
}
}

View File

@ -0,0 +1,180 @@
//
// UIView+Hierarchy.swift
// https://github.com/hackiftekhar/IQKeyboardCore
// 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
/**
UIView hierarchy category.
*/
@available(iOSApplicationExtension, unavailable)
@MainActor
public extension IQKeyboardExtension where Base: UIView {
// MARK: viewControllers
/**
Returns the UIViewController object that manages the receiver.
*/
func viewContainingController() -> UIViewController? {
var nextResponder: UIResponder? = base
repeat {
nextResponder = nextResponder?.next
if let viewController: UIViewController = nextResponder as? UIViewController {
return viewController
}
} while nextResponder != nil
return nil
}
@available(*, unavailable, message: "This has been removed because it no longer needed")
func topMostController() -> UIViewController? { nil }
}
@available(iOSApplicationExtension, unavailable)
@MainActor
public extension IQKeyboardExtension where Base: UIView {
// MARK: Superviews
/**
Returns the superView of provided class type.
@param classType class type of the object which is to be search in above hierarchy and return
@param belowView view object in upper hierarchy where method should stop searching and return nil
*/
func superviewOf<T: UIView>(type classType: T.Type, belowView: UIView? = nil) -> T? {
var superView: UIView? = base?.superview
while let aSuperview: UIView = superView {
if aSuperview.isKind(of: classType) {
// If it's UIScrollView, then validating for special cases
if aSuperview is UIScrollView {
let classNameString: String = "\(type(of: aSuperview.self))"
// If it's not UITableViewWrapperView class,
// this is internal class which is actually manage in UITableview.
// The speciality of this class is that it's superview is UITableView.
// If it's not UITableViewCellScrollView class,
// this is internal class which is actually manage in UITableviewCell.
// The speciality of this class is that it's superview is UITableViewCell.
// If it's not _UIQueuingScrollView class,
// actually we validate for _ prefix which usually used by Apple internal classes
if !(aSuperview.superview is UITableView),
!(aSuperview.superview is UITableViewCell),
!classNameString.hasPrefix("_") {
return superView as? T
}
} else {
return superView as? T
}
} else if aSuperview == belowView {
return nil
}
superView = aSuperview.superview
}
return nil
}
// MARK: Special TextFields
/**
Returns searchBar if receiver object is UISearchBarTextField, otherwise return nil.
*/
func textFieldSearchBar() -> UISearchBar? {
if base is UISearchTextField,
var textInputViewController = base?.iq.viewContainingController() {
if let navController: UINavigationController = textInputViewController as? UINavigationController,
let topController: UIViewController = navController.topViewController {
textInputViewController = topController
}
// Not adjusting for searchTextField inside searchController.
if textInputViewController.navigationItem.searchController?.searchBar.searchTextField == base {
return textInputViewController.navigationItem.searchController?.searchBar
}
} else {
var responder: UIResponder? = base?.next
while let bar: UIResponder = responder {
if let searchBar: UISearchBar = bar as? UISearchBar {
return searchBar
} else if bar is UIViewController {
break
}
responder = bar.next
}
}
return nil
}
/**
Returns YES if the receiver object is UIAlertSheetTextField, otherwise return NO.
*/
func isAlertViewTextField() -> Bool {
var alertViewController: UIResponder? = viewContainingController()
var isAlertViewTextField: Bool = false
while let controller: UIResponder = alertViewController, !isAlertViewTextField {
if controller is UIAlertController {
isAlertViewTextField = true
break
}
alertViewController = controller.next
}
return isAlertViewTextField
}
}
@available(iOSApplicationExtension, unavailable)
@MainActor
@objc public extension UIView {
@available(*, unavailable, renamed: "iq.viewContainingController()")
func viewContainingController() -> UIViewController? { nil }
@available(*, unavailable, message: "This has been removed because it no longer needed")
func topMostController() -> UIViewController? { nil }
@available(*, unavailable, renamed: "iq.superviewOf(type:belowView:)")
func superviewOfClassType(_ classType: UIView.Type, belowView: UIView? = nil) -> UIView? { nil }
}

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

75
Pods/IQKeyboardCore/README.md generated Normal file
View File

@ -0,0 +1,75 @@
# IQKeyboardCore
[![CI Status](https://img.shields.io/travis/hackiftekhar/IQKeyboardCore.svg?style=flat)](https://travis-ci.org/hackiftekhar/IQKeyboardCore)
[![Version](https://img.shields.io/cocoapods/v/IQKeyboardCore.svg?style=flat)](https://cocoapods.org/pods/IQKeyboardCore)
[![License](https://img.shields.io/cocoapods/l/IQKeyboardCore.svg?style=flat)](https://cocoapods.org/pods/IQKeyboardCore)
[![Platform](https://img.shields.io/cocoapods/p/IQKeyboardCore.svg?style=flat)](https://cocoapods.org/pods/IQKeyboardCore)
## Example
To run the example project, clone the repo, and run `pod install` from the Example directory first.
## Requirements
## Installation
IQKeyboardCore is available through [CocoaPods](https://cocoapods.org). To install
it, simply add the following line to your Podfile:
```ruby
pod 'IQKeyboardCore'
```
## Usage
IQKeyboardCore is not intended to use independently. It's just a helper and extension for most of the IQKeyboard related libraries
This contains IQTextInputView protocol
```swift
@objc public protocol IQTextInputView where Self: UIView, Self: UITextInputTraits {
}
```
UITextField, UITextView and UISearchBar are the known classes who adopted this protocol within the library
```swift
@objc extension UITextField: IQTextInputView {...}
@objc extension UITextView: IQTextInputView {...}
@objc extension UISearchBar: IQTextInputView {...}
```
This library also contains IQEnableMode which is used by other libraries
```swift
@objc public enum IQEnableMode: Int {
case `default`
case enabled
case disabled
}
```
There are other extension functions which are available on UIView
```swift
public extension IQKeyboardExtension where Base: UIView {
func viewContainingController() -> UIViewController?
func superviewOf<T: UIView>(type classType: T.Type, belowView: UIView? = nil) -> T?
func textFieldSearchBar() -> UISearchBar?
func isAlertViewTextField() -> Bool
}
```
Above extension functions can be used like below
```swift
view.iq.viewContainingController()
view.iq.superviewOf(type: UIScrollView.self)
view.iq.textFieldSearchBar()
view.iq.isAlertViewTextField()
```
## Author
hackiftekhar, ideviftekhar@gmail.com
## License
IQKeyboardCore is available under the MIT license. See the LICENSE file for more info.

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

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.

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.

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,150 @@
//
// IQBarButtonItem.swift
// https://github.com/hackiftekhar/IQKeyboardToolbar
// 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 open class IQBarButtonItem: UIBarButtonItem {
internal static let flexibleBarButtonItem: IQBarButtonItem = {
let barButton = IQBarButtonItem(barButtonSystemItem: .flexibleSpace,
target: nil, action: nil)
#if compiler(>=6.2) // Xcode 26
let requiresCompatibility: Bool = Bundle.main.object(forInfoDictionaryKey: "UIDesignRequiresCompatibility") as? Bool ?? false
if #available(iOS 26.0, *), !requiresCompatibility {
barButton.hidesSharedBackground = false
}
#endif
return barButton
}()
/**
Fixed space bar button of toolbar.
*/
internal static let fixedSpaceBarButton: IQBarButtonItem = {
let barButton = IQBarButtonItem(barButtonSystemItem: .fixedSpace,
target: nil, action: nil)
barButton.isSystemItem = true
#if compiler(>=6.2) // Xcode 26
let requiresCompatibility: Bool = Bundle.main.object(forInfoDictionaryKey: "UIDesignRequiresCompatibility") as? Bool ?? false
if #available(iOS 26.0, *), !requiresCompatibility {
barButton.hidesSharedBackground = false
} else {
barButton.width = 6
}
#else
barButton.width = 6
#endif
return barButton
}()
public override init() {
super.init()
initialize()
}
public required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initialize()
}
private func initialize() {
let states: [UIControl.State] = [.normal, .highlighted, .disabled, .focused]
for state in states {
setBackgroundImage(UIImage(), for: state, barMetrics: .default)
setBackgroundImage(UIImage(), for: state, style: .plain, barMetrics: .default)
setBackButtonBackgroundImage(UIImage(), for: state, barMetrics: .default)
}
setTitlePositionAdjustment(UIOffset(), for: .default)
setBackgroundVerticalPositionAdjustment(0, for: .default)
setBackButtonBackgroundVerticalPositionAdjustment(0, for: .default)
}
override open var tintColor: UIColor? {
didSet {
var textAttributes: [NSAttributedString.Key: Any] = [:]
textAttributes[.foregroundColor] = tintColor
if let attributes: [NSAttributedString.Key: Any] = titleTextAttributes(for: .normal) {
for (key, value) in attributes {
textAttributes[key] = value
}
}
setTitleTextAttributes(textAttributes, for: .normal)
}
}
/**
Boolean to know if it's a system item or custom item,
we are having a limitation that we cannot override a designated initializer,
so we are manually setting this property once in initialization
*/
internal var isSystemItem: Bool = false
/**
Additional target & action to do get callback action.
Note that setting custom target & selector doesn't affect native functionality,
this is just an additional target to get a callback.
@param target Target object.
@param action Target Selector.
*/
open func setTarget(_ target: AnyObject?, action: Selector?) {
if let target: AnyObject = target, let action: Selector = action {
invocation = IQInvocation(target: target, action: action)
} else {
invocation = nil
}
}
/**
Customized Invocation to be called when button is pressed.
invocation is internally created using setTarget:action: method.
*/
open var invocation: IQInvocation? {
didSet {
// We have to put this condition here because if we override this function then
// We were getting "Cannot override '_' which has been marked unavailable" in Xcode 15
if let titleBarButton = self as? IQTitleBarButtonItem {
if let target = invocation?.target, let action = invocation?.action {
titleBarButton.isEnabled = true
titleBarButton.titleButton.isEnabled = true
titleBarButton.titleButton.addTarget(target, action: action, for: .touchUpInside)
} else {
titleBarButton.isEnabled = false
titleBarButton.titleButton.isEnabled = false
titleBarButton.titleButton.removeTarget(nil, action: nil, for: .touchUpInside)
}
}
}
}
}

View File

@ -0,0 +1,102 @@
//
// IQBarButtonItemConfiguration.swift
// https://github.com/hackiftekhar/IQKeyboardToolbar
// 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
/**
IQBarButtonItemConfiguration for creating toolbar with bar button items
*/
@available(iOSApplicationExtension, unavailable)
@MainActor
@objcMembers public final class IQBarButtonItemConfiguration: 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? // System Item to be used to instantiate bar button.
public let image: UIImage? // Image to show on bar button item if it's not a system item.
public let title: String? // Title to show on bar button item if it's not a system item.
public var action: Selector? // action for bar button item. Usually 'doneAction:(IQBarButtonItem*)item'.
public override var accessibilityLabel: String? { didSet { } } // Accessibility related labels
public func apply(on oldBarButtonItem: IQBarButtonItem, target: AnyObject?) -> IQBarButtonItem {
var newBarButtonItem: IQBarButtonItem = oldBarButtonItem
if systemItem == nil, !oldBarButtonItem.isSystemItem {
newBarButtonItem.title = title
newBarButtonItem.image = image
newBarButtonItem.target = target
newBarButtonItem.action = action
} else {
if let systemItem: UIBarButtonItem.SystemItem = systemItem {
newBarButtonItem = IQBarButtonItem(barButtonSystemItem: systemItem, target: target, action: action)
newBarButtonItem.isSystemItem = true
#if compiler(>=6.2) // Xcode 26
let requiresCompatibility: Bool = Bundle.main.object(forInfoDictionaryKey: "UIDesignRequiresCompatibility") as? Bool ?? false
if #available(iOS 26.0, *), !requiresCompatibility {
newBarButtonItem.style = .plain
}
#endif
} else if let image: UIImage = image {
newBarButtonItem = IQBarButtonItem(image: image, style: .plain, target: target, action: action)
} else {
newBarButtonItem = IQBarButtonItem(title: title, style: .plain, target: target, action: action)
}
newBarButtonItem.invocation = oldBarButtonItem.invocation
newBarButtonItem.isEnabled = oldBarButtonItem.isEnabled
newBarButtonItem.tag = oldBarButtonItem.tag
}
newBarButtonItem.accessibilityLabel = accessibilityLabel
newBarButtonItem.accessibilityIdentifier = oldBarButtonItem.accessibilityLabel
return newBarButtonItem
}
}

View File

@ -0,0 +1,42 @@
//
// IQInvocation.swift
// https://github.com/hackiftekhar/IQKeyboardToolbar
// 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 IQInvocation: NSObject {
public weak var target: AnyObject?
public let action: Selector
public init(target: AnyObject, action: Selector) {
self.target = target
self.action = action
}
public func invoke(from: Any) {
guard let target: AnyObject = target else { return }
UIApplication.shared.sendAction(action, to: target, from: from, for: UIEvent())
}
}

View File

@ -0,0 +1,166 @@
//
// IQTitleBarButtonItem.swift
// https://github.com/hackiftekhar/IQKeyboardToolbar
// 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 open class IQTitleBarButtonItem: IQBarButtonItem {
open var titleFont: UIFont? {
didSet {
if let titleFont: UIFont = titleFont {
titleButton.titleLabel?.font = titleFont
} else {
titleButton.titleLabel?.font = UIFont.systemFont(ofSize: 13)
}
}
}
override open var title: String? {
didSet {
titleButton.setTitle(title, for: .normal)
updateAccessibility()
}
}
/**
titleColor to be used for displaying button text when displaying title (disabled state).
*/
open var titleColor: UIColor? {
didSet {
if let titleColor: UIColor = titleColor {
titleButton.setTitleColor(titleColor, for: .disabled)
} else {
titleButton.setTitleColor(UIColor.lightGray, for: .disabled)
}
}
}
/**
selectableTitleColor to be used for displaying button text when button is enabled.
*/
open var selectableTitleColor: UIColor? {
didSet {
if let selectableTitleColor: UIColor = selectableTitleColor {
titleButton.setTitleColor(selectableTitleColor, for: .normal)
} else {
titleButton.setTitleColor(UIColor.systemBlue, for: .normal)
}
}
}
internal let titleButton: UIButton = UIButton(type: .system)
private let _titleView: UIView = UIView()
private override init() {
super.init()
}
public convenience init(title: String?) {
self.init(title: nil, style: .plain, target: nil, action: nil)
_titleView.backgroundColor = UIColor.clear
titleButton.isAccessibilityElement = false
titleButton.isEnabled = false
titleButton.titleLabel?.numberOfLines = 3
titleButton.setTitleColor(UIColor.lightGray, for: .disabled)
titleButton.setTitleColor(UIColor.systemBlue, for: .normal)
titleButton.backgroundColor = UIColor.clear
titleButton.titleLabel?.textAlignment = .center
titleButton.setTitle(title, for: .normal)
titleFont = UIFont.systemFont(ofSize: 13.0)
titleButton.titleLabel?.font = self.titleFont
_titleView.addSubview(titleButton)
let lowPriority: UILayoutPriority = UILayoutPriority(rawValue: UILayoutPriority.defaultLow.rawValue-1)
let highPriority: UILayoutPriority = UILayoutPriority(rawValue: UILayoutPriority.defaultHigh.rawValue-1)
_titleView.translatesAutoresizingMaskIntoConstraints = false
_titleView.setContentHuggingPriority(lowPriority, for: .vertical)
_titleView.setContentHuggingPriority(lowPriority, for: .horizontal)
_titleView.setContentCompressionResistancePriority(highPriority, for: .vertical)
_titleView.setContentCompressionResistancePriority(highPriority, for: .horizontal)
titleButton.translatesAutoresizingMaskIntoConstraints = false
titleButton.setContentHuggingPriority(lowPriority, for: .vertical)
titleButton.setContentHuggingPriority(lowPriority, for: .horizontal)
titleButton.setContentCompressionResistancePriority(highPriority, for: .vertical)
titleButton.setContentCompressionResistancePriority(highPriority, for: .horizontal)
let top: NSLayoutConstraint = NSLayoutConstraint(item: titleButton, attribute: .top,
relatedBy: .equal,
toItem: _titleView, attribute: .top,
multiplier: 1, constant: 0)
let bottom: NSLayoutConstraint = NSLayoutConstraint(item: titleButton, attribute: .bottom,
relatedBy: .equal,
toItem: _titleView, attribute: .bottom,
multiplier: 1, constant: 0)
let leading: NSLayoutConstraint = NSLayoutConstraint(item: titleButton, attribute: .leading,
relatedBy: .equal,
toItem: _titleView, attribute: .leading,
multiplier: 1, constant: 0)
let trailing: NSLayoutConstraint = NSLayoutConstraint(item: titleButton, attribute: .trailing,
relatedBy: .equal,
toItem: _titleView, attribute: .trailing,
multiplier: 1, constant: 0)
_titleView.addConstraints([top, bottom, leading, trailing])
customView = _titleView
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
private func updateAccessibility() {
if title == nil || title?.isEmpty == true {
isAccessibilityElement = false
// Swift 6: Reference to static property 'none'
// is not concurrency-safe because it involves shared mutable state
// accessibilityTraits = .none
accessibilityTraits = .init(rawValue: 0)
} else if titleButton.isEnabled == true {
isAccessibilityElement = true
// Swift 6: Reference to static property 'button'
// is not concurrency-safe because it involves shared mutable state
// accessibilityTraits = .button
accessibilityTraits = .init(rawValue: 1)
} else {
isAccessibilityElement = true
// Swift 6: Reference to static property 'staticText'
// is not concurrency-safe because it involves shared mutable state
// accessibilityTraits = .staticText
accessibilityTraits = .init(rawValue: 64)
}
}
}

View File

@ -0,0 +1,389 @@
//
// UIView+IQKeyboardExtension.swift
// https://github.com/hackiftekhar/IQKeyboardToolbar
// 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 toolbar: Int = 0
static var hidePlaceholder: Int = 0
static var placeholder: Int = 0
}
@available(iOSApplicationExtension, unavailable)
@MainActor
public extension IQKeyboardExtension where Base: IQTextInputView {
// MARK: Toolbar
/**
Toolbar references for better customization control.
*/
var toolbar: IQKeyboardToolbar {
var toolbar: IQKeyboardToolbar? = base?.inputAccessoryView as? IQKeyboardToolbar
if toolbar == nil, let base = base {
toolbar = objc_getAssociatedObject(base, &AssociatedKeys.toolbar) as? IQKeyboardToolbar
}
if let toolbar: IQKeyboardToolbar = toolbar {
return toolbar
} else {
let width: CGFloat = base?.window?.windowScene?.screen.bounds.width ?? 0
let height: CGFloat
#if compiler(>=6.2) // Xcode 26
let requiresCompatibility: Bool = Bundle.main.object(forInfoDictionaryKey: "UIDesignRequiresCompatibility") as? Bool ?? false
if #available(iOS 26.0, *), !requiresCompatibility {
height = 58
} else {
height = 44
}
#else
height = 44
#endif
let frame = CGRect(origin: .zero, size: .init(width: width, height: height))
let newToolbar = IQKeyboardToolbar(frame: frame)
if let base = base {
objc_setAssociatedObject(base, &AssociatedKeys.toolbar, newToolbar, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
return newToolbar
}
}
// MARK: Toolbar title
/**
If `hideToolbarPlaceholder` is YES, then title will not be added to the toolbar. Default to NO.
*/
var hidePlaceholder: Bool {
get {
if let base = base {
return objc_getAssociatedObject(base, &AssociatedKeys.hidePlaceholder) as? Bool ?? false
}
return false
}
set(newValue) {
if let base = base {
objc_setAssociatedObject(base, &AssociatedKeys.hidePlaceholder,
newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
toolbar.titleBarButton.title = drawingPlaceholder
}
}
}
/**
`toolbarPlaceholder` to override default `placeholder` text when drawing text on toolbar.
*/
var placeholder: String? {
get {
if let base = base {
return objc_getAssociatedObject(base, &AssociatedKeys.placeholder) as? String
}
return nil
}
set(newValue) {
if let base = base {
// swiftlint:disable line_length
objc_setAssociatedObject(base, &AssociatedKeys.placeholder, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
toolbar.titleBarButton.title = drawingPlaceholder
// swiftlint:enable line_length
}
}
}
/**
`drawingToolbarPlaceholder` will be actual text used to draw on toolbar.
This would either `placeholder` or `toolbarPlaceholder`.
*/
var drawingPlaceholder: String? {
guard !hidePlaceholder else { return nil }
if let placeholder = placeholder,
!placeholder.isEmpty {
return placeholder
}
guard let placeholderable: any IQPlaceholderable = base as? (any IQPlaceholderable) else { return nil }
if let placeholder = placeholderable.attributedPlaceholder?.string,
!placeholder.isEmpty {
return placeholder
} else if let placeholder = placeholderable.placeholder {
return placeholder
} else {
return nil
}
}
// MARK: Common
func addToolbar(target: AnyObject?,
previousConfiguration: IQBarButtonItemConfiguration? = nil,
nextConfiguration: IQBarButtonItemConfiguration? = nil,
rightConfiguration: IQBarButtonItemConfiguration? = nil,
title: String?,
titleAccessibilityLabel: String? = nil) {
guard let base = base else { return }
// Creating a toolBar for phoneNumber keyboard
let toolbar: IQKeyboardToolbar = toolbar
let items: [UIBarButtonItem] = Self.constructBarButtonItems(target: target, toolbar: toolbar,
previousConfiguration: previousConfiguration,
nextConfiguration: nextConfiguration,
rightConfiguration: rightConfiguration,
title: title,
titleAccessibilityLabel: titleAccessibilityLabel)
// Adding button to toolBar.
toolbar.items = items
switch base.keyboardAppearance {
case .dark:
toolbar.barStyle = .black
default:
toolbar.barStyle = .default
}
// Setting toolbar to keyboard.
let reloadInputViews: Bool = base.inputAccessoryView != toolbar
guard reloadInputViews else { return }
base.inputAccessoryView = toolbar
base.reloadInputViews()
}
// MARK: Right
func addDone(target: AnyObject?,
action: Selector,
showPlaceholder: Bool = false, titleAccessibilityLabel: String? = nil) {
let title: String? = showPlaceholder ? drawingPlaceholder : nil
addDone(target: target, action: action,
title: title, titleAccessibilityLabel: titleAccessibilityLabel)
}
func addDone(target: AnyObject?,
action: Selector,
title: String?, titleAccessibilityLabel: String? = nil) {
let rightConfiguration: IQBarButtonItemConfiguration
#if compiler(>=6.2) // Xcode 26
let requiresCompatibility: Bool = Bundle.main.object(forInfoDictionaryKey: "UIDesignRequiresCompatibility") as? Bool ?? false
if #available(iOS 26.0, *), !requiresCompatibility {
rightConfiguration = IQBarButtonItemConfiguration(image: UIImage(systemName: "checkmark")!, action: action)
} else {
rightConfiguration = IQBarButtonItemConfiguration(systemItem: .done, action: action)
}
#else
rightConfiguration = IQBarButtonItemConfiguration(systemItem: .done, action: action)
#endif
addToolbar(target: target, rightConfiguration: rightConfiguration,
title: title, titleAccessibilityLabel: titleAccessibilityLabel)
}
func addRightButton(target: AnyObject?,
configuration: IQBarButtonItemConfiguration,
showPlaceholder: Bool = false, titleAccessibilityLabel: String? = nil) {
let title: String? = showPlaceholder ? drawingPlaceholder : nil
addRightButton(target: target, configuration: configuration, title: title,
titleAccessibilityLabel: titleAccessibilityLabel)
}
func addRightButton(target: AnyObject?,
configuration: IQBarButtonItemConfiguration,
title: String?, titleAccessibilityLabel: String? = nil) {
addToolbar(target: target, rightConfiguration: configuration, title: title,
titleAccessibilityLabel: titleAccessibilityLabel)
}
// MARK: Right/Left
func addRightLeft(target: AnyObject?,
rightConfiguration: IQBarButtonItemConfiguration, leftConfiguration: IQBarButtonItemConfiguration,
showPlaceholder: Bool = false, titleAccessibilityLabel: String? = nil) {
let title: String? = showPlaceholder ? drawingPlaceholder : nil
addRightLeft(target: target,
rightConfiguration: rightConfiguration, leftConfiguration: leftConfiguration,
title: title, titleAccessibilityLabel: titleAccessibilityLabel)
}
func addRightLeft(target: AnyObject?,
rightConfiguration: IQBarButtonItemConfiguration, leftConfiguration: IQBarButtonItemConfiguration,
title: String?, titleAccessibilityLabel: String? = nil) {
addToolbar(target: target,
previousConfiguration: leftConfiguration, rightConfiguration: rightConfiguration,
title: title, titleAccessibilityLabel: titleAccessibilityLabel)
}
// MARK: Previous/Next/Right
func addPreviousNextRight(target: AnyObject?,
previousConfiguration: IQBarButtonItemConfiguration? = nil,
nextConfiguration: IQBarButtonItemConfiguration? = nil,
rightConfiguration: IQBarButtonItemConfiguration?,
showPlaceholder: Bool = false, titleAccessibilityLabel: String? = nil) {
let title: String? = showPlaceholder ? drawingPlaceholder : nil
addPreviousNextRight(target: target,
previousConfiguration: previousConfiguration, nextConfiguration: nextConfiguration,
rightConfiguration: rightConfiguration,
title: title, titleAccessibilityLabel: titleAccessibilityLabel)
}
func addPreviousNextRight(target: AnyObject?,
previousConfiguration: IQBarButtonItemConfiguration? = nil,
nextConfiguration: IQBarButtonItemConfiguration? = nil,
rightConfiguration: IQBarButtonItemConfiguration?,
title: String?, titleAccessibilityLabel: String? = nil) {
addToolbar(target: target,
previousConfiguration: previousConfiguration, nextConfiguration: nextConfiguration,
rightConfiguration: rightConfiguration,
title: title, titleAccessibilityLabel: titleAccessibilityLabel)
}
func addPreviousNextDone(target: AnyObject?, previousAction: Selector, nextAction: Selector, doneAction: Selector,
showPlaceholder: Bool = false, titleAccessibilityLabel: String? = nil) {
let title: String? = showPlaceholder ? drawingPlaceholder : nil
addPreviousNextDone(target: target, previousAction: previousAction, nextAction: nextAction,
doneAction: doneAction,
title: title, titleAccessibilityLabel: titleAccessibilityLabel)
}
func addPreviousNextDone(target: AnyObject?,
previousAction: Selector, nextAction: Selector, doneAction: Selector,
title: String?, titleAccessibilityLabel: String? = nil) {
let chevronUp: UIImage = UIImage(systemName: "chevron.up") ?? UIImage()
let chevronDown: UIImage = UIImage(systemName: "chevron.down") ?? UIImage()
let previousConfiguration = IQBarButtonItemConfiguration(image: chevronUp,
action: previousAction)
let nextConfiguration = IQBarButtonItemConfiguration(image: chevronDown,
action: nextAction)
let rightConfiguration: IQBarButtonItemConfiguration
#if compiler(>=6.2) // Xcode 26
let requiresCompatibility: Bool = Bundle.main.object(forInfoDictionaryKey: "UIDesignRequiresCompatibility") as? Bool ?? false
if #available(iOS 26.0, *), !requiresCompatibility {
rightConfiguration = IQBarButtonItemConfiguration(image: UIImage(systemName: "checkmark")!, action: doneAction)
} else {
rightConfiguration = IQBarButtonItemConfiguration(systemItem: .done, action: doneAction)
}
#else
rightConfiguration = IQBarButtonItemConfiguration(systemItem: .done, action: doneAction)
#endif
addToolbar(target: target, previousConfiguration: previousConfiguration,
nextConfiguration: nextConfiguration, rightConfiguration: rightConfiguration,
title: title, titleAccessibilityLabel: titleAccessibilityLabel)
}
}
@available(iOSApplicationExtension, unavailable)
@MainActor
private extension IQKeyboardExtension where Base: IQTextInputView {
private static func constructBarButtonItems(target: AnyObject?,
toolbar: IQKeyboardToolbar,
previousConfiguration: IQBarButtonItemConfiguration? = nil,
nextConfiguration: IQBarButtonItemConfiguration? = nil,
rightConfiguration: IQBarButtonItemConfiguration? = nil,
title: String?,
titleAccessibilityLabel: String? = nil) -> [UIBarButtonItem] {
var items: [UIBarButtonItem] = []
// Leading group
do {
if let previousConfiguration: IQBarButtonItemConfiguration = previousConfiguration {
let prev: IQBarButtonItem = previousConfiguration.apply(on: toolbar.previousBarButton, target: target)
toolbar.previousBarButton = prev
items.append(prev)
}
if previousConfiguration != nil, nextConfiguration != nil {
items.append(IQBarButtonItem.fixedSpaceBarButton)
}
if let nextConfiguration: IQBarButtonItemConfiguration = nextConfiguration {
let next: IQBarButtonItem = nextConfiguration.apply(on: toolbar.nextBarButton, target: target)
toolbar.nextBarButton = next
items.append(next)
}
if !toolbar.additionalLeadingItems.isEmpty {
items.append(contentsOf: toolbar.additionalLeadingItems)
}
}
// Center Title bar button item
if let title = title, !title.isEmpty {
// Title button
toolbar.titleBarButton.title = title
toolbar.titleBarButton.accessibilityLabel = titleAccessibilityLabel
toolbar.titleBarButton.accessibilityIdentifier = titleAccessibilityLabel
toolbar.titleBarButton.customView?.frame = .zero
#if compiler(>=6.2) // Xcode 26
let requiresCompatibility: Bool = Bundle.main.object(forInfoDictionaryKey: "UIDesignRequiresCompatibility") as? Bool ?? false
if #available(iOS 26.0, *), !requiresCompatibility {
if !items.isEmpty {
items.append(IQBarButtonItem.flexibleBarButtonItem)
}
} else {
items.append(IQBarButtonItem.flexibleBarButtonItem)
}
#else
items.append(IQBarButtonItem.flexibleBarButtonItem)
#endif
items.append(toolbar.titleBarButton)
}
// Flexible space
items.append(IQBarButtonItem.flexibleBarButtonItem)
// Trailing group
do {
if !toolbar.additionalTrailingItems.isEmpty {
items.append(contentsOf: toolbar.additionalTrailingItems)
}
if let rightConfiguration: IQBarButtonItemConfiguration = rightConfiguration {
let done: IQBarButtonItem = rightConfiguration.apply(on: toolbar.doneBarButton, target: target)
toolbar.doneBarButton = done
items.append(done)
}
}
return items
}
}

View File

@ -0,0 +1,228 @@
//
// UIView+IQKeyboardExtensionDeprecated.swift
// https://github.com/hackiftekhar/IQKeyboardToolbar
// 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 function_parameter_count
@available(iOSApplicationExtension, unavailable)
@MainActor
@objc public extension UIView {
@available(*, unavailable, renamed: "iq.toolbar")
var keyboardToolbar: IQKeyboardToolbar {
get { fatalError() }
set {}
}
@available(*, unavailable, renamed: "iq.hidePlaceholder")
var shouldHideToolbarPlaceholder: Bool {
get { false }
set {}
}
@available(*, unavailable, renamed: "iq.placeholder")
var toolbarPlaceholder: String? {
get { nil }
set {}
}
@available(*, unavailable, renamed: "iq.drawingPlaceholder")
var drawingToolbarPlaceholder: String? {
get { nil }
set {}
}
@available(*, unavailable, renamed: "iq.addToolbar(target:previousConfiguration:nextConfiguration:rightConfiguration:title:titleAccessibilityLabel:)")
func addKeyboardToolbarWithTarget(target: AnyObject?,
titleText: String?,
titleAccessibilityLabel: String? = nil,
rightBarButtonConfiguration: IQBarButtonItemConfiguration?,
previousBarButtonConfiguration: IQBarButtonItemConfiguration? = nil,
nextBarButtonConfiguration: IQBarButtonItemConfiguration? = nil) {
}
@available(*, unavailable, renamed: "iq.addDone(target:action:showPlaceholder:titleAccessibilityLabel:)")
func addDoneOnKeyboardWithTarget(_ target: AnyObject?,
action: Selector,
shouldShowPlaceholder: Bool = false,
titleAccessibilityLabel: String? = nil) {
}
@available(*, unavailable, renamed: "iq.addDone(target:action:title:titleAccessibilityLabel:)")
func addDoneOnKeyboardWithTarget(_ target: AnyObject?,
action: Selector,
titleText: String?,
titleAccessibilityLabel: String? = nil) {
}
@available(*, unavailable, renamed: "iq.addRightButton(target:configuration:showPlaceholder:titleAccessibilityLabel:)")
func addRightButtonOnKeyboardWithImage(_ image: UIImage,
target: AnyObject?,
action: Selector,
shouldShowPlaceholder: Bool = false,
titleAccessibilityLabel: String? = nil) {
}
@available(*, unavailable, renamed: "iq.addRightButton(target:configuration:title:titleAccessibilityLabel:)")
func addRightButtonOnKeyboardWithImage(_ image: UIImage,
target: AnyObject?,
action: Selector,
titleText: String?,
titleAccessibilityLabel: String? = nil) {
}
@available(*, unavailable, renamed: "iq.addRightButton(target:configuration:showPlaceholder:titleAccessibilityLabel:)")
func addRightButtonOnKeyboardWithText(_ text: String,
target: AnyObject?,
action: Selector,
shouldShowPlaceholder: Bool = false,
titleAccessibilityLabel: String? = nil) {
}
@available(*, unavailable, renamed: "iq.addRightButton(target:configuration:title:titleAccessibilityLabel:)")
func addRightButtonOnKeyboardWithText(_ text: String,
target: AnyObject?,
action: Selector,
titleText: String?,
titleAccessibilityLabel: String? = nil) {
}
@available(*, unavailable, renamed: "iq.addRightLeft(target:rightConfiguration:leftConfiguration:showPlaceholder:titleAccessibilityLabel:)")
func addCancelDoneOnKeyboardWithTarget(_ target: AnyObject?,
cancelAction: Selector,
doneAction: Selector,
shouldShowPlaceholder: Bool = false,
titleAccessibilityLabel: String? = nil) {
}
@available(*, unavailable, renamed: "iq.addRightLeft(target:rightConfiguration:leftConfiguration:showPlaceholder:titleAccessibilityLabel:)")
func addRightLeftOnKeyboardWithTarget(_ target: AnyObject?,
leftButtonTitle: String,
rightButtonTitle: String,
leftButtonAction: Selector,
rightButtonAction: Selector,
shouldShowPlaceholder: Bool = false,
titleAccessibilityLabel: String? = nil) {
}
@available(*, unavailable, renamed: "iq.addRightLeft(target:rightConfiguration:leftConfiguration:showPlaceholder:titleAccessibilityLabel:)")
func addRightLeftOnKeyboardWithTarget(_ target: AnyObject?,
leftButtonImage: UIImage,
rightButtonImage: UIImage,
leftButtonAction: Selector,
rightButtonAction: Selector,
shouldShowPlaceholder: Bool = false,
titleAccessibilityLabel: String? = nil) {
}
@available(*, unavailable, renamed: "iq.addRightLeft(target:rightConfiguration:leftConfiguration:title:titleAccessibilityLabel:)")
func addCancelDoneOnKeyboardWithTarget(_ target: AnyObject?,
cancelAction: Selector,
doneAction: Selector,
titleText: String?,
titleAccessibilityLabel: String? = nil) {
}
@available(*, unavailable, renamed: "iq.addRightLeft(target:rightConfiguration:leftConfiguration:title:titleAccessibilityLabel:)")
func addRightLeftOnKeyboardWithTarget(_ target: AnyObject?,
leftButtonTitle: String,
rightButtonTitle: String,
leftButtonAction: Selector,
rightButtonAction: Selector,
titleText: String?,
titleAccessibilityLabel: String? = nil) {
}
@available(*, unavailable, renamed: "iq.addRightLeft(target:rightConfiguration:leftConfiguration:title:titleAccessibilityLabel:)")
func addRightLeftOnKeyboardWithTarget(_ target: AnyObject?,
leftButtonImage: UIImage,
rightButtonImage: UIImage,
leftButtonAction: Selector,
rightButtonAction: Selector,
titleText: String?,
titleAccessibilityLabel: String? = nil) {
}
@available(*, unavailable, renamed: "iq.addPreviousNextDone(target:previousAction:nextAction:doneAction:showPlaceholder:titleAccessibilityLabel:)")
func addPreviousNextDone(_ target: AnyObject?,
previousAction: Selector,
nextAction: Selector,
doneAction: Selector,
shouldShowPlaceholder: Bool = false,
titleAccessibilityLabel: String? = nil) {
}
@available(*, unavailable, renamed: "iq.addPreviousNextDone(target:previousAction:nextAction:doneAction:title:titleAccessibilityLabel:)")
func addPreviousNextDoneOnKeyboardWithTarget(_ target: AnyObject?,
previousAction: Selector,
nextAction: Selector,
doneAction: Selector,
titleText: String?,
titleAccessibilityLabel: String? = nil) {
}
@available(*, unavailable, renamed: "iq.addPreviousNextRight(target:previousConfiguration:nextConfiguration:rightConfiguration:showPlaceholder:titleAccessibilityLabel:)")
func addPreviousNextRightOnKeyboardWithTarget(_ target: AnyObject?,
rightButtonImage: UIImage,
previousAction: Selector,
nextAction: Selector,
rightButtonAction: Selector,
shouldShowPlaceholder: Bool = false,
titleAccessibilityLabel: String? = nil) {
}
@available(*, unavailable, renamed: "iq.addPreviousNextRight(target:previousConfiguration:nextConfiguration:rightConfiguration:showPlaceholder:titleAccessibilityLabel:)")
func addPreviousNextRightOnKeyboardWithTarget(_ target: AnyObject?,
rightButtonTitle: String,
previousAction: Selector,
nextAction: Selector,
rightButtonAction: Selector,
shouldShowPlaceholder: Bool = false,
titleAccessibilityLabel: String? = nil) {
}
@available(*, unavailable, renamed: "iq.addPreviousNextRight(target:previousConfiguration:nextConfiguration:rightConfiguration:title:titleAccessibilityLabel:)")
func addPreviousNextRightOnKeyboardWithTarget(_ target: AnyObject?,
rightButtonImage: UIImage,
previousAction: Selector,
nextAction: Selector,
rightButtonAction: Selector,
titleText: String?,
titleAccessibilityLabel: String? = nil) {
}
@available(*, unavailable, renamed: "iq.addPreviousNextRight(target:previousConfiguration:nextConfiguration:rightConfiguration:title:titleAccessibilityLabel:)")
func addPreviousNextRightOnKeyboardWithTarget(_ target: AnyObject?,
rightButtonTitle: String,
previousAction: Selector,
nextAction: Selector,
rightButtonAction: Selector,
titleText: String?,
titleAccessibilityLabel: String? = nil) {
}
}
// swiftlint:enable unused_setter_value
// swiftlint:enable line_length
// swiftlint:enable function_parameter_count

View File

@ -0,0 +1,254 @@
//
// UIView+IQKeyboardExtensionObjc.swift
// https://github.com/hackiftekhar/IQKeyboardToolbar
// 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 UITextField {
var iq_toolbar: IQKeyboardToolbar { iq.toolbar }
var iq_hidePlaceholder: Bool {
get { iq.hidePlaceholder }
set(newValue) { iq.hidePlaceholder = newValue }
}
var iq_placeholder: String? {
get { iq.placeholder }
set(newValue) { iq.placeholder = newValue }
}
var iq_drawingPlaceholder: String? { iq.drawingPlaceholder }
func iq_addToolbar(target: AnyObject?,
previousConfiguration: IQBarButtonItemConfiguration? = nil,
nextConfiguration: IQBarButtonItemConfiguration? = nil,
rightConfiguration: IQBarButtonItemConfiguration? = nil,
title: String?,
titleAccessibilityLabel: String? = nil) {
iq.addToolbar(target: target, previousConfiguration: previousConfiguration,
nextConfiguration: nextConfiguration,
rightConfiguration: rightConfiguration,
title: title, titleAccessibilityLabel: titleAccessibilityLabel)
}
func iq_addDone(target: AnyObject?,
action: Selector,
showPlaceholder: Bool = false, titleAccessibilityLabel: String? = nil) {
iq.addDone(target: target, action: action, showPlaceholder: showPlaceholder,
titleAccessibilityLabel: titleAccessibilityLabel)
}
func iq_addDone(target: AnyObject?,
action: Selector,
title: String?, titleAccessibilityLabel: String? = nil) {
iq.addDone(target: target, action: action, title: title, titleAccessibilityLabel: titleAccessibilityLabel)
}
func iq_addRightButton(target: AnyObject?,
configuration: IQBarButtonItemConfiguration,
showPlaceholder: Bool = false, titleAccessibilityLabel: String? = nil) {
iq.addRightButton(target: target, configuration: configuration, showPlaceholder: showPlaceholder,
titleAccessibilityLabel: titleAccessibilityLabel)
}
func iq_addRightButton(target: AnyObject?,
configuration: IQBarButtonItemConfiguration,
title: String?, titleAccessibilityLabel: String? = nil) {
iq.addRightButton(target: target, configuration: configuration, title: title,
titleAccessibilityLabel: titleAccessibilityLabel)
}
func iq_addRightLeft(target: AnyObject?,
rightConfiguration: IQBarButtonItemConfiguration,
leftConfiguration: IQBarButtonItemConfiguration,
showPlaceholder: Bool = false, titleAccessibilityLabel: String? = nil) {
iq.addRightLeft(target: target, rightConfiguration: rightConfiguration,
leftConfiguration: leftConfiguration, showPlaceholder: showPlaceholder,
titleAccessibilityLabel: titleAccessibilityLabel)
}
func iq_addRightLeft(target: AnyObject?,
rightConfiguration: IQBarButtonItemConfiguration,
leftConfiguration: IQBarButtonItemConfiguration,
title: String?, titleAccessibilityLabel: String? = nil) {
iq.addRightLeft(target: target, rightConfiguration: rightConfiguration,
leftConfiguration: leftConfiguration, title: title,
titleAccessibilityLabel: titleAccessibilityLabel)
}
func iq_addPreviousNextRight(target: AnyObject?,
previousConfiguration: IQBarButtonItemConfiguration? = nil,
nextConfiguration: IQBarButtonItemConfiguration? = nil,
rightConfiguration: IQBarButtonItemConfiguration?,
showPlaceholder: Bool = false, titleAccessibilityLabel: String? = nil) {
iq.addPreviousNextRight(target: target, previousConfiguration: previousConfiguration,
nextConfiguration: nextConfiguration, rightConfiguration: rightConfiguration,
showPlaceholder: showPlaceholder, titleAccessibilityLabel: titleAccessibilityLabel)
}
func iq_addPreviousNextRight(target: AnyObject?,
previousConfiguration: IQBarButtonItemConfiguration? = nil,
nextConfiguration: IQBarButtonItemConfiguration? = nil,
rightConfiguration: IQBarButtonItemConfiguration?,
title: String?, titleAccessibilityLabel: String? = nil) {
iq.addPreviousNextRight(target: target, previousConfiguration: previousConfiguration,
nextConfiguration: nextConfiguration,
rightConfiguration: rightConfiguration,
title: title, titleAccessibilityLabel: titleAccessibilityLabel)
}
func iq_addPreviousNextDone(target: AnyObject?, previousAction: Selector,
nextAction: Selector, doneAction: Selector,
showPlaceholder: Bool = false,
titleAccessibilityLabel: String? = nil) {
iq.addPreviousNextDone(target: target, previousAction: previousAction,
nextAction: nextAction, doneAction: doneAction,
showPlaceholder: showPlaceholder,
titleAccessibilityLabel: titleAccessibilityLabel)
}
func iq_addPreviousNextDone(target: AnyObject?,
previousAction: Selector, nextAction: Selector, doneAction: Selector,
title: String?, titleAccessibilityLabel: String? = nil) {
iq.addPreviousNextDone(target: target, previousAction: previousAction,
nextAction: nextAction, doneAction: doneAction,
title: title, titleAccessibilityLabel: titleAccessibilityLabel)
}
}
@objc public extension UITextView {
var iq_toolbar: IQKeyboardToolbar { iq.toolbar }
var iq_hidePlaceholder: Bool {
get { iq.hidePlaceholder }
set(newValue) { iq.hidePlaceholder = newValue }
}
var iq_placeholder: String? {
get { iq.placeholder }
set(newValue) { iq.placeholder = newValue }
}
var iq_drawingPlaceholder: String? { iq.drawingPlaceholder }
func iq_addToolbar(target: AnyObject?,
previousConfiguration: IQBarButtonItemConfiguration? = nil,
nextConfiguration: IQBarButtonItemConfiguration? = nil,
rightConfiguration: IQBarButtonItemConfiguration? = nil,
title: String?,
titleAccessibilityLabel: String? = nil) {
iq.addToolbar(target: target, previousConfiguration: previousConfiguration,
nextConfiguration: nextConfiguration, rightConfiguration: rightConfiguration,
title: title, titleAccessibilityLabel: titleAccessibilityLabel)
}
func iq_addDone(target: AnyObject?,
action: Selector,
showPlaceholder: Bool = false, titleAccessibilityLabel: String? = nil) {
iq.addDone(target: target, action: action, showPlaceholder: showPlaceholder,
titleAccessibilityLabel: titleAccessibilityLabel)
}
func iq_addDone(target: AnyObject?,
action: Selector,
title: String?, titleAccessibilityLabel: String? = nil) {
iq.addDone(target: target, action: action, title: title,
titleAccessibilityLabel: titleAccessibilityLabel)
}
func iq_addRightButton(target: AnyObject?,
configuration: IQBarButtonItemConfiguration,
showPlaceholder: Bool = false, titleAccessibilityLabel: String? = nil) {
iq.addRightButton(target: target, configuration: configuration,
showPlaceholder: showPlaceholder, titleAccessibilityLabel: titleAccessibilityLabel)
}
func iq_addRightButton(target: AnyObject?,
configuration: IQBarButtonItemConfiguration,
title: String?, titleAccessibilityLabel: String? = nil) {
iq.addRightButton(target: target, configuration: configuration,
title: title, titleAccessibilityLabel: titleAccessibilityLabel)
}
func iq_addRightLeft(target: AnyObject?,
rightConfiguration: IQBarButtonItemConfiguration,
leftConfiguration: IQBarButtonItemConfiguration,
showPlaceholder: Bool = false, titleAccessibilityLabel: String? = nil) {
iq.addRightLeft(target: target, rightConfiguration: rightConfiguration,
leftConfiguration: leftConfiguration, showPlaceholder: showPlaceholder,
titleAccessibilityLabel: titleAccessibilityLabel)
}
func iq_addRightLeft(target: AnyObject?,
rightConfiguration: IQBarButtonItemConfiguration,
leftConfiguration: IQBarButtonItemConfiguration,
title: String?, titleAccessibilityLabel: String? = nil) {
iq.addRightLeft(target: target, rightConfiguration: rightConfiguration,
leftConfiguration: leftConfiguration, title: title,
titleAccessibilityLabel: titleAccessibilityLabel)
}
func iq_addPreviousNextRight(target: AnyObject?,
previousConfiguration: IQBarButtonItemConfiguration? = nil,
nextConfiguration: IQBarButtonItemConfiguration? = nil,
rightConfiguration: IQBarButtonItemConfiguration?,
showPlaceholder: Bool = false, titleAccessibilityLabel: String? = nil) {
iq.addPreviousNextRight(target: target, previousConfiguration: previousConfiguration,
nextConfiguration: nextConfiguration, rightConfiguration: rightConfiguration,
showPlaceholder: showPlaceholder, titleAccessibilityLabel: titleAccessibilityLabel)
}
func iq_addPreviousNextRight(target: AnyObject?,
previousConfiguration: IQBarButtonItemConfiguration? = nil,
nextConfiguration: IQBarButtonItemConfiguration? = nil,
rightConfiguration: IQBarButtonItemConfiguration?,
title: String?, titleAccessibilityLabel: String? = nil) {
iq.addPreviousNextRight(target: target, previousConfiguration: previousConfiguration,
nextConfiguration: nextConfiguration,
rightConfiguration: rightConfiguration,
title: title, titleAccessibilityLabel: titleAccessibilityLabel)
}
func iq_addPreviousNextDone(target: AnyObject?, previousAction: Selector, nextAction: Selector,
doneAction: Selector, showPlaceholder: Bool = false,
titleAccessibilityLabel: String? = nil) {
iq.addPreviousNextDone(target: target, previousAction: previousAction,
nextAction: nextAction,
doneAction: doneAction, showPlaceholder: showPlaceholder,
titleAccessibilityLabel: titleAccessibilityLabel)
}
func iq_addPreviousNextDone(target: AnyObject?,
previousAction: Selector, nextAction: Selector, doneAction: Selector,
title: String?, titleAccessibilityLabel: String? = nil) {
iq.addPreviousNextDone(target: target, previousAction: previousAction,
nextAction: nextAction,
doneAction: doneAction, title: title,
titleAccessibilityLabel: titleAccessibilityLabel)
}
}
// swiftlint:enable identifier_name

View File

@ -0,0 +1,183 @@
//
// IQKeyboardToolbar.swift
// https://github.com/hackiftekhar/IQKeyboardToolbar
// 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
/** @abstract IQKeyboardToolbar. */
@available(iOSApplicationExtension, unavailable)
@MainActor
@objcMembers open class IQKeyboardToolbar: UIToolbar {
override public init(frame: CGRect) {
super.init(frame: frame)
initialize()
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initialize()
}
private func initialize() {
sizeToFit()
autoresizingMask = .flexibleWidth
self.isTranslucent = true
self.barTintColor = nil
let positions: [UIBarPosition] = [.any, .bottom, .top, .topAttached]
for position in positions {
self.setBackgroundImage(nil, forToolbarPosition: position, barMetrics: .default)
self.setShadowImage(nil, forToolbarPosition: .any)
}
// Background color
self.backgroundColor = nil
}
/**
Additional bar buttons to show at the leading side.
*/
open var additionalLeadingItems: [UIBarButtonItem] = []
/**
Additional bar buttons to show at the trailing side.
*/
open var additionalTrailingItems: [UIBarButtonItem] = []
/**
Previous bar button of toolbar.
*/
private var privatePreviousBarButton: IQBarButtonItem?
open var previousBarButton: IQBarButtonItem {
get {
if privatePreviousBarButton == nil {
privatePreviousBarButton = IQBarButtonItem(image: nil, style: .plain, target: nil, action: nil)
}
return privatePreviousBarButton!
}
set (newValue) {
privatePreviousBarButton = newValue
}
}
/**
Next bar button of toolbar.
*/
private var privateNextBarButton: IQBarButtonItem?
open var nextBarButton: IQBarButtonItem {
get {
if privateNextBarButton == nil {
privateNextBarButton = IQBarButtonItem(image: nil, style: .plain, target: nil, action: nil)
}
return privateNextBarButton!
}
set (newValue) {
privateNextBarButton = newValue
}
}
/**
Title bar button of toolbar.
*/
private var privateTitleBarButton: IQTitleBarButtonItem?
open var titleBarButton: IQTitleBarButtonItem {
get {
if privateTitleBarButton == nil {
privateTitleBarButton = IQTitleBarButtonItem(title: nil)
}
return privateTitleBarButton!
}
set (newValue) {
privateTitleBarButton = newValue
}
}
/**
Done bar button of toolbar.
*/
private var privateDoneBarButton: IQBarButtonItem?
open var doneBarButton: IQBarButtonItem {
get {
if privateDoneBarButton == nil {
privateDoneBarButton = IQBarButtonItem(title: nil, style: .done, target: nil, action: nil)
#if compiler(>=6.2) // Xcode 26
let requiresCompatibility: Bool = Bundle.main.object(forInfoDictionaryKey: "UIDesignRequiresCompatibility") as? Bool ?? false
if #available(iOS 26.0, *), !requiresCompatibility {
privateDoneBarButton?.style = .plain
}
#endif
}
return privateDoneBarButton!
}
set (newValue) {
privateDoneBarButton = newValue
}
}
override open func sizeThatFits(_ size: CGSize) -> CGSize {
var sizeThatFit: CGSize = super.sizeThatFits(size)
let height: CGFloat
#if compiler(>=6.2) // Xcode 26
let requiresCompatibility: Bool = Bundle.main.object(forInfoDictionaryKey: "UIDesignRequiresCompatibility") as? Bool ?? false
if #available(iOS 26.0, *), !requiresCompatibility {
height = 58
} else {
height = 44
}
#else
height = 44
#endif
sizeThatFit.height = height
return sizeThatFit
}
override open var tintColor: UIColor! {
didSet {
guard let items: [UIBarButtonItem] = items else { return }
for item in items {
item.tintColor = tintColor
}
}
}
}
@available(iOSApplicationExtension, unavailable)
@MainActor
@objc extension IQKeyboardToolbar: UIInputViewAudioFeedback {
open var enableInputClicksWhenVisible: Bool {
return true
}
}

View File

@ -0,0 +1,60 @@
//
// IQKeyboardToolbarPlaceholderConfiguration.swift
// https://github.com/hackiftekhar/IQKeyboardToolbar
// 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 type_name
@available(iOSApplicationExtension, unavailable)
@MainActor
@objcMembers public final class IQKeyboardToolbarPlaceholderConfiguration: NSObject {
/**
If YES, then it add the textInputView's placeholder text on toolbar. Default is YES.
*/
public var showPlaceholder: Bool = true
/**
Placeholder Font. Default is nil.
*/
public var font: UIFont?
/**
Placeholder Color. Default is nil. Which means lightGray
*/
public var color: UIColor?
/**
Placeholder Button Color when it's treated as button. Default is nil.
*/
public var buttonColor: UIColor?
/**
Placeholder accessibility Label
*/
public override var accessibilityLabel: String? { didSet { } }
}
// swiftlint:enable type_name
@available(*, unavailable, renamed: "IQKeyboardToolbarPlaceholderConfiguration")
@MainActor
@objcMembers public final class IQToolbarPlaceholderConfiguration: NSObject {}

View File

@ -0,0 +1,37 @@
//
// IQPlaceholderable.swift
// https://github.com/hackiftekhar/IQKeyboardToolbar
// 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 Foundation
import UIKit
@available(iOSApplicationExtension, unavailable)
@MainActor
@objc public protocol IQPlaceholderable: AnyObject {
var placeholder: String? { get set }
var attributedPlaceholder: NSAttributedString? { get set }
}
@available(iOSApplicationExtension, unavailable)
@MainActor
@objc extension UITextField: IQPlaceholderable { }

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

85
Pods/IQKeyboardToolbar/README.md generated Normal file
View File

@ -0,0 +1,85 @@
# IQKeyboardToolbar
[![CI Status](https://img.shields.io/travis/hackiftekhar/IQKeyboardToolbar.svg?style=flat)](https://travis-ci.org/hackiftekhar/IQKeyboardToolbar)
[![Version](https://img.shields.io/cocoapods/v/IQKeyboardToolbar.svg?style=flat)](https://cocoapods.org/pods/IQKeyboardToolbar)
[![License](https://img.shields.io/cocoapods/l/IQKeyboardToolbar.svg?style=flat)](https://cocoapods.org/pods/IQKeyboardToolbar)
[![Platform](https://img.shields.io/cocoapods/p/IQKeyboardToolbar.svg?style=flat)](https://cocoapods.org/pods/IQKeyboardToolbar)
![Screenshot1](https://raw.githubusercontent.com/hackiftekhar/IQKeyboardToolbar/master/Screenshot/IQKeyboardToolbarScreenshot.png)
![Screenshot2](https://raw.githubusercontent.com/hackiftekhar/IQKeyboardToolbar/master/Screenshot/IQKeyboardToolbarScreenshot2.png)
![Screenshot3](https://raw.githubusercontent.com/hackiftekhar/IQKeyboardToolbar/master/Screenshot/IQKeyboardToolbarScreenshot3.png)
![Screenshot4](https://raw.githubusercontent.com/hackiftekhar/IQKeyboardToolbar/master/Screenshot/IQKeyboardToolbarScreenshot4.png)
## Example
To run the example project, clone the repo, and run `pod install` from the Example directory first.
## Requirements
## Installation
IQKeyboardToolbar is available through [CocoaPods](https://cocoapods.org). To install
it, simply add the following line to your Podfile:
```ruby
pod 'IQKeyboardToolbar'
```
## Usage
This library can be used to add toolbar to the keyboard
Previous Next and Done button
```swift
textField.iq.addPreviousNextDone(target: self,
previousAction: #selector(textField1PreviousAction),
nextAction: #selector(textField1NextAction),
doneAction: #selector(doneAction), showPlaceholder: true)
```
Previous Next and Right button with customized titles or images
```swift
textField.iq.addPreviousNextRight(target: self,
previousConfiguration: .init(title: "Prev", action: #selector(textView1PreviousAction)),
nextConfiguration: .init(title: "Next", action: #selector(textView1NextAction)),
rightConfiguration: .init(image: UIImage(systemName: "chevron.down")!, action: #selector(doneAction)),
title: "Text View 1")
```
Action button
```swift
textField.iq.addDone(target: self,
action: #selector(doneAction),
title: "Select Account")
textField.iq.toolbar.titleBarButton.setTarget(self,
action: #selector(selectAccount))
```
Additional leading and trailing buttons
```swift
textField.iq.toolbar.additionalLeadingItems = [.init(barButtonSystemItem: .add, target: self, action: #selector(addAction))]
textField.iq.toolbar.additionalTrailingItems = [.init(barButtonSystemItem: .camera, target: self, action: #selector(cameraAction))]
textField.iq.addToolbar(target: self,
previousConfiguration: nil,
nextConfiguration: .init(title: "Next", action: #selector(doneAction)),
rightConfiguration: .init(title: "Finish", action: #selector(doneAction)),
title: "TextView 2")
```
Hide Placeholder
```swift
textField1.iq.hidePlaceholder = true
```
Customized Placeholder
```swift
textField1.iq.placeholder = "My Own Placeholder"
```
## Author
Iftekhar Qurashi hack.iftekhar@gmail.com
## License
IQKeyboardToolbar is available under the MIT license. See the LICENSE file for more info.

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,90 @@
//
// IQKeyboardToolbarConfiguration.swift
// https://github.com/hackiftekhar/IQKeyboardToolbarManager
// 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 IQKeyboardToolbar
@available(iOSApplicationExtension, unavailable)
@MainActor
@objcMembers public final class IQKeyboardToolbarConfiguration: NSObject {
/**
If YES, then uses textInputView's tintColor property,
otherwise tint color is default. Default is NO.
*/
public var useTextInputViewTintColor: Bool = false
/**
This is used for toolbar.tintColor when textInputView.keyboardAppearance is UIKeyboardAppearanceDefault.
If useTextInputViewTintColor is YES then this property is ignored. Default is nil and uses black color.
*/
public var tintColor: UIColor?
/**
This is used for toolbar.barTintColor. Default is nil.
*/
public var barTintColor: UIColor?
/**
IQPreviousNextDisplayModeDefault: Show NextPrevious when there are more than 1 textInputView otherwise hide.
IQPreviousNextDisplayModeAlwaysHide: Do not show NextPrevious buttons in any case.
IQPreviousNextDisplayModeAlwaysShow: Always show nextPrevious buttons,
if there are more than 1 textInputView then both buttons will be visible but will be shown as disabled.
*/
public var previousNextDisplayMode: IQPreviousNextDisplayMode = .default
/**
/**
IQAutoToolbarBySubviews: Creates Toolbar according to subview's hierarchy of TextInputView's in view.
IQAutoToolbarByTag: Creates Toolbar according to tag property of TextInputView's.
IQAutoToolbarByPosition: Creates Toolbar according to the y,x position
of textInputView in it's superview coordinate.
Default is IQAutoToolbarBySubviews.
*/
AutoToolbar managing behavior. Default is IQAutoToolbarBySubviews.
*/
public var manageBehavior: IQKeyboardToolbarManageBehavior = .bySubviews
/**
Buttons configuration displayed on the toolbar, the selector parameter is ignored in below configuration
*/
public var previousBarButtonConfiguration: IQBarButtonItemConfiguration?
public var nextBarButtonConfiguration: IQBarButtonItemConfiguration?
public var doneBarButtonConfiguration: IQBarButtonItemConfiguration?
public let placeholderConfiguration: IQKeyboardToolbarPlaceholderConfiguration = .init()
}
// swiftlint:disable unused_setter_value
@available(iOSApplicationExtension, unavailable)
@MainActor
@objc public extension IQKeyboardToolbarConfiguration {
@available(*, unavailable, renamed: "useTextInputViewTintColor")
var useTextFieldTintColor: Bool {
get { fatalError() }
set {}
}
}
// swiftlint:enable unused_setter_value

View File

@ -0,0 +1,68 @@
//
// IQKeyboardToolbarConstants.swift
// https://github.com/hackiftekhar/IQKeyboardToolbarManager
// 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 Foundation
// MARK: IQKeyboardToolbarManageBehavior
/**
`IQAutoToolbarBySubviews`
Creates Toolbar according to subview's hierarchy of textInputView's in view.
`IQAutoToolbarByTag`
Creates Toolbar according to tag property of textInputView's.
`IQAutoToolbarByPosition`
Creates Toolbar according to the y,x position of textInputView in it's superview coordinate.
*/
@available(iOSApplicationExtension, unavailable)
@objc public enum IQKeyboardToolbarManageBehavior: Int {
case bySubviews
case byTag
case byPosition
}
@available(*, unavailable, renamed: "IQKeyboardToolbarManageBehavior")
@objc public enum IQAutoToolbarManageBehavior: Int {
case bySubviews
case byTag
case byPosition
}
/**
`IQPreviousNextDisplayModeDefault`
Show NextPrevious when there are more than 1 textInputView otherwise hide.
`IQPreviousNextDisplayModeAlwaysHide`
Do not show NextPrevious buttons in any case.
`IQPreviousNextDisplayModeAlwaysShow`
Always show nextPrevious buttons,
if there are more than 1 textInputView then both buttons will be visible but will be shown as disabled.
*/
@available(iOSApplicationExtension, unavailable)
@objc public enum IQPreviousNextDisplayMode: Int {
case `default`
case alwaysHide
case alwaysShow
}

View File

@ -0,0 +1,52 @@
//
// IQKeyboardToolbarManager+Debug
// https://github.com/hackiftekhar/IQKeyboardToolbarManager
// 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 Foundation
// MARK: Debugging & Developer options
@available(iOSApplicationExtension, unavailable)
@MainActor
internal extension IQKeyboardToolbarManager {
func showLog(_ logString: String, indentation: Int = 0) {
guard isDebuggingEnabled else {
return
}
if indentation < 0 {
self.logIndentation = max(0, self.logIndentation + indentation)
}
var preLog: String = "IQKeyboardToolbarManager"
for _ in 0 ... self.logIndentation {
preLog += "|\t"
}
print(preLog + logString)
if indentation > 0 {
self.logIndentation += indentation
}
}
}

View File

@ -0,0 +1,29 @@
//
// IQDeepResponderContainerView.swift
// https://github.com/hackiftekhar/IQKeyboardToolbarManager
// 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 open class IQDeepResponderContainerView: UIView {
}

View File

@ -0,0 +1,183 @@
//
// IQKeyboardToolbarManager+Deprecated.swift
// https://github.com/hackiftekhar/IQKeyboardToolbarManager
// 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 IQKeyboardToolbar
// swiftlint:disable unused_setter_value
// swiftlint:disable identifier_name
@available(iOSApplicationExtension, unavailable)
@MainActor
@objc public extension IQKeyboardToolbarManager {
@available(*, unavailable, renamed: "isEnabled")
var enableAutoToolbar: Bool {
get { false }
set { }
}
@available(*, unavailable, renamed: "playInputClicks")
var shouldPlayInputClicks: Bool {
get { false }
set { }
}
}
@available(iOSApplicationExtension, unavailable)
@MainActor
@objc public extension IQKeyboardToolbarManager {
@available(*, unavailable, renamed: "toolbarConfiguration.manageBehavior")
var toolbarManageBehaviour: IQAutoToolbarManageBehavior {
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 IQKeyboardToolbarManager {
@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 IQKeyboardToolbarManager {
@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,148 @@
//
// IQKeyboardToolbarManager+Internal.swift
// https://github.com/hackiftekhar/IQKeyboardToolbarManager
// 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
internal extension IQKeyboardToolbarManager {
/** Get all textInputView siblings of textInputView. */
func responderViews(of textInputView: some IQTextInputView) -> [any IQTextInputView]? {
var superConsideredView: UIView?
// If find any consider responderView in it's upper hierarchy then will get deepResponderView.
for allowedClass in deepResponderAllowedContainerClasses {
superConsideredView = (textInputView as UIView).iq.superviewOf(type: allowedClass)
if superConsideredView != nil {
break
}
}
let swiftUIHostingView: UIView? = Self.getSwiftUIHostingView(textInputView: textInputView)
// (Enhancement ID: #22)
// If there is a superConsideredView in view's hierarchy,
// then fetching all it's subview that responds.
// No sorting for superConsideredView, it's by subView position.
if let view: UIView = swiftUIHostingView {
return view.iq.deepResponderViews()
} else if let view: UIView = superConsideredView {
return view.iq.deepResponderViews()
} else { // Otherwise fetching all the siblings
let textInputViews: [any IQTextInputView] = textInputView.iq.responderSiblings()
// Sorting textInputViews according to behavior
switch toolbarConfiguration.manageBehavior {
// If autoToolbar behavior is bySubviews, then returning it.
case .bySubviews: return textInputViews
// If autoToolbar behavior is by tag, then sorting it according to tag property.
case .byTag: return textInputViews.sortedByTag()
// If autoToolbar behavior is by tag, then sorting it according to tag property.
case .byPosition: return textInputViews.sortedByPosition()
}
}
}
func privateIsEnableAutoToolbar(of textInputView: some IQTextInputView) -> Bool {
var isEnabled: Bool = isEnabled
guard var textInputViewController = (textInputView as UIView).iq.viewContainingController() else {
return isEnabled
}
if textInputView is UISearchTextField {
if let navController: UINavigationController = textInputViewController as? UINavigationController,
let topController: UIViewController = navController.topViewController {
textInputViewController = topController
}
// Not adding toolbar for searchTextField inside searchController.
if textInputViewController.navigationItem.searchController?.searchBar.searchTextField == textInputView {
return false
}
}
if !isEnabled, enabledToolbarClasses.contains(where: { textInputViewController.isKind(of: $0) }) {
isEnabled = true
}
if isEnabled {
// If found any toolbar disabled classes then return.
if disabledToolbarClasses.contains(where: { textInputViewController.isKind(of: $0) }) {
isEnabled = false
}
// Special Controllers
if isEnabled {
let classNameString: String = "\(type(of: textInputViewController.self))"
// _UIAlertControllerTextFieldViewController
if classNameString.contains("UIAlertController"),
classNameString.hasSuffix("TextFieldViewController") {
isEnabled = false
}
}
}
return isEnabled
}
}
@available(iOSApplicationExtension, unavailable)
@MainActor
private extension IQKeyboardToolbarManager {
private static func getSwiftUIHostingView(textInputView: some IQTextInputView) -> UIView? {
var swiftUIHostingView: UIView?
let swiftUIHostingViewName: String = "UIHostingView<"
var superView: UIView? = textInputView.superview
while let aSuperview: UIView = superView {
let classNameString: String = {
var name: String = "\(type(of: aSuperview.self))"
if name.hasPrefix("_") {
name.removeFirst()
}
return name
}()
if classNameString.hasPrefix(swiftUIHostingViewName) {
swiftUIHostingView = aSuperview
break
}
superView = aSuperview.superview
}
return swiftUIHostingView
}
}

View File

@ -0,0 +1,173 @@
//
// IQKeyboardToolbarManager.swift
// https://github.com/hackiftekhar/IQKeyboardToolbarManager
// 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
import IQKeyboardToolbar
import Combine
@available(iOSApplicationExtension, unavailable)
@MainActor
@objcMembers public final class IQKeyboardToolbarManager: NSObject {
private let textInputViewObserver: IQTextInputViewNotification = .init()
private var storage: Set<AnyCancellable> = []
internal var textInputView: (some IQTextInputView)? {
textInputViewObserver.textInputView
}
/**
Returns the default singleton instance.
*/
@MainActor
@objc public static let shared: IQKeyboardToolbarManager = .init()
/**
Automatic add the toolbar functionality. Default is false.
*/
public var isEnabled: Bool = false {
didSet {
reloadInputViews()
showLog("isEnabled: \(isEnabled ? "Yes" : "NO")")
}
}
public var isDebuggingEnabled: Bool = false
/**
Configurations related to the toolbar display over the keyboard.
*/
public let toolbarConfiguration: IQKeyboardToolbarConfiguration = .init()
// MARK: UISound handling
/**
If YES, then it plays inputClick sound on next/previous/done click.
*/
public var playInputClicks: Bool = true
/**
Disable automatic toolbar creation within the scope of disabled toolbar viewControllers classes.
Within this scope, 'isEnabled' property is ignored. Class should be kind of UIViewController.
*/
public var disabledToolbarClasses: [UIViewController.Type] = [
UIAlertController.self,
UIInputViewController.self
]
/**
Enable automatic toolbar creation within the scope of enabled toolbar viewControllers classes.
Within this scope, 'isEnabled' property is ignored. Class should be kind of UIViewController.
If same Class is added in disabledToolbarClasses list, then enabledToolbarClasses will be ignore.
*/
public var enabledToolbarClasses: [UIViewController.Type] = []
/**
Allowed subclasses of UIView to add all inner textInputView,
this will allow to navigate between textInputView contains in different superview.
Class should be kind of UIView.
*/
public var deepResponderAllowedContainerClasses: [UIView.Type] = [
UITableView.self,
UICollectionView.self,
IQDeepResponderContainerView.self
]
internal var logIndentation = 0
internal override init() {
super.init()
addTextInputViewObserver()
addBecomeActiveObserver()
// (Bug ID: #550)
// Loading IQKeyboardToolbar, IQTitleBarButtonItem, IQBarButtonItem to fix first time keyboard appearance delay
// If you experience exception breakpoint issue at below line then try these solutions
// https://stackoverflow.com/questions/27375640/all-exception-break-point-is-stopping-for-no-reason-on-simulator
DispatchQueue.main.async {
let textInputView: UITextField = UITextField()
textInputView.iq.addDone(target: nil, action: #selector(self.doneAction(_:)))
textInputView.iq.addPreviousNextDone(target: nil,
previousAction: #selector(self.previousAction(_:)),
nextAction: #selector(self.nextAction(_:)),
doneAction: #selector(self.doneAction(_:)))
}
}
}
@available(iOSApplicationExtension, unavailable)
@MainActor
private extension IQKeyboardToolbarManager {
private func removeTextInputViewObserver() {
textInputViewObserver.unsubscribe(identifier: "IQKeyboardToolbarManager")
}
private func addTextInputViewObserver() {
textInputViewObserver.subscribe(identifier: "IQKeyboardToolbarManager",
changeHandler: { [weak self] event, textInputView in
guard let self = self else { return }
guard (textInputView as UIView).iq.isAlertViewTextField() == false else {
return
}
switch event {
case .beginEditing:
reloadInputViews()
case .endEditing:
removeToolbarIfRequired(of: textInputView)
}
})
}
}
@available(iOSApplicationExtension, unavailable)
@MainActor
private extension IQKeyboardToolbarManager {
private func removeBecomeActiveObserver() {
storage.forEach { $0.cancel() }
storage.removeAll()
}
private func addBecomeActiveObserver() {
let beginEditingNotificationNames: [Notification.Name] = [
UIApplication.didBecomeActiveNotification
]
for notificationName in beginEditingNotificationNames {
NotificationCenter.default.publisher(for: notificationName)
.sink(receiveValue: { [weak self] info in
guard let self = self else { return }
// In SwiftUI View's when app comes from background to foreground
// The inputAccessoryView was getting removed in iOS 26. So forcing to reload.
self.reloadInputViews()
})
.store(in: &storage)
}
}
}

View File

@ -0,0 +1,210 @@
//
// IQKeyboardToolbarManager+Action.swift
// https://github.com/hackiftekhar/IQKeyboardToolbarManager
// 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 IQKeyboardToolbar
// MARK: Previous next button actions
@available(iOSApplicationExtension, unavailable)
@MainActor
@objc public extension IQKeyboardToolbarManager {
/**
Returns YES if can navigate to previous responder textInputView, otherwise NO.
*/
var canGoPrevious: Bool {
// If it is not first textInputView. then it's previous object canBecomeFirstResponder.
guard let textInputView: any IQTextInputView = self.textInputView,
let textInputViews: [any IQTextInputView] = responderViews(of: textInputView),
let index: Int = textInputViews.firstIndex(where: { $0 == textInputView }),
index > 0 else {
return false
}
return true
}
/**
Returns YES if can navigate to next responder textInputViews, otherwise NO.
*/
var canGoNext: Bool {
// If it is not first textInputView. then it's previous object canBecomeFirstResponder.
guard let textInputView: any IQTextInputView = self.textInputView,
let textInputViews: [any IQTextInputView] = responderViews(of: textInputView),
let index: Int = textInputViews.firstIndex(where: { $0 == textInputView }),
index < textInputViews.count-1 else {
return false
}
return true
}
/**
Navigate to previous responder textInputViews
*/
@discardableResult
func goPrevious() -> Bool {
// If it is not first textInputView. then it's previous object becomeFirstResponder.
guard let textInputView: any IQTextInputView = self.textInputView,
let textInputViews: [any IQTextInputView] = responderViews(of: textInputView),
let index: Int = textInputViews.firstIndex(where: { $0 == textInputView }),
index > 0 else {
return false
}
let nextTextInputView: any IQTextInputView = textInputViews[index-1]
let isAcceptAsFirstResponder: Bool = nextTextInputView.becomeFirstResponder()
// If it refuses then becoming previous textInputView as first responder again. (Bug ID: #96)
if !isAcceptAsFirstResponder {
showLog("Refuses to become first responder: \(nextTextInputView)")
}
return isAcceptAsFirstResponder
}
/**
Navigate to next responder textInputView.
*/
@discardableResult
func goNext() -> Bool {
// If it is not first textInputView. then it's previous object becomeFirstResponder.
guard let textInputView: any IQTextInputView = self.textInputView,
let textInputViews: [any IQTextInputView] = responderViews(of: textInputView),
let index: Int = textInputViews.firstIndex(where: { $0 == textInputView }),
index < textInputViews.count-1 else {
return false
}
let nextTextInputView: any IQTextInputView = textInputViews[index+1]
let isAcceptAsFirstResponder: Bool = nextTextInputView.becomeFirstResponder()
// If it refuses then becoming previous textInputView as first responder again. (Bug ID: #96)
if !isAcceptAsFirstResponder {
showLog("Refuses to become first responder: \(nextTextInputView)")
}
return isAcceptAsFirstResponder
}
}
@available(iOSApplicationExtension, unavailable)
@MainActor
internal extension IQKeyboardToolbarManager {
/** previousAction. */
func previousAction(_ barButton: IQBarButtonItem) {
// If user wants to play input Click sound.
if playInputClicks {
// Play Input Click Sound.
UIDevice.current.playInputClick()
}
guard canGoPrevious,
let textInputView: any IQTextInputView = self.textInputView else {
return
}
let isAcceptedAsFirstResponder: Bool = goPrevious()
if isAcceptedAsFirstResponder {
Self.sendInvokeAction(of: barButton, sender: textInputView)
}
}
/** nextAction. */
func nextAction(_ barButton: IQBarButtonItem) {
// If user wants to play input Click sound.
if playInputClicks {
// Play Input Click Sound.
UIDevice.current.playInputClick()
}
guard canGoNext,
let textInputView: any IQTextInputView = self.textInputView else {
return
}
let isAcceptedAsFirstResponder: Bool = goNext()
if isAcceptedAsFirstResponder {
Self.sendInvokeAction(of: barButton, sender: textInputView)
}
}
/** doneAction. Resigning current textInputView. */
func doneAction(_ barButton: IQBarButtonItem) {
// If user wants to play input Click sound.
if playInputClicks {
// Play Input Click Sound.
UIDevice.current.playInputClick()
}
guard let textInputView: any IQTextInputView = self.textInputView else {
return
}
// Resign textInputView.
let isResignedFirstResponder: Bool = textInputView.resignFirstResponder()
if isResignedFirstResponder {
Self.sendInvokeAction(of: barButton, sender: textInputView)
}
}
}
@available(iOSApplicationExtension, unavailable)
@MainActor
private extension IQKeyboardToolbarManager {
private static func sendInvokeAction(of barButton: IQBarButtonItem, sender: some IQTextInputView) {
// Handling search bar special case
if let searchBar: UISearchBar = sender.iq.textFieldSearchBar() {
switch barButton {
case sender.internalToolbar.nextBarButton:
searchBar.internalToolbar.nextBarButton.invocation?.invoke(from: searchBar)
case sender.internalToolbar.previousBarButton:
searchBar.internalToolbar.previousBarButton.invocation?.invoke(from: searchBar)
case sender.internalToolbar.doneBarButton:
searchBar.internalToolbar.doneBarButton.invocation?.invoke(from: searchBar)
default:
break
}
} else {
barButton.invocation?.invoke(from: sender)
}
}
}
@available(iOSApplicationExtension, unavailable)
@MainActor
fileprivate extension IQTextInputView {
var internalToolbar: IQKeyboardToolbar {
return iq.toolbar
}
}

View File

@ -0,0 +1,287 @@
//
// IQKeyboardToolbarManager+Toolbar.swift
// https://github.com/hackiftekhar/IQKeyboardToolbarManager
// 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 IQKeyboardToolbar
@available(iOSApplicationExtension, unavailable)
@MainActor
@objc public extension IQKeyboardToolbarManager {
/** reloadInputViews to reload toolbar buttons enable/disable state on the fly Enhancement ID #434. */
func reloadInputViews() {
guard let textInputView = textInputView else { return }
// If enabled then adding toolbar.
if privateIsEnableAutoToolbar(of: textInputView) {
self.addToolbarIfRequired(of: textInputView)
} else {
self.removeToolbarIfRequired(of: textInputView)
}
}
}
@available(iOSApplicationExtension, unavailable)
@MainActor
internal extension IQKeyboardToolbarManager {
/**
Default tag for toolbar with Done button -1001
*/
private static let toolbarTag = -1001
// swiftlint:disable function_body_length
// swiftlint:disable cyclomatic_complexity
/**
Add toolbar if it is required to add on textInputViews and it's siblings.
*/
func addToolbarIfRequired(of textInputView: some IQTextInputView) {
// Either there is no inputAccessoryView or
// if accessoryView is not appropriate for current situation
// (There is Previous/Next/Done toolbar)
guard let siblings: [any IQTextInputView] = responderViews(of: textInputView), !siblings.isEmpty,
!Self.hasUserDefinedInputAccessoryView(textInputView: textInputView) else {
return
}
showLog(">>>>> \(#function) started >>>>>", indentation: 1)
defer {
showLog("<<<<< \(#function) ended <<<<<", indentation: -1)
}
showLog("Found \(siblings.count) responder sibling(s)")
let rightConfiguration: IQBarButtonItemConfiguration = getRightConfiguration()
let isTableCollectionView: Bool
if (textInputView as UIView).iq.superviewOf(type: UITableView.self) != nil ||
(textInputView as UIView).iq.superviewOf(type: UICollectionView.self) != nil {
isTableCollectionView = true
} else {
isTableCollectionView = false
}
let previousNextDisplayMode: IQPreviousNextDisplayMode = toolbarConfiguration.previousNextDisplayMode
let havePreviousNext: Bool
switch previousNextDisplayMode {
case .default:
// If the textInputView is part of UITableView/UICollectionView then we should be exposing previous/next too
// Because at this time we don't know previous or next cell if it contains another textInputView to move.
if isTableCollectionView {
havePreviousNext = true
} else if siblings.count <= 1 {
// If only one object is found, then adding only Done button.
havePreviousNext = false
} else {
havePreviousNext = true
}
case .alwaysShow:
havePreviousNext = true
case .alwaysHide:
havePreviousNext = false
}
let placeholderConfig: IQKeyboardToolbarPlaceholderConfiguration = toolbarConfiguration.placeholderConfiguration
let titleText: String? = placeholderConfig.showPlaceholder ? textInputView.iq.drawingPlaceholder : nil
if havePreviousNext {
let prevConfiguration: IQBarButtonItemConfiguration = getPreviousConfiguration()
let nextConfiguration: IQBarButtonItemConfiguration = getNextConfiguration()
textInputView.iq.addToolbar(target: self,
previousConfiguration: prevConfiguration,
nextConfiguration: nextConfiguration,
rightConfiguration: rightConfiguration, title: titleText,
titleAccessibilityLabel: placeholderConfig.accessibilityLabel)
if isTableCollectionView {
// (Bug ID: #56)
// In case of UITableView, the next/previous buttons should always be enabled.
textInputView.iq.toolbar.previousBarButton.isEnabled = true
textInputView.iq.toolbar.nextBarButton.isEnabled = true
} else {
// If first textInputView, then previous should not be enabled.
if let first = siblings.first {
textInputView.iq.toolbar.previousBarButton.isEnabled = first != textInputView
} else {
textInputView.iq.toolbar.previousBarButton.isEnabled = false
}
// If last textInputView, then next should not be enabled.
if let last = siblings.last {
textInputView.iq.toolbar.nextBarButton.isEnabled = last != textInputView
} else {
textInputView.iq.toolbar.nextBarButton.isEnabled = false
}
}
} else {
textInputView.iq.addToolbar(target: self, rightConfiguration: rightConfiguration,
title: titleText,
titleAccessibilityLabel: placeholderConfig.accessibilityLabel)
}
// (Bug ID: #78)
textInputView.inputAccessoryView?.tag = IQKeyboardToolbarManager.toolbarTag
Self.applyToolbarConfiguration(textInputView: textInputView, toolbarConfiguration: toolbarConfiguration)
}
// swiftlint:enable function_body_length
// swiftlint:enable cyclomatic_complexity
/** Remove any toolbar if it is IQKeyboardToolbar. */
func removeToolbarIfRequired(of textInputView: some IQTextInputView) { // (Bug ID: #18)
guard let toolbar: IQKeyboardToolbar = textInputView.inputAccessoryView as? IQKeyboardToolbar,
toolbar.tag == IQKeyboardToolbarManager.toolbarTag else {
return
}
showLog(">>>>> \(#function) started >>>>>", indentation: 1)
defer {
showLog("<<<<< \(#function) ended <<<<<", indentation: -1)
}
textInputView.inputAccessoryView = nil
}
}
@available(iOSApplicationExtension, unavailable)
@MainActor
private extension IQKeyboardToolbarManager {
static func hasUserDefinedInputAccessoryView(textInputView: some IQTextInputView) -> Bool {
guard let inputAccessoryView: UIView = textInputView.inputAccessoryView,
inputAccessoryView.tag != IQKeyboardToolbarManager.toolbarTag else { return false }
let swiftUIAccessoryNamePrefixes: [String] = [
"InputAccessoryHost<InputAccessoryBar>", // iOS 17 and below
"RootUIView", // iOS 18
"_UIInputViewContent", // iOS 18 system views
"_UIKeyboardInputAccessory" // Additional iOS 18 system view
]
let classNameString: String = "\(type(of: inputAccessoryView.classForCoder))"
// If it's SwiftUI accessory view but doesn't have a height (fake accessory view), then we should
// add our own accessoryView otherwise, keep the SwiftUI accessoryView since user has added it from code
let isSwiftUIAccessoryView = swiftUIAccessoryNamePrefixes.contains(where: { classNameString.hasPrefix($0) })
// A height of 0 or very small height typically indicates a system placeholder
guard isSwiftUIAccessoryView,
inputAccessoryView.subviews.isEmpty else {
return true
}
return false
}
func getRightConfiguration() -> IQBarButtonItemConfiguration {
let rightConfiguration: IQBarButtonItemConfiguration
if let configuration: IQBarButtonItemConfiguration = toolbarConfiguration.doneBarButtonConfiguration {
rightConfiguration = configuration
rightConfiguration.action = #selector(self.doneAction(_:))
} else {
#if compiler(>=6.2) // Xcode 26
let requiresCompatibility: Bool = Bundle.main.object(forInfoDictionaryKey: "UIDesignRequiresCompatibility") as? Bool ?? false
if #available(iOS 26.0, *), !requiresCompatibility {
rightConfiguration = IQBarButtonItemConfiguration(image: UIImage(systemName: "checkmark")!, action: #selector(self.doneAction(_:)))
} else {
rightConfiguration = IQBarButtonItemConfiguration(systemItem: .done, action: #selector(self.doneAction(_:)))
}
#else
rightConfiguration = IQBarButtonItemConfiguration(systemItem: .done, action: #selector(self.doneAction(_:)))
#endif
rightConfiguration.accessibilityLabel = "Done"
}
return rightConfiguration
}
func getPreviousConfiguration() -> IQBarButtonItemConfiguration {
let prevConfiguration: IQBarButtonItemConfiguration
if let configuration: IQBarButtonItemConfiguration = toolbarConfiguration.previousBarButtonConfiguration {
configuration.action = #selector(self.previousAction(_:))
prevConfiguration = configuration
} else {
let chevronUp: UIImage = UIImage(systemName: "chevron.up") ?? UIImage()
prevConfiguration = IQBarButtonItemConfiguration(image: chevronUp,
action: #selector(self.previousAction(_:)))
prevConfiguration.accessibilityLabel = "Previous"
}
return prevConfiguration
}
func getNextConfiguration() -> IQBarButtonItemConfiguration {
let nextConfiguration: IQBarButtonItemConfiguration
if let configuration: IQBarButtonItemConfiguration = toolbarConfiguration.nextBarButtonConfiguration {
configuration.action = #selector(self.nextAction(_:))
nextConfiguration = configuration
} else {
let chevronDown: UIImage = UIImage(systemName: "chevron.down") ?? UIImage()
nextConfiguration = IQBarButtonItemConfiguration(image: chevronDown,
action: #selector(self.nextAction(_:)))
nextConfiguration.accessibilityLabel = "Next"
}
return nextConfiguration
}
static func applyToolbarConfiguration(textInputView: some IQTextInputView,
toolbarConfiguration: IQKeyboardToolbarConfiguration) {
let toolbar: IQKeyboardToolbar = textInputView.iq.toolbar
// Setting toolbar tintColor // (Enhancement ID: #30)
if toolbarConfiguration.useTextInputViewTintColor {
toolbar.tintColor = textInputView.tintColor
} else {
toolbar.tintColor = toolbarConfiguration.tintColor
}
// Bar style according to keyboard appearance
switch textInputView.keyboardAppearance {
case .dark:
toolbar.barStyle = .black
toolbar.barTintColor = nil
default:
toolbar.barStyle = .default
toolbar.barTintColor = toolbarConfiguration.barTintColor
}
// Setting toolbar title font. // (Enhancement ID: #30)
guard toolbarConfiguration.placeholderConfiguration.showPlaceholder,
!textInputView.iq.hidePlaceholder else {
toolbar.titleBarButton.title = nil
return
}
// Updating placeholder font to toolbar. //(Bug ID: #148, #272)
if toolbar.titleBarButton.title == nil ||
toolbar.titleBarButton.title != textInputView.iq.drawingPlaceholder {
toolbar.titleBarButton.title = textInputView.iq.drawingPlaceholder
}
// Setting toolbar title font. // (Enhancement ID: #30)
toolbar.titleBarButton.titleFont = toolbarConfiguration.placeholderConfiguration.font
// Setting toolbar title color. // (Enhancement ID: #880)
toolbar.titleBarButton.titleColor = toolbarConfiguration.placeholderConfiguration.color
// Setting toolbar button title color. // (Enhancement ID: #880)
toolbar.titleBarButton.selectableTitleColor = toolbarConfiguration.placeholderConfiguration.buttonColor
}
}

View File

@ -0,0 +1,58 @@
//
// Array+Sort.swift
// https://github.com/hackiftekhar/IQKeyboardToolbarManager
// 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.subviews sorting category.
*/
@available(iOSApplicationExtension, unavailable)
@MainActor
internal extension Array where Element: IQTextInputView {
/**
Returns the array by sorting the UIView's by their tag property.
*/
func sortedByTag() -> [Element] {
return sorted(by: { (obj1: Element, obj2: Element) -> Bool in
return (obj1.tag < obj2.tag)
})
}
/**
Returns the array by sorting the UIView's by their tag property.
*/
func sortedByPosition() -> [Element] {
return sorted(by: { (obj1: Element, obj2: Element) -> Bool in
if obj1.frame.minY != obj2.frame.minY {
return obj1.frame.minY < obj2.frame.minY
} else {
return obj1.frame.minX < obj2.frame.minX
}
})
}
}

View File

@ -0,0 +1,179 @@
//
// UIView+Responders.swift
// https://github.com/hackiftekhar/IQKeyboardToolbarManager
// 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 ignoreSwitchingByNextPrevious: Int = 0
}
/**
UIView category for managing textInputView
*/
@available(iOSApplicationExtension, unavailable)
@MainActor
public extension IQKeyboardExtension where Base: IQTextInputView {
/**
If ignoreSwitchingByNextPrevious is true then library will ignore this textInputView
while moving to other textInputView using keyboard toolbar next previous buttons.
Default is false
*/
var ignoreSwitchingByNextPrevious: Bool {
get {
if let base = base {
return objc_getAssociatedObject(base, &AssociatedKeys.ignoreSwitchingByNextPrevious) as? Bool ?? false
}
return false
}
set(newValue) {
if let base = base {
objc_setAssociatedObject(base, &AssociatedKeys.ignoreSwitchingByNextPrevious,
newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
}
}
// swiftlint:disable unused_setter_value
@available(iOSApplicationExtension, unavailable)
@MainActor
@objc public extension UIView {
@available(*, unavailable, renamed: "iq.ignoreSwitchingByNextPrevious")
var ignoreSwitchingByNextPrevious: Bool {
get { false }
set { }
}
}
// swiftlint:enable unused_setter_value
@available(iOSApplicationExtension, unavailable)
@MainActor
internal extension IQKeyboardExtension where Base: IQTextInputView {
/**
Returns all siblings of the receiver which canBecomeFirstResponder.
*/
func responderSiblings() -> [any IQTextInputView] {
// Getting all siblings
guard let siblings: [UIView] = base?.superview?.subviews else { return [] }
// Array of textInputView.
var textInputViews: [any IQTextInputView] = []
for view in siblings {
if let view = view as? any IQTextInputView,
view == base || !view.internalIgnoreSwitchingByNextPrevious,
view.internalCanBecomeFirstResponder() {
textInputViews.append(view)
}
}
return textInputViews
}
}
/**
UIView category for managing textInputView
*/
@available(iOSApplicationExtension, unavailable)
@MainActor
internal extension IQKeyboardExtension where Base: UIView {
/**
Returns all deep subViews of the receiver which canBecomeFirstResponder.
*/
func deepResponderViews() -> [any IQTextInputView] {
guard let subviews: [UIView] = base?.subviews, !subviews.isEmpty else { return [] }
// Array of textInputViews.
var textInputViews: [any IQTextInputView] = []
for view in subviews {
if let view = view as? any IQTextInputView,
view == base || !view.internalIgnoreSwitchingByNextPrevious,
view.internalCanBecomeFirstResponder() {
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 view.subviews.count != 0,
base?.isUserInteractionEnabled == true,
base?.isHidden == false, base?.alpha != 0.0 {
for deepView in view.iq.deepResponderViews() {
textInputViews.append(deepView)
}
}
}
// 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: base)
let frame2: CGRect = view2.convert(view2.bounds, to: base)
if frame1.minY != frame2.minY {
return frame1.minY < frame2.minY
} else {
return frame1.minX < frame2.minX
}
})
}
}
@available(iOSApplicationExtension, unavailable)
@MainActor
private extension IQKeyboardExtension where Base: IQTextInputView {
func canBecomeFirstResponder() -> Bool {
var canBecomeFirstResponder: Bool = base?.iqIsEnabled == true
if canBecomeFirstResponder {
canBecomeFirstResponder = base?.isUserInteractionEnabled == true &&
base?.isHidden == false &&
base?.alpha != 0.0 &&
!isAlertViewTextField() &&
textFieldSearchBar() == nil
}
return canBecomeFirstResponder
}
}
@available(iOSApplicationExtension, unavailable)
@MainActor
fileprivate extension IQTextInputView {
var internalIgnoreSwitchingByNextPrevious: Bool {
return iq.ignoreSwitchingByNextPrevious
}
func internalCanBecomeFirstResponder() -> Bool {
return iq.canBecomeFirstResponder()
}
}

View File

@ -0,0 +1,44 @@
//
// UIView+RespondersObjc.swift
// https://github.com/hackiftekhar/IQKeyboardToolbar
// 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 UITextField {
var iq_ignoreSwitchingByNextPrevious: Bool {
get { iq.ignoreSwitchingByNextPrevious }
set { iq.ignoreSwitchingByNextPrevious = newValue }
}
}
@objc public extension UITextView {
var iq_ignoreSwitchingByNextPrevious: Bool {
get { iq.ignoreSwitchingByNextPrevious }
set { iq.ignoreSwitchingByNextPrevious = newValue }
}
}
// swiftlint:enable identifier_name

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

134
Pods/IQKeyboardToolbarManager/README.md generated Normal file
View File

@ -0,0 +1,134 @@
# IQKeyboardToolbarManager
[![CI Status](https://img.shields.io/travis/hackiftekhar/IQKeyboardToolbarManager.svg?style=flat)](https://travis-ci.org/hackiftekhar/IQKeyboardToolbarManager)
[![Version](https://img.shields.io/cocoapods/v/IQKeyboardToolbarManager.svg?style=flat)](https://cocoapods.org/pods/IQKeyboardToolbarManager)
[![License](https://img.shields.io/cocoapods/l/IQKeyboardToolbarManager.svg?style=flat)](https://cocoapods.org/pods/IQKeyboardToolbarManager)
[![Platform](https://img.shields.io/cocoapods/p/IQKeyboardToolbarManager.svg?style=flat)](https://cocoapods.org/pods/IQKeyboardToolbarManager)
![Screenshot](https://raw.githubusercontent.com/hackiftekhar/IQKeyboardToolbarManager/master/Screenshot/IQKeyboardToolbarManagerScreenshot.png)
## Example
To run the example project, clone the repo, and run `pod install` from the Example directory first.
## Requirements
## Installation
### Swift Package Manager (Recommended)
IQKeyboardToolbarManager is available through [Swift Package Manager](https://swift.org/package-manager/).
**Requirements:** iOS 13.0+, Swift 5.7+
#### Using Xcode:
1. In Xcode, go to `File``Add Package Dependencies...`
2. Enter the repository URL: `https://github.com/hackiftekhar/IQKeyboardToolbarManager`
3. Select the version rule (e.g., "Up to Next Major Version")
4. Click `Add Package`
5. Select the `IQKeyboardToolbarManager` library and click `Add Package`
#### Using Package.swift:
Add the following dependency to your `Package.swift` file:
```swift
dependencies: [
.package(url: "https://github.com/hackiftekhar/IQKeyboardToolbarManager", from: "1.1.3")
]
```
Then add `IQKeyboardToolbarManager` to your target dependencies:
```swift
.target(
name: "YourTarget",
dependencies: ["IQKeyboardToolbarManager"]
)
```
### CocoaPods
IQKeyboardToolbarManager is also available through [CocoaPods](https://cocoapods.org). To install
it, simply add the following line to your Podfile:
```ruby
pod 'IQKeyboardToolbarManager'
```
### Carthage
Add the following line to your `Cartfile`:
```
github "hackiftekhar/IQKeyboardToolbarManager"
```
## Usage
After adding IQKeyboardToolbarManager to your project, import it and enable toolbar handling in AppDelegate:
```swift
import UIKit
import IQKeyboardToolbarManager
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
IQKeyboardToolbarManager.shared.isEnabled = true
return true
}
```
Cuistomize Behavior
```swift
IQKeyboardToolbarManager.shared.toolbarConfiguration.useTextInputViewTintColor = true
IQKeyboardToolbarManager.shared.toolbarConfiguration.tintColor = UIColor.systemGreen
IQKeyboardToolbarManager.shared.toolbarConfiguration.barTintColor = UIColor.systemYellow
IQKeyboardToolbarManager.shared.toolbarConfiguration.previousNextDisplayMode = .alwaysShow
IQKeyboardToolbarManager.shared.toolbarConfiguration.manageBehavior = .byPosition
IQKeyboardToolbarManager.shared.toolbarConfiguration.previousBarButtonConfiguration = ... // BarButton configuration to change title, image or system image etc
IQKeyboardToolbarManager.shared.toolbarConfiguration.nextBarButtonConfiguration = ... // BarButton configuration to change title, image or system image etc
IQKeyboardToolbarManager.shared.toolbarConfiguration.doneBarButtonConfiguration = ... // BarButton configuration to change title, image or system image etc
IQKeyboardToolbarManager.shared.toolbarConfiguration.placeholderConfiguration.showPlaceholder = false
IQKeyboardToolbarManager.shared.toolbarConfiguration.placeholderConfiguration.font = UIFont.italicSystemFont(ofSize: 14)
IQKeyboardToolbarManager.shared.toolbarConfiguration.placeholderConfiguration.color = UIColor.systemPurple
IQKeyboardToolbarManager.shared.toolbarConfiguration.placeholderConfiguration.buttonColor = UIColor.systemBrown // This is used only if placeholder is an action button
IQKeyboardToolbarManager.shared.playInputClicks = false
IQKeyboardToolbarManager.shared.disabledToolbarClasses.append(ChatViewController.self)
IQKeyboardToolbarManager.shared.enabledToolbarClasses.append(LoginViewController.self)
IQKeyboardToolbarManager.shared.deepResponderAllowedContainerClasses.append(UIStackView.self)
```
Useful functions and variables
```swift
if IQKeyboardToolbarManager.shared.canGoPrevious {
...
}
if IQKeyboardToolbarManager.shared.canGoNext {
...
}
IQKeyboardToolbarManager.shared.goPrevious()
IQKeyboardToolbarManager.shared.goNext()
IQKeyboardToolbarManager.shared.reloadInputViews() // If some textInputView hierarchy are changed on the fly then use this to reload button states
```
Useful functions and variables for TextInputView
```swift
textField.iq.ignoreSwitchingByNextPrevious = false
```
## Author
Iftekhar Qurashi hack.iftekhar@gmail.com
## License
IQKeyboardToolbarManager is available under the MIT license. See the LICENSE file for more info.

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,91 @@
//
// IQTextInputViewInfo.swift
// https://github.com/hackiftekhar/IQTextInputViewNotification
// 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
public import IQKeyboardCore
@available(iOSApplicationExtension, unavailable)
@MainActor
public struct IQTextInputViewInfo: Equatable {
nonisolated public static func == (lhs: Self, rhs: Self) -> Bool {
return lhs.textInputView == rhs.textInputView &&
lhs.event == rhs.event
}
@MainActor
@objc public enum Event: Int {
case beginEditing
case endEditing
public var name: String {
switch self {
case .beginEditing:
return "BeginEditing"
case .endEditing:
return "EndEditing"
}
}
}
public let event: Event
public let textInputView: any IQTextInputView
internal init?(notification: Notification, event: Event) {
guard let view: any IQTextInputView = notification.object as? (any IQTextInputView) else {
return nil
}
self.event = event
textInputView = view
}
}
// MARK: Deprecated
@available(iOSApplicationExtension, unavailable)
public extension IQTextInputViewInfo {
@available(*, unavailable, renamed: "event")
var name: Event { event }
@available(*, unavailable, renamed: "textInputView")
var textFieldView: any IQTextInputView { textInputView }
}
@available(*, unavailable, renamed: "IQTextInputViewInfo")
@MainActor
public struct IQTextFieldViewInfo: Equatable {}
@available(iOSApplicationExtension, unavailable)
@objcMembers public class IQTextInputViewInfoObjC: NSObject {
private let wrappedValue: IQTextInputViewInfo
public var event: IQTextInputViewInfo.Event { wrappedValue.event }
public var textInputView: any IQTextInputView { wrappedValue.textInputView }
init(wrappedValue: IQTextInputViewInfo){
self.wrappedValue = wrappedValue
}
}

View File

@ -0,0 +1,196 @@
//
// IQTextInputViewNotification.swift
// https://github.com/hackiftekhar/IQTextInputViewNotification
// 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
public import IQKeyboardCore
@available(iOSApplicationExtension, unavailable)
@MainActor
@objcMembers public final class IQTextInputViewNotification: NSObject {
private var storage: Set<AnyCancellable> = []
private var textInputViewObservers: [AnyHashable: TextInputViewCompletion] = [:]
private var findInteractionTextInputViewInfo: IQTextInputViewInfo?
public private(set) var textInputViewInfo: IQTextInputViewInfo?
public var event: IQTextInputViewInfo.Event? {
textInputViewInfo?.event
}
public var textInputView: (any IQTextInputView)? {
return textInputViewInfo?.textInputView
}
public override init() {
super.init()
// Registering for TextInputView notification.
do {
let beginEditingNotificationNames: [Notification.Name] = [
UITextField.textDidBeginEditingNotification,
UITextView.textDidBeginEditingNotification
]
for notificationName in beginEditingNotificationNames {
NotificationCenter.default.publisher(for: notificationName)
.compactMap({ IQTextInputViewInfo(notification: $0, event: .beginEditing) })
.sink(receiveValue: { [weak self] info in
guard let self = self else { return }
self.didBeginEditing(info: info)
})
.store(in: &storage)
}
}
do {
let endEditingNotificationNames: [Notification.Name] = [
UITextField.textDidEndEditingNotification,
UITextView.textDidEndEditingNotification
]
for notificationName in endEditingNotificationNames {
NotificationCenter.default.publisher(for: notificationName)
.compactMap({ IQTextInputViewInfo(notification: $0, event: .endEditing) })
.sink(receiveValue: { [weak self] info in
guard let self = self else { return }
self.didEndEditing(info: info)
})
.store(in: &storage)
}
}
}
private func didBeginEditing(info: IQTextInputViewInfo) {
#if compiler(>=5.7) // Xcode 14
if #available(iOS 16.0, *),
let findInteractionTextInputViewInfo = findInteractionTextInputViewInfo,
findInteractionTextInputViewInfo.textInputView.iqFindInteraction?.isFindNavigatorVisible == true {
// // This means the this didBeginEditing call comes due to find interaction
textInputViewInfo = findInteractionTextInputViewInfo
sendEvent(info: findInteractionTextInputViewInfo)
} else if textInputViewInfo != info {
textInputViewInfo = info
findInteractionTextInputViewInfo = nil
sendEvent(info: info)
} else {
findInteractionTextInputViewInfo = nil
}
#else
if textInputViewInfo != info {
textInputViewInfo = info
findInteractionTextInputViewInfo = nil
sendEvent(info: info)
} else {
findInteractionTextInputViewInfo = nil
}
#endif
}
private func didEndEditing(info: IQTextInputViewInfo) {
if textInputViewInfo != info {
#if compiler(>=5.7) // Xcode 14
if #available(iOS 16.0, *),
info.textInputView.iqIsFindInteractionEnabled {
findInteractionTextInputViewInfo = textInputViewInfo
} else {
findInteractionTextInputViewInfo = nil
}
#else
findInteractionTextInputViewInfo = nil
#endif
textInputViewInfo = info
sendEvent(info: info)
textInputViewInfo = nil
}
}
}
@available(iOSApplicationExtension, unavailable)
@MainActor
@objc public extension IQTextInputViewNotification {
typealias TextInputViewCompletion = (_ event: IQTextInputViewInfo.Event,
_ textInputView: any IQTextInputView) -> Void
func subscribe(identifier: AnyHashable, changeHandler: @escaping TextInputViewCompletion) {
textInputViewObservers[identifier] = changeHandler
if let textInputViewInfo = textInputViewInfo {
changeHandler(textInputViewInfo.event, textInputViewInfo.textInputView)
}
}
func unsubscribe(identifier: AnyHashable) {
textInputViewObservers[identifier] = nil
}
func isSubscribed(identifier: AnyHashable) -> Bool {
return textInputViewObservers[identifier] != nil
}
@nonobjc
private func sendEvent(info: IQTextInputViewInfo) {
for block in textInputViewObservers.values {
block(info.event, info.textInputView)
}
}
}
// MARK: Deprecated
@available(iOSApplicationExtension, unavailable)
@MainActor
public extension IQTextInputViewNotification {
@available(*, unavailable, renamed: "textInputViewInfo")
var textFieldViewInfo: IQTextInputViewInfo? { textInputViewInfo }
@available(*, unavailable, renamed: "textInputView")
var textFieldView: (some IQTextInputView)? { textInputView }
@available(*, unavailable, renamed: "subscribe(identifier:changeHandler:)")
func registerTextFieldViewChange(identifier: AnyHashable, changeHandler: @escaping TextInputViewCompletion) {}
@available(*, unavailable, renamed: "unsubscribe(identifier:)")
func unregisterSizeChange(identifier: AnyHashable) {}
}
@available(*, unavailable, renamed: "IQTextInputViewNotification")
@MainActor
@objcMembers public final class IQTextFieldViewListener: NSObject {}
@available(iOSApplicationExtension, unavailable)
@MainActor
@objc public extension IQTextInputViewNotification {
var textInputViewInfoObjc: IQTextInputViewInfoObjC? {
guard let textInputViewInfo = textInputViewInfo else { return nil }
return IQTextInputViewInfoObjC(wrappedValue: textInputViewInfo)
}
}

19
Pods/IQTextInputViewNotification/LICENSE generated Normal file
View File

@ -0,0 +1,19 @@
Copyright (c) 2024 hackiftekhar <ideviftekhar@gmail.com>
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.

View File

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

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,29 @@
//
// IQTextView.swift
// https://github.com/hackiftekhar/IQTextView
// 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 IQKeyboardToolbar
@available(iOSApplicationExtension, unavailable)
@MainActor
@objc extension IQTextView: IQPlaceholderable { }

View File

@ -0,0 +1,193 @@
//
// IQTextView.swift
// https://github.com/hackiftekhar/IQTextView
// 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
/** @abstract UITextView with placeholder support */
@available(iOSApplicationExtension, unavailable)
@MainActor
@objcMembers open class IQTextView: UITextView {
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
override public init(frame: CGRect, textContainer: NSTextContainer?) {
super.init(frame: frame, textContainer: textContainer)
setup()
}
override open func awakeFromNib() {
super.awakeFromNib()
setup()
}
private func setup() {
NotificationCenter.default.addObserver(self, selector: #selector(self.refreshPlaceholder),
name: UITextView.textDidChangeNotification, object: self)
do {
placeholderLabel.frame = placeholderExpectedFrame
placeholderLabel.autoresizingMask = [.flexibleWidth, .flexibleHeight]
placeholderLabel.lineBreakMode = .byWordWrapping
placeholderLabel.numberOfLines = 0
placeholderLabel.font = self.font
placeholderLabel.textAlignment = self.textAlignment
placeholderLabel.backgroundColor = UIColor.clear
placeholderLabel.isAccessibilityElement = false
placeholderLabel.textColor = UIColor.placeholderText
self.addSubview(placeholderLabel)
refreshPlaceholder()
}
}
private var placeholderInsets: UIEdgeInsets {
let top: CGFloat = self.textContainerInset.top
let left: CGFloat = self.textContainerInset.left + self.textContainer.lineFragmentPadding
let bottom: CGFloat = self.textContainerInset.bottom
let right: CGFloat = self.textContainerInset.right + self.textContainer.lineFragmentPadding
return UIEdgeInsets(top: top, left: left, bottom: bottom, right: right)
}
private var placeholderExpectedFrame: CGRect {
let insets: UIEdgeInsets = self.placeholderInsets
let maxWidth: CGFloat = self.frame.width-insets.left-insets.right
let size: CGSize = CGSize(width: maxWidth, height: self.frame.height-insets.top-insets.bottom)
let expectedSize: CGSize = placeholderLabel.sizeThatFits(size)
return CGRect(x: insets.left, y: insets.top, width: maxWidth, height: expectedSize.height)
}
public let placeholderLabel: UILabel = .init()
/** @abstract To set textView's placeholder text color. */
@IBInspectable open var placeholderTextColor: UIColor? {
get {
return placeholderLabel.textColor
}
set {
placeholderLabel.textColor = newValue
}
}
/** @abstract To set textView's placeholder text. Default is nil. */
@IBInspectable open var placeholder: String? {
get {
return placeholderLabel.text
}
set {
placeholderLabel.text = newValue
refreshPlaceholder()
}
}
/** @abstract To set textView's placeholder attributed text. Default is nil. */
open var attributedPlaceholder: NSAttributedString? {
get {
return placeholderLabel.attributedText
}
set {
placeholderLabel.attributedText = newValue
refreshPlaceholder()
}
}
open override func layoutSubviews() {
super.layoutSubviews()
placeholderLabel.frame = placeholderExpectedFrame
}
@objc private func refreshPlaceholder() {
let text: String = text ?? attributedText?.string ?? ""
placeholderLabel.alpha = text.isEmpty ? 1 : 0
}
open override var text: String! {
didSet {
refreshPlaceholder()
}
}
open override var attributedText: NSAttributedString! {
didSet {
refreshPlaceholder()
}
}
open override var font: UIFont? {
didSet {
if let font: UIFont = font {
placeholderLabel.font = font
} else {
placeholderLabel.font = UIFont.systemFont(ofSize: 12)
}
}
}
open override var textAlignment: NSTextAlignment {
didSet {
placeholderLabel.textAlignment = textAlignment
}
}
open override var intrinsicContentSize: CGSize {
guard !hasText else {
return super.intrinsicContentSize
}
var newSize: CGSize = super.intrinsicContentSize
let placeholderInsets: UIEdgeInsets = self.placeholderInsets
newSize.height = placeholderExpectedFrame.height + placeholderInsets.top + placeholderInsets.bottom
return newSize
}
open override func caretRect(for position: UITextPosition) -> CGRect {
var originalRect = super.caretRect(for: position)
// When placeholder is visible and text alignment is centered
guard placeholderLabel.alpha == 1 && self.textAlignment == .center else { return originalRect }
// Calculate the width of the placeholder text
let font = placeholderLabel.font ?? UIFont.systemFont(ofSize: UIFont.systemFontSize)
let textSize = placeholderLabel.text?.size(withAttributes: [.font: font]) ?? .zero
// Calculate the starting x position of the centered placeholder text
let centeredTextX = (self.bounds.size.width - textSize.width) / 2
// Update the caret position to match the starting x position of the centered text
originalRect.origin.x = centeredTextX
return originalRect
}
}

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

48
Pods/IQTextView/README.md generated Normal file
View File

@ -0,0 +1,48 @@
# IQTextView
[![CI Status](https://img.shields.io/travis/hackiftekhar/IQTextView.svg?style=flat)](https://travis-ci.org/hackiftekhar/IQTextView)
[![Version](https://img.shields.io/cocoapods/v/IQTextView.svg?style=flat)](https://cocoapods.org/pods/IQTextView)
[![License](https://img.shields.io/cocoapods/l/IQTextView.svg?style=flat)](https://cocoapods.org/pods/IQTextView)
[![Platform](https://img.shields.io/cocoapods/p/IQTextView.svg?style=flat)](https://cocoapods.org/pods/IQTextView)
![Screenshot](https://raw.githubusercontent.com/hackiftekhar/IQTextView/master/Screenshot/IQTextViewScreenshot.png)
## Example
To run the example project, clone the repo, and run `pod install` from the Example directory first.
## Requirements
## Installation
IQTextView is available through [CocoaPods](https://cocoapods.org). To install
it, simply add the following line to your Podfile:
```ruby
pod 'IQTextView'
```
## Usage
You can set any UITextView class to IQTextView and then you can change the placeholder and placeholderTextColor from the storyboard
To change them via code, you may have to set them programmatically
```swift
textView.placeholderTextColor = UIColor.lightGray
textView.placeholder = "Enter your message here..."
```
To work this with IQKeyboardToolbarManager to show placeholder in toolbar, you may have to confirm IQPlaceholderable manually in your code
```swift
@available(iOSApplicationExtension, unavailable)
@MainActor
extension IQTextView: IQPlaceholderable { }
```
## Author
Iftekhar Qurashi hack.iftekhar@gmail.com
## License
IQTextView is available under the MIT license. See the LICENSE file for more info.

1
Pods/JCore/README.md generated Normal file
View File

@ -0,0 +1 @@
# jcore-sdk

View File

@ -0,0 +1,44 @@
<?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>AvailableLibraries</key>
<array>
<dict>
<key>HeadersPath</key>
<string>Headers</string>
<key>LibraryIdentifier</key>
<string>ios-arm64</string>
<key>LibraryPath</key>
<string>libJCore.a</string>
<key>SupportedArchitectures</key>
<array>
<string>arm64</string>
</array>
<key>SupportedPlatform</key>
<string>ios</string>
</dict>
<dict>
<key>HeadersPath</key>
<string>Headers</string>
<key>LibraryIdentifier</key>
<string>ios-arm64_x86_64-simulator</string>
<key>LibraryPath</key>
<string>libJCore.a</string>
<key>SupportedArchitectures</key>
<array>
<string>arm64</string>
<string>x86_64</string>
</array>
<key>SupportedPlatform</key>
<string>ios</string>
<key>SupportedPlatformVariant</key>
<string>simulator</string>
</dict>
</array>
<key>CFBundlePackageType</key>
<string>XFWK</string>
<key>XCFrameworkFormatVersion</key>
<string>1.0</string>
</dict>
</plist>

View File

@ -0,0 +1,19 @@
//
// JGCOREService.h
// JCore
//
// Created by Shuni Huang on 2024/3/19.
// Copyright © 2024 jiguang. All rights reserved.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface JGCOREAPI : NSObject
+ (void)ci:(BOOL)enable;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,27 @@
//
// JGInforCollectionAuth.h
// JCore
//
// Created by 豆瓣 on 2021/10/27.
// Copyright © 2021 jiguang. All rights reserved.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
/// 合规接口,是否授权极光采集一定的设备信息
@interface JGInforCollectionAuthItems : NSObject
/// 是否授权默认YES
@property(nonatomic,assign)BOOL isAuth;
@end
@interface JGInforCollectionAuth : NSObject
/// 设备信息采集授权接口(合规接口)
/// 请务必在调用初始化、功能性接口前调用此接口进行合规授权
/// @param authBlock auth:YES 则极光认为您同意极光采集一定的设备信息
+(void)JCollectionAuth:(void(^_Nullable)(JGInforCollectionAuthItems *authInfo))authBlock;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,64 @@
<?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>NSPrivacyTrackingDomains</key>
<array/>
<key>NSPrivacyCollectedDataTypes</key>
<array>
<dict>
<key>NSPrivacyCollectedDataType</key>
<string>NSPrivacyCollectedDataTypeCrashData</string>
<key>NSPrivacyCollectedDataTypeLinked</key>
<false/>
<key>NSPrivacyCollectedDataTypeTracking</key>
<false/>
<key>NSPrivacyCollectedDataTypePurposes</key>
<array>
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
</array>
</dict>
<dict>
<key>NSPrivacyCollectedDataType</key>
<string>NSPrivacyCollectedDataTypePreciseLocation</string>
<key>NSPrivacyCollectedDataTypeLinked</key>
<false/>
<key>NSPrivacyCollectedDataTypeTracking</key>
<false/>
<key>NSPrivacyCollectedDataTypePurposes</key>
<array>
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
</array>
</dict>
</array>
<key>NSPrivacyTracking</key>
<false/>
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>E174.1</string>
</array>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryDiskSpace</string>
</dict>
<dict>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>CA92.1</string>
</array>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
</dict>
<dict>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>C617.1</string>
</array>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
</dict>
</array>
</dict>
</plist>

Some files were not shown because too many files have changed in this diff Show More