415 Commits

Author SHA1 Message Date
bdf2a9af80 修改人设长按排序后,键盘人设要同步问题 2026-01-07 19:05:52 +08:00
e858d35722 修改首页pop对钩问题 2026-01-07 15:45:01 +08:00
f2d5210313 1 2026-01-07 14:32:57 +08:00
1b0af3e2d6 修改颜色 2026-01-07 14:32:49 +08:00
0965cd3c7e 修改了横屏键盘不居中为题 2026-01-07 13:11:23 +08:00
c3909d63da 添加埋点 2026-01-06 19:25:34 +08:00
1096f24c57 修改hud 2025-12-31 17:32:39 +08:00
7ed84fd445 修改分享方式 2025-12-30 20:41:30 +08:00
4e2d7d2908 添加部分通用上报
修改bug 未登录在键盘点击充值要先去跳转登录
2025-12-30 15:27:35 +08:00
34089ddeea 1 2025-12-26 15:51:27 +08:00
6ec98468de Merge branch 'dev_处理立刻清空和撤销删除' into dev_st
# Conflicts:
#	CustomKeyboard/Utils/KBBackspaceLongPressHandler.m
解决冲突
2025-12-26 15:08:14 +08:00
2d5919016f 测试上报 接口报错 2025-12-26 15:02:48 +08:00
c0fa51bb2e 修改用户名UI 2025-12-26 14:48:24 +08:00
6713f36387 修改国际化 2025-12-26 14:38:29 +08:00
f24750458a 修改立刻清空按钮 2025-12-26 14:19:39 +08:00
510a2f4d66 添加邀请链接 2025-12-26 14:07:21 +08:00
ae37730da6 修改 我已经退出界面,然后从新进入界面弹起键盘,为什么撤销删除按钮显示? 2025-12-26 13:55:07 +08:00
203f104ece 修改键盘长按立即清空和撤销删除 2025-12-26 11:47:44 +08:00
8e934dd83a 1 2025-12-25 17:58:33 +08:00
1676916a5c 1 2025-12-25 17:32:34 +08:00
1af5a0e849 添加部分埋点 2025-12-25 17:20:24 +08:00
5b6e0a8fbf 修改key 2025-12-25 13:07:28 +08:00
9968883bab 修改邀请 2025-12-25 13:01:14 +08:00
af5f637d31 1 2025-12-24 20:17:37 +08:00
0a725e845e 处理详情tag的背景色 2025-12-23 20:56:00 +08:00
6a539dc3c5 处理键盘长按删除 撤销出现的bug 2025-12-23 18:05:01 +08:00
73d6ec933a 修改金币超出问题 2025-12-23 15:33:14 +08:00
000d603241 优化下载皮肤❤️ 2025-12-23 15:26:32 +08:00
fbf9fe9f2a 2 2025-12-23 14:37:11 +08:00
8e4d7e1ee8 处理按钮 2025-12-23 14:15:27 +08:00
262eb57b36 修改接口 2025-12-23 14:07:53 +08:00
2e1c261775 修改UI 2025-12-23 14:02:44 +08:00
6ad2079351 修改emoji 更换api 2025-12-23 13:27:25 +08:00
a477592f5d 新增按钮 2025-12-22 21:16:38 +08:00
6f336e8368 国际化 2025-12-22 20:53:24 +08:00
17e038beb1 修改接口 2025-12-22 19:22:12 +08:00
4e6fd90668 1 2025-12-22 19:19:28 +08:00
5cfc76e6c5 修改我的皮肤逻辑 2025-12-22 16:42:56 +08:00
9e33c93763 修改弹窗 2025-12-22 15:49:28 +08:00
1c9ae7bc06 1 2025-12-22 15:37:22 +08:00
472e9ad341 修改联想背景色 2025-12-22 14:02:43 +08:00
19c69f4f6f Merge branch 'dev_联想' into dev_st 2025-12-22 13:47:23 +08:00
8788cbb105 处理UI 2025-12-22 13:46:45 +08:00
ea77e9a5f8 处理苹果bug 默认键盘颜色改为 2025-12-22 13:29:00 +08:00
eaaf0e1ed6 修改UI 2025-12-22 13:08:59 +08:00
8a344b293d 添加联想 2025-12-22 12:54:28 +08:00
9cdd024ce2 修改UI 2025-12-19 22:00:52 +08:00
d612346db5 删除立即充值按钮 2025-12-19 21:36:11 +08:00
2cacaab974 更换AI皮肤逻辑图片 + 更换placeholdimage 2025-12-19 21:29:11 +08:00
200b1ab9f8 修复键盘功能键 发送按钮距离底部很远 2025-12-19 20:40:53 +08:00
8d0939cd78 1 2025-12-19 20:25:22 +08:00
df51620ca9 Emoji 默认选中第一个笑脸 2025-12-19 20:13:57 +08:00
70520fb7d9 AI键移除 放在顶部KBToolBar 2025-12-19 20:08:13 +08:00
7587fe6714 KBFunctionView 长按删除不弹出立刻清空按钮 2025-12-19 19:56:39 +08:00
1c8834caf6 键盘添加撤销操作 2025-12-19 19:21:08 +08:00
68306aa07f 1 2025-12-19 18:58:29 +08:00
9544ad75ff 重构键盘长按删除 2025-12-19 18:45:14 +08:00
d90e080981 修改立刻清空出现延迟时间 2025-12-19 18:14:28 +08:00
fc65052583 1 2025-12-19 18:11:09 +08:00
e0d5ae0257 新增键盘删除长按删除 2025-12-19 18:08:51 +08:00
639ce7eafd 1 2025-12-19 17:06:01 +08:00
e0379d3717 添加长按多文字删除 2025-12-19 16:24:47 +08:00
c108077178 刷新详情头部 2025-12-19 15:40:39 +08:00
182e5b9da1 未登录处理 2025-12-19 13:34:58 +08:00
ea4ecc05b4 添加右滑返回 2025-12-18 14:57:28 +08:00
ae05127292 1 2025-12-18 13:46:14 +08:00
38a3d2879e 优化未登录 键盘点击充值去登录 2025-12-18 13:20:49 +08:00
10ba4cd80f 添加反馈接口 2025-12-17 20:52:23 +08:00
857822c49c 2 2025-12-17 20:43:16 +08:00
85012eab78 2 2025-12-17 20:37:53 +08:00
8aa43d723a 1 2025-12-17 20:33:14 +08:00
1ecb7d60e5 新增搜索 2025-12-17 20:30:01 +08:00
904a6c932a 1 2025-12-17 19:56:22 +08:00
886de394d0 1 2025-12-17 19:45:39 +08:00
8bad475288 1 2025-12-17 19:08:44 +08:00
4a26419e67 1 2025-12-17 18:22:37 +08:00
1e04e7c39a 2 2025-12-17 17:01:49 +08:00
dde8716262 添加键盘里的充值界面 2025-12-17 16:22:41 +08:00
c6b4444589 处理网络请求token为空的问题 2025-12-17 15:39:45 +08:00
5bd20a911f 修改UI 2025-12-16 21:43:00 +08:00
b43567748c 1 2025-12-16 20:20:57 +08:00
59297eac77 重构 2025-12-16 20:05:33 +08:00
9f7d805a52 修改时间 2025-12-16 19:04:21 +08:00
6800864866 新增恢复购买 2025-12-16 16:25:50 +08:00
e8a980ff5b 2 2025-12-16 16:09:14 +08:00
dfbd5efe69 2 2025-12-16 16:03:14 +08:00
4a474e9b44 2 2025-12-16 16:00:00 +08:00
30f2e4f24f 1 2025-12-16 15:47:12 +08:00
c898d16688 删除storkit 1 2025-12-16 14:29:47 +08:00
f10ddd9a31 1 2025-12-16 14:14:49 +08:00
1651258eec 处理storkit2 2025-12-16 13:49:08 +08:00
fd0ddfd45a 1 2025-12-16 13:10:50 +08:00
444877fb73 2 2025-12-15 21:02:48 +08:00
1436464eca 1 2025-12-15 18:56:29 +08:00
05b9a0b823 添加删除按钮 2025-12-15 18:32:54 +08:00
053001170a 1 2025-12-15 18:02:58 +08:00
9cafb0f70e 引导UI修改 2025-12-15 17:56:06 +08:00
a399af53b5 1 2025-12-15 16:52:16 +08:00
a7574cd286 1 2025-12-15 16:41:53 +08:00
06dee39566 1 2025-12-15 15:47:16 +08:00
d5b4ef2b59 2 2025-12-15 15:40:25 +08:00
2620bd6845 1 2025-12-15 15:22:27 +08:00
1f0bdb1bd4 2 2025-12-15 15:11:16 +08:00
b21a2a8193 1 2025-12-15 15:03:35 +08:00
1f07120289 处理点击一个label,再次点击一个label崩溃了 2025-12-15 14:21:42 +08:00
64e0218add 添加emoji的map图标对应key 2025-12-15 13:45:14 +08:00
7972188ac3 修改emoji 2025-12-15 13:32:47 +08:00
6963c8016f 添加emoji1 2025-12-15 13:24:43 +08:00
633e6a9123 1 2025-12-12 20:08:34 +08:00
437f796a08 1 2025-12-12 16:09:14 +08:00
2b08dd44ee 2 2025-12-12 14:54:45 +08:00
1eeeef266b 1 2025-12-12 14:46:38 +08:00
3813974eae 1 2025-12-12 14:16:48 +08:00
6a5bda44e6 1 2025-12-11 22:02:18 +08:00
704553cd4e 1 2025-12-11 21:00:43 +08:00
35597f89ca 2 2025-12-11 20:40:49 +08:00
577b749198 2 2025-12-11 19:43:55 +08:00
cccced6afa 1 2025-12-11 18:36:14 +08:00
14637a21ad 1 2025-12-11 17:59:25 +08:00
111fe42782 1 2025-12-11 17:51:00 +08:00
58da905ade 1 2025-12-11 17:36:16 +08:00
f338a54e41 3 2025-12-11 16:59:14 +08:00
526ac1a7df 2 2025-12-11 16:45:39 +08:00
7f90240731 1 2025-12-11 16:39:22 +08:00
e4442afe72 1 2025-12-11 16:18:00 +08:00
be1d1ad70d 1 2025-12-11 15:39:33 +08:00
4fd0a52a36 2 2025-12-11 15:19:23 +08:00
04c7d19c37 1 2025-12-11 15:00:58 +08:00
94269209e0 2 2025-12-11 14:03:13 +08:00
2b4123741a 1 2025-12-11 13:51:04 +08:00
45695364e9 1 2025-12-11 13:40:32 +08:00
d348b35984 1 2025-12-11 13:16:06 +08:00
e39104c431 处理流逝返回
处理粘贴
2025-12-09 16:12:54 +08:00
7b86b739eb 处理searchresult 2025-12-09 15:19:10 +08:00
ade23e7a20 1 2025-12-09 14:32:21 +08:00
1b2b0c1143 1 2025-12-09 13:59:32 +08:00
0400d2020b 1 2025-12-08 20:46:24 +08:00
2cc93e0b48 1 2025-12-08 19:48:13 +08:00
fd8c08316b 1 2025-12-08 16:39:47 +08:00
0a1c30f669 处理AI回复问题 2025-12-05 21:54:10 +08:00
ca3ea0630e 2 2025-12-05 21:23:54 +08:00
a26b6b58a9 1 2025-12-05 21:15:48 +08:00
6fd4a86a7e 扩展添加token 2025-12-05 20:22:53 +08:00
fa999f502f 1 2025-12-05 20:18:18 +08:00
ad18a47d21 一次性配置内购服务 2025-12-05 13:49:12 +08:00
d2258883df 1 2025-12-04 21:53:47 +08:00
f7d11c5f8b 1 2025-12-04 21:10:32 +08:00
40d9b5aad4 1 2025-12-04 20:57:39 +08:00
6ac6514f89 1 2025-12-04 20:34:23 +08:00
eb7ad1a9f1 1 2025-12-04 20:27:26 +08:00
17ce91d40a 1 2025-12-04 20:04:02 +08:00
515487e748 1 2025-12-04 19:44:54 +08:00
7a25a6a5fa 1 2025-12-04 19:24:42 +08:00
64887054e0 1 2025-12-04 19:12:34 +08:00
8f63741d8c 1 2025-12-04 16:59:59 +08:00
f593ef0b4a 1 2025-12-04 16:41:33 +08:00
231f7f8c13 1 2025-12-04 16:18:43 +08:00
c9863cd353 11 2025-12-04 15:44:43 +08:00
ce4c4f0531 1 2025-12-04 15:35:23 +08:00
af2bcc42fd 1 2025-12-04 15:28:11 +08:00
1596aac717 2 2025-12-04 15:00:15 +08:00
d13bb734b1 1 2025-12-04 14:53:25 +08:00
2665b5ad1f 2 2025-12-04 14:44:56 +08:00
cffd77eeb5 1 2025-12-04 14:26:22 +08:00
b8f8d2e6b0 1 2025-12-04 14:17:47 +08:00
279255a14c 1 2025-12-04 14:07:12 +08:00
b216ddaa61 1 2025-12-04 13:37:11 +08:00
f770f8055e 1 2025-12-03 20:41:24 +08:00
819d74cc8d 2 2025-12-03 20:31:33 +08:00
9651ae7ad7 1 2025-12-03 20:14:14 +08:00
eca168957d 1 2025-12-03 20:02:37 +08:00
f026b9f9fd 1 2025-12-03 19:50:23 +08:00
b368ba0159 2 2025-12-03 19:43:45 +08:00
43e8b85656 1 2025-12-03 19:38:55 +08:00
716f91bdd0 3 2025-12-03 19:19:20 +08:00
b00283cd96 2 2025-12-03 19:05:53 +08:00
25edf2d817 1 2025-12-03 19:00:55 +08:00
1d6371c37e 1 2025-12-03 18:53:15 +08:00
82123fc232 1 2025-12-03 18:49:18 +08:00
6f02bc7cf5 2 2025-12-03 18:19:51 +08:00
a5ff2ce51b 1 2025-12-03 18:02:20 +08:00
4deccb76dc 1 2025-12-03 17:46:28 +08:00
93556ddb9c 6 2025-12-03 16:54:23 +08:00
681fced59d 2 2025-12-03 16:51:46 +08:00
91e2b047eb 5 2025-12-03 16:48:25 +08:00
c1c4c85bd2 2 2025-12-03 16:45:58 +08:00
d06e0499bc 1 2025-12-03 16:38:20 +08:00
49f730b609 3 2025-12-03 16:26:49 +08:00
04a392e7c7 4 2025-12-03 16:05:00 +08:00
22e393e588 2 2025-12-03 15:53:26 +08:00
6556689c8f 1 2025-12-03 15:46:26 +08:00
a50d18b486 1 2025-12-03 15:19:03 +08:00
599a5de3bc 5 2025-12-03 14:30:02 +08:00
c1eb6a3458 4 2025-12-03 13:54:57 +08:00
b87998549c 3 2025-12-03 13:31:02 +08:00
27aa723e7d 1 2025-12-03 12:55:51 +08:00
6be90ebb10 1 2025-12-02 21:32:49 +08:00
2f55e7bfa1 3 2025-12-02 20:33:17 +08:00
c56655c728 1 2025-12-02 19:39:37 +08:00
5e4c16c577 2 2025-12-02 19:19:25 +08:00
8245e7b3d1 1 2025-12-02 18:29:04 +08:00
cafde48f4a 修改UI逻辑 2025-11-28 16:55:26 +08:00
c897111855 1 2025-11-28 16:19:06 +08:00
a2bb61408b 2 2025-11-28 16:06:34 +08:00
9268a21eb8 处理bug 2025-11-28 14:25:13 +08:00
d4c553f072 处理视频退到后台进前台没有继续播放 2025-11-28 13:56:29 +08:00
73802b6e80 1 2025-11-28 13:20:50 +08:00
1a0a444a99 1 2025-11-28 13:07:35 +08:00
c37038f163 2 2025-11-27 21:39:03 +08:00
3144315de5 1 2025-11-27 20:05:39 +08:00
8f16250cbe 1 2025-11-27 19:20:20 +08:00
95d5e2b972 更改显示 2025-11-27 15:36:56 +08:00
2760a070a3 添加guard 蒙层 2025-11-27 15:34:33 +08:00
2435d760e8 添加键盘功能viewUI改动 2025-11-26 21:16:56 +08:00
80e4db86e4 1 2025-11-26 19:46:23 +08:00
4ab8a61a3c 处理因为网络下载失败导致之前的皮肤不在的bug 2025-11-25 22:00:13 +08:00
73c83153f9 缺少键盘图
修改网络环境下载图
2025-11-25 21:50:07 +08:00
1b67998f6a 2 2025-11-25 20:35:08 +08:00
b8cc38aa61 3 2025-11-25 18:54:53 +08:00
c4398a689b 2 2025-11-25 16:53:38 +08:00
b660eb19f4 1 2025-11-25 16:10:08 +08:00
1eb73f5257 封装KBFont,适配字体 2025-11-25 15:36:16 +08:00
71423df1c0 添加苹果登录的测试 2025-11-24 20:47:36 +08:00
709e0f4453 添加local文本 2025-11-24 20:31:23 +08:00
18df76a2b4 新增页面 2025-11-24 20:15:41 +08:00
15e37841bb 处理ios18从其他app用自己键盘 拉起主app的bug
其他问题
2025-11-24 19:58:19 +08:00
8e93f8f86f 动态化高度比例 2025-11-21 21:50:40 +08:00
fd35c5c993 1 2025-11-21 20:59:39 +08:00
fd7b3a7f75 优化键盘弹出宽度
优化键盘按钮视觉效果
2025-11-21 19:40:57 +08:00
af8fff5b13 添加无网络去设置,优化多语言 2025-11-21 18:36:00 +08:00
fc87c545a0 封装跨应用拉起, 2025-11-21 18:26:02 +08:00
0f4ca89060 固定键盘高度250
优化kbkeyboardview
优化ui
2025-11-21 16:22:00 +08:00
c371c7224e 优化键盘 预览宽度固定 2025-11-21 13:48:22 +08:00
31bb72c8f4 按钮之间无间距,按钮里的图片设置间距 2025-11-20 21:53:46 +08:00
faa05e2a10 添加按钮文字预览提示 2025-11-20 21:11:27 +08:00
6bdd111a3a 添加按下去动画 2025-11-20 20:36:31 +08:00
8296ac12b6 修改键盘UI 2025-11-20 20:17:19 +08:00
b2994adc1c UI 2025-11-20 19:57:11 +08:00
75d2e4072a 1 2025-11-20 18:23:56 +08:00
b23927968f 2 2025-11-20 15:35:22 +08:00
c27fd099f6 2 2025-11-20 15:10:22 +08:00
bc1264e28f 2 2025-11-20 14:56:15 +08:00
799b0f3989 添加扩展键盘本地皮肤 2025-11-20 14:27:57 +08:00
b3ce856ad4 添加启动图 2025-11-19 21:58:54 +08:00
f51fe1fac9 恢复默认皮肤 2025-11-19 20:30:30 +08:00
8dbaa9dcf6 配置化json strings 2025-11-19 20:16:19 +08:00
0196128008 1 2025-11-19 19:15:28 +08:00
4108aed4e0 处理键盘图片和自定义文字同时存在的bug 2025-11-19 16:13:30 +08:00
cc55bb107a 添加皮肤 2025-11-19 15:39:47 +08:00
7518a29d2f 2 2025-11-19 15:07:24 +08:00
37e131eb09 1 2025-11-19 14:54:45 +08:00
3dcc4932c3 3 2025-11-18 20:53:47 +08:00
254e65906a 添加app groups 2025-11-18 14:41:35 +08:00
ced0b88ca4 1 2025-11-18 13:48:22 +08:00
b2021dcb3c 6 2025-11-17 21:35:25 +08:00
0ef7b7d1d8 2 2025-11-17 21:08:25 +08:00
7254e2dbd9 2 2025-11-17 20:55:11 +08:00
26ef29ac4e 1 2025-11-17 20:26:39 +08:00
005e3c7581 2 2025-11-17 20:07:39 +08:00
ee433db4ad 66 2025-11-17 18:51:06 +08:00
ea4b8168b7 1 2025-11-17 16:42:32 +08:00
f366a4aa6c 1 2025-11-17 16:16:38 +08:00
d849b201ca 3 2025-11-17 15:39:03 +08:00
dc813fcabc 2 2025-11-17 15:06:05 +08:00
1d215ffdb3 1 2025-11-17 14:53:23 +08:00
d9bfc30c88 1 2025-11-17 13:30:01 +08:00
9305acb69b 1 2025-11-15 14:27:41 +08:00
f9a8955384 1 2025-11-15 00:33:29 +08:00
1f9dbba39d 1 2025-11-14 23:09:04 +08:00
dace0a9309 1 2025-11-14 19:48:15 +08:00
4f2e80e482 1 2025-11-14 18:43:08 +08:00
b27b9f9ee1 1 2025-11-14 18:24:38 +08:00
66a1ddef66 1 2025-11-14 16:34:01 +08:00
eacac8425c 1 2025-11-14 14:07:04 +08:00
d164514fcf 添加pay 2025-11-13 21:22:10 +08:00
ae79d1b1ba 更新UI 2025-11-13 19:20:57 +08:00
50163d02a7 3 2025-11-13 19:07:59 +08:00
5ec950cc61 统一api 2025-11-13 18:03:26 +08:00
a61b5fa2fd 1 2025-11-13 16:23:46 +08:00
f406416698 1 2025-11-13 15:34:56 +08:00
debbe2777b 1 2025-11-13 14:11:44 +08:00
bc261661ae 2 2025-11-12 21:23:31 +08:00
0aead49816 删除无关代码
退出登录
2025-11-12 19:46:07 +08:00
66b7a9218e 处理\n 2025-11-12 17:55:59 +08:00
2f4205ad1a 3 2025-11-12 16:49:19 +08:00
fea22aecab 重构了KBFunctionView 2025-11-12 16:03:30 +08:00
62f3ddae4a 删除测试数据 2025-11-12 15:40:30 +08:00
c317afc0fe 2 2025-11-12 15:31:22 +08:00
1dbe04cdf9 2 2025-11-12 14:36:15 +08:00
afc44cb471 1 2025-11-12 14:18:56 +08:00
39d8b3d547 1 2025-11-12 13:43:48 +08:00
f387b95d0d 测试假数据 2025-11-11 21:48:26 +08:00
1d064c1f31 1 2025-11-11 20:24:13 +08:00
3440cc4773 1 2025-11-11 19:39:33 +08:00
20b13bcffa 1 2025-11-11 17:36:12 +08:00
105e2ddf9b 1 2025-11-11 16:46:05 +08:00
a1a38d821c 添加复制 2025-11-11 15:59:19 +08:00
83987db5ac fix ui 2025-11-11 15:55:52 +08:00
d10114572e name pop 2025-11-11 15:28:22 +08:00
e34288ae56 fix ui 2025-11-11 15:13:43 +08:00
17b8bf2bfd fix 2025-11-11 14:56:57 +08:00
57bd4ba109 添加弹窗 2025-11-11 14:38:38 +08:00
e4ba237a00 1 2025-11-11 14:02:36 +08:00
9059a24637 1 2025-11-10 21:33:00 +08:00
dc0c55c495 2 2025-11-10 20:40:11 +08:00
a007a77db9 fix color 2025-11-10 19:55:50 +08:00
3eb3a86376 2 2025-11-10 19:51:23 +08:00
1dc9560a1f 1 2025-11-10 19:22:31 +08:00
8069b08fab Merge branch 'dev_st'
# Conflicts:
#	keyBoard.xcodeproj/project.pbxproj
解决冲突
2025-11-10 16:12:38 +08:00
2c8142c0d2 增加webview页面 2025-11-10 16:09:47 +08:00
9f4110b24a 添加弹窗 2025-11-10 15:55:36 +08:00
1cdc17b710 1 2025-11-10 15:38:30 +08:00
97316c7989 添加底部view 2025-11-10 15:29:21 +08:00
fac5e7657c Merge branch 'dev_st' 2025-11-10 13:27:44 +08:00
50dcb78417 提交 2025-11-10 13:27:26 +08:00
998fa7aa67 修改项目配置,锦支持iPhone 2025-11-10 13:25:39 +08:00
5e1a1f540e 1 2025-11-09 21:41:35 +08:00
2415e97c97 fix 2025-11-09 21:05:03 +08:00
aa71cc3c4f 1 2025-11-09 20:54:30 +08:00
e5ddcc4308 添加LYEmptyView '~> 0.3.10' 2025-11-09 20:54:14 +08:00
883b222254 fix 2025-11-09 18:07:47 +08:00
dc9ee10023 1 2025-11-09 17:07:43 +08:00
2c4a4329ff 2 2025-11-09 16:05:42 +08:00
553238de0c 1 2025-11-09 15:59:18 +08:00
80b6102673 1 2025-11-09 14:58:44 +08:00
705b0f374e 1 2025-11-09 14:44:31 +08:00
5bdc7ddec0 1 2025-11-09 14:26:02 +08:00
5d2a3de2f4 fix 2025-11-09 13:56:13 +08:00
675a9f6d64 3 2025-11-08 22:25:57 +08:00
41b14ceea4 3 2025-11-08 21:44:41 +08:00
a729396401 3 2025-11-08 20:49:05 +08:00
3b0beb52da 2 2025-11-08 20:04:50 +08:00
faeb930fe3 1 2025-11-08 11:48:06 +08:00
9a39c29e88 2 2025-11-07 22:22:41 +08:00
b23c9a678b 处理所有UI 2025-11-07 21:57:42 +08:00
96cd32ed99 添加箭头 2025-11-07 21:37:31 +08:00
50dd53b0c0 1 2025-11-07 21:05:25 +08:00
48a12f0919 1 2025-11-07 20:58:14 +08:00
91d754b389 1 2025-11-07 19:55:11 +08:00
450798c8bd 2 2025-11-07 19:33:54 +08:00
c3acc11f6a 1 2025-11-07 19:32:02 +08:00
d592c9f12e 1 2025-11-07 16:58:33 +08:00
26e39ce416 1 2025-11-07 16:46:08 +08:00
32521208a0 2 2025-11-07 16:29:15 +08:00
6e969648c6 1 2025-11-07 15:03:45 +08:00
074596ebcb 添加homeheadView 2025-11-07 14:21:03 +08:00
f0542c11c8 修改三方库 JXCategoryIndicatorCell self.contentView.layer.cornerRadius = 4; 2025-11-07 00:12:28 +08:00
0fa3d10284 9 2025-11-06 21:38:58 +08:00
0d13192723 优化ui 2025-11-06 19:51:50 +08:00
a72aae84ef 处理tabbar
处理HomeRankContentVC去除弹性效果 - 适配HWPanModal下拉效果
2025-11-06 19:29:52 +08:00
6ba1339c0b 修改UI 2025-11-06 19:19:12 +08:00
a75afbe4c1 创建nav宏
去除tabbar透明
2025-11-06 16:57:28 +08:00
1f45564539 5 2025-11-06 16:05:28 +08:00
41aec6b89e 4 2025-11-06 15:16:03 +08:00
a1db745b6c 3 2025-11-06 14:59:00 +08:00
15fc9621cd 2 2025-11-06 14:02:22 +08:00
d7874829d9 1 2025-11-06 13:18:27 +08:00
abf32e8457 添加HWPanModal和FLAnimatedImage 2025-11-05 22:04:56 +08:00
efdcf60ed1 在非刘海添加地球 2025-11-05 20:11:10 +08:00
7a1b17d060 处理键盘不能拉起主app的问题 2025-11-05 18:10:56 +08:00
f43f94b94d 添加键盘背景 2025-11-04 21:01:46 +08:00
3e2dc4bcb6 1 2025-11-04 16:37:24 +08:00
6fb9e56720 修改项目方向设置 2025-11-04 13:55:45 +08:00
e2cff76d13 合并代码 2025-11-04 13:52:13 +08:00
6e57f1c853 Merge branch 'main' of https://git.hanxiaokj.cn/zw/keyboard 2025-11-04 13:51:42 +08:00
4fe18c77dc temp 2025-11-04 13:51:15 +08:00
cb5819e330 bugly模拟器可用配置
修改包名
2025-11-03 21:04:39 +08:00
6f20e6aeb1 111 2025-11-03 20:03:53 +08:00
5af2612ff7 fix 2025-11-03 20:02:11 +08:00
cac2f13b88 修改引导逻辑 2025-11-03 19:00:47 +08:00
edf88721da 调整逻辑 2025-11-03 18:45:06 +08:00
915b329805 1 2025-11-03 16:57:24 +08:00
1673a2f4be 添加多语言 2025-11-03 16:37:28 +08:00
e4cebeac85 处理再次进入弹起权限弹窗 2025-11-03 15:04:19 +08:00
c7021e382e fixUI 2025-11-03 13:25:41 +08:00
ffea9d2022 修改KBAuthSession主程序添加token extension没有拿到的情况 2025-10-31 16:50:15 +08:00
90c1e7ff6c 添加token管理 2025-10-31 16:06:54 +08:00
59d04bb33c extension添加提示 2025-10-31 15:08:30 +08:00
eb0d3aaa71 提取baseurl配置 2025-10-31 13:05:34 +08:00
10dfe9b1d6 处理通用链接宏控制 2025-10-30 20:53:44 +08:00
6993bfd682 3 2025-10-30 20:46:54 +08:00
247a87891e 2 2025-10-30 20:23:34 +08:00
9af91cc4bc 1 2025-10-30 18:31:12 +08:00
4f23118ec0 修改逻辑 2025-10-30 18:28:36 +08:00
482756f6f0 优化apple sign 2025-10-30 14:35:06 +08:00
85a3694e35 apple login 2025-10-30 14:29:11 +08:00
f58bf61500 1 2025-10-30 14:04:55 +08:00
783d088f22 处理UI 2025-10-30 13:57:34 +08:00
74476cd592 修改功能 2025-10-30 13:27:09 +08:00
9b43274e93 测试网络,修改UI 2025-10-30 13:10:33 +08:00
8ce1d95c8c 添加图片库、封装 2025-10-29 20:57:45 +08:00
e8c88a6148 修改hud 2025-10-29 19:13:35 +08:00
e218c1bf3d 添加color 2025-10-29 18:25:52 +08:00
c5326a3079 移动文件 2025-10-29 16:44:00 +08:00
9101ffaab0 添加指引 2025-10-29 16:26:57 +08:00
e594711fa3 添加轮询粘贴 2025-10-29 15:49:43 +08:00
7fd084e529 1 2025-10-29 15:02:37 +08:00
11b25241bf basetableview封装 2025-10-29 14:49:35 +08:00
8fcfce7376 添加DZNEmptyDataSet 2025-10-29 14:30:19 +08:00
23317c9fd4 添加基本页面 2025-10-29 14:28:57 +08:00
045d5eaff8 创建base 2025-10-29 14:17:26 +08:00
72b6dbb157 添加MBP 2025-10-29 13:25:46 +08:00
e78b56e2cb 修改UI 2025-10-29 12:59:22 +08:00
6c05026402 fix 2025-10-28 21:27:01 +08:00
13facba33a 4 2025-10-28 20:11:40 +08:00
935284388c 数字面板#+=/123”切换 2025-10-28 20:03:43 +08:00
4c7fd9049f 默认小写 2025-10-28 19:24:35 +08:00
0031b7a5f6 添加设置 2025-10-28 18:02:10 +08:00
02dd204744 处理网络请求 2025-10-28 16:11:35 +08:00
f28f7de49d pod 网络库 2025-10-28 15:59:09 +08:00
c2859f888a 添加网络库,修改问题 2025-10-28 15:34:19 +08:00
a2b51189aa 添加功能组件 2025-10-28 15:18:12 +08:00
2f2f20cfc2 2 2025-10-28 15:10:38 +08:00
377e88b6db 1 2025-10-28 14:30:03 +08:00
1deca2ae5b 1 2025-10-28 10:18:10 +08:00
1360 changed files with 365781 additions and 5195 deletions

View File

@@ -0,0 +1,17 @@
Dec 18 13:42:00 macbookpro com.apple.dt.xcodebuild[42901] <Error>: Unable to deliver request ({
"developer_dir" = "/Applications/Xcode.app/Contents/Developer";
request = "set_developer_dir";
}) because we are not connected to CoreSimulatorService.
Dec 18 13:42:00 macbookpro com.apple.dt.xcodebuild[42901] <Warning>: Unable to discover any Simulator runtimes. Developer Directory is /Applications/Xcode.app/Contents/Developer.
Dec 18 13:42:00 macbookpro com.apple.dt.xcodebuild[42901] <Error>: Could not kickstart simdiskimaged; SimDiskImageManager services will not be available: Error Domain=NSPOSIXErrorDomain Code=53 "Software caused connection abort" UserInfo={NSLocalizedDescription=Error returned in reply from CoreSimulatorService: Connection invalid}
Dec 18 13:42:00 macbookpro com.apple.dt.xcodebuild[42901] <Error>: simdiskimaged returned error (invalid), marking disconnected.
Dec 18 13:42:00 macbookpro com.apple.dt.xcodebuild[42901] <Error>: simdiskimaged returned error (invalid), marking disconnected.
Dec 18 13:42:00 macbookpro com.apple.dt.xcodebuild[42901] <Error>: Could not get list of trusted mount directories: Error Domain=com.apple.CoreSimulator.SimError Code=410 "The service used to manage runtime disk images (simdiskimaged) crashed or is not responding" UserInfo={NSLocalizedDescription=The service used to manage runtime disk images (simdiskimaged) crashed or is not responding}
Dec 18 13:42:00 macbookpro com.apple.dt.xcodebuild[42901] <Error>: Unable to deliver request ({
request = "notification_subscription";
"set_path" = "/Users/mac/Library/Developer/CoreSimulator/Devices";
}) because we are not connected to CoreSimulatorService.
Dec 18 13:42:00 macbookpro com.apple.dt.xcodebuild[42901] <Error>: Unable to deliver request ({
request = "notification_subscription";
"set_path" = "/Users/mac/Library/Developer/CoreSimulator/Devices";
}) because we are not connected to CoreSimulatorService.

View File

@@ -0,0 +1,18 @@
<?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>com.apple.developer.associated-domains</key>
<array>
<string>applinks:app.tknb.net</string>
</array>
<key>com.apple.security.application-groups</key>
<array>
<string>group.com.loveKey.nyx</string>
</array>
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)com.loveKey.nyx.shared</string>
</array>
</dict>
</plist>

View File

@@ -2,6 +2,10 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>kbkeyboardAppExtension</string>
</array>
<key>NSExtension</key> <key>NSExtension</key>
<dict> <dict>
<key>NSExtensionAttributes</key> <key>NSExtensionAttributes</key>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "App_icon.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "App_icon@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "App_icon@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "切图 270@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "切图 270@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "back_keybord_icon@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "back_keybord_icon@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "buy_sel_icon@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "buy_sel_icon@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "home_ai_icon@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "home_ai_icon@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "home_chat_icon@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "home_chat_icon@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "home_emotion_icon@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "home_emotion_icon@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "home_keyboard_icon@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "home_keyboard_icon@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "kb_del_icon@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "kb_del_icon@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "kb_zt_icon@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "kb_zt_icon@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1014 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "key_revoke@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "key_revoke@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

View File

@@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "keybord_bg_icon@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "keybord_bg_icon@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 486 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

View File

@@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "upgrad_vip_icon@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "upgrad_vip_icon@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

View File

@@ -6,74 +6,825 @@
// //
#import "KeyboardViewController.h" #import "KeyboardViewController.h"
#import "KBKeyBoardMainView.h"
static CGFloat KEYBOARDHEIGHT = 256; #import "KBKey.h"
#import "KBFunctionView.h"
#import "KBSettingView.h"
#import "Masonry.h"
#import "KBAuthManager.h"
#import "KBFullAccessManager.h"
#import "KBSkinManager.h"
#import "KBSkinInstallBridge.h"
#import "KBHostAppLauncher.h"
#import "KBKeyboardSubscriptionView.h"
#import "KBKeyboardSubscriptionProduct.h"
#import "KBBackspaceUndoManager.h"
#import "KBInputBufferManager.h"
#import "KBSuggestionEngine.h"
@interface KeyboardViewController () // 使 static kb_consumePendingShopSkin
@property (nonatomic, strong) UIButton *nextKeyboardButton; @interface KeyboardViewController (KBSkinShopBridge)
- (void)kb_consumePendingShopSkin;
@end
// 375 稿
static const CGFloat kKBKeyboardBaseHeight = 250.0f;
static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
void *observer,
CFStringRef name,
const void *object,
CFDictionaryRef userInfo) {
KeyboardViewController *strongSelf = (__bridge KeyboardViewController *)observer;
if (!strongSelf) { return; }
dispatch_async(dispatch_get_main_queue(), ^{
if ([strongSelf respondsToSelector:@selector(kb_consumePendingShopSkin)]) {
[strongSelf kb_consumePendingShopSkin];
}
});
}
@interface KeyboardViewController () <KBKeyBoardMainViewDelegate, KBFunctionViewDelegate, KBKeyboardSubscriptionViewDelegate>
@property (nonatomic, strong) UIButton *nextKeyboardButton; //
@property (nonatomic, strong) UIView *contentView;
@property (nonatomic, strong) KBKeyBoardMainView *keyBoardMainView; // 0
@property (nonatomic, strong) KBFunctionView *functionView; // 0
@property (nonatomic, strong) KBSettingView *settingView; //
@property (nonatomic, strong) UIImageView *bgImageView; //
@property (nonatomic, strong) KBKeyboardSubscriptionView *subscriptionView;
@property (nonatomic, strong) KBSuggestionEngine *suggestionEngine;
@property (nonatomic, copy) NSString *currentWord;
@property (nonatomic, assign) BOOL suppressSuggestions;
@property (nonatomic, strong) MASConstraint *contentWidthConstraint;
@property (nonatomic, strong) MASConstraint *contentHeightConstraint;
@property (nonatomic, strong) NSLayoutConstraint *kb_heightConstraint;
@property (nonatomic, strong) NSLayoutConstraint *kb_widthConstraint;
@property (nonatomic, assign) CGFloat kb_lastPortraitWidth;
@property (nonatomic, assign) CGFloat kb_lastKeyboardHeight;
@end @end
@implementation KeyboardViewController @implementation KeyboardViewController
- (void)updateViewConstraints { {
[super updateViewConstraints]; BOOL _kb_didTriggerLoginDeepLinkOnce;
// Add custom view sizing constraints here
} }
- (void)viewDidLoad { - (void)viewDidLoad {
[super viewDidLoad]; [super viewDidLoad];
// // /
// // Perform custom UI setup here [[KBBackspaceUndoManager shared] registerNonClearAction];
// self.nextKeyboardButton = [UIButton buttonWithType:UIButtonTypeSystem];
//
// [self.nextKeyboardButton setTitle:NSLocalizedString(@"Next Keyboard", @"Title for 'Next Keyboard' button") forState:UIControlStateNormal];
// [self.nextKeyboardButton sizeToFit];
// self.nextKeyboardButton.translatesAutoresizingMaskIntoConstraints = NO;
//
// [self.nextKeyboardButton addTarget:self action:@selector(handleInputModeListFromView:withEvent:) forControlEvents:UIControlEventAllTouchEvents];
//
// [self.view addSubview:self.nextKeyboardButton];
//
// [self.nextKeyboardButton.leftAnchor constraintEqualToAnchor:self.view.leftAnchor].active = YES;
// [self.nextKeyboardButton.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor].active = YES;
[self setupUI]; [self setupUI];
self.suggestionEngine = [KBSuggestionEngine shared];
self.currentWord = @"";
// HUD App KeyWindow
[KBHUD setContainerView:self.view];
// 访便
[[KBFullAccessManager shared] bindInputController:self];
__unused id token = [[NSNotificationCenter defaultCenter] addObserverForName:KBFullAccessChangedNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(__unused NSNotification * _Nonnull note) {
// 访 UI
}];
//
__unused id token2 = [[NSNotificationCenter defaultCenter] addObserverForName:KBSkinDidChangeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(__unused NSNotification * _Nonnull note) {
[self kb_applyTheme];
}];
[self kb_applyTheme];
CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(),
(__bridge const void *)(self),
KBSkinInstallNotificationCallback,
(__bridge CFStringRef)KBDarwinSkinInstallRequestNotification,
NULL,
CFNotificationSuspensionBehaviorDeliverImmediately);
[self kb_consumePendingShopSkin];
}
- (void)viewWillAppear:(BOOL)animated{
[super viewWillAppear:animated];
// /
[[KBBackspaceUndoManager shared] registerNonClearAction];
[[KBInputBufferManager shared] resetWithText:@""];
[[KBLocalizationManager shared] reloadFromSharedStorageIfNeeded];
// /QQ 宿 documentContext liveText manualSnapshot
[[KBInputBufferManager shared] updateFromExternalContextBefore:self.textDocumentProxy.documentContextBeforeInput
after:self.textDocumentProxy.documentContextAfterInput];
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[[KBBackspaceUndoManager shared] registerNonClearAction];
}
- (void)textDidChange:(id<UITextInput>)textInput {
[super textDidChange:textInput];
[[KBInputBufferManager shared] updateFromExternalContextBefore:self.textDocumentProxy.documentContextBeforeInput
after:self.textDocumentProxy.documentContextAfterInput];
} }
- (void)setupUI { - (void)setupUI {
CGFloat toolBarHeight = 40; self.view.translatesAutoresizingMaskIntoConstraints = NO;
CGFloat bottom = 5;
CGFloat buttonSpace = 8;
CGFloat eachButtonHeight = (KEYBOARDHEIGHT - toolBarHeight - 10 - 8 * 3 - bottom) / 4;
UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, SCREEN_WIDTH, 30)]; //
view.backgroundColor = [UIColor redColor]; CGFloat portraitWidth = [self kb_portraitWidth];
[self.view addSubview:view]; CGFloat keyboardHeight = [self kb_keyboardHeightForWidth:portraitWidth];
} CGFloat screenWidth = CGRectGetWidth([UIScreen mainScreen].bounds);
CGFloat outerVerticalInset = KBFit(4.0f);
NSLayoutConstraint *h = [self.view.heightAnchor constraintEqualToConstant:keyboardHeight];
NSLayoutConstraint *w = [self.view.widthAnchor constraintEqualToConstant:screenWidth];
self.kb_heightConstraint = h;
self.kb_widthConstraint = w;
- (void)viewWillLayoutSubviews h.priority = UILayoutPriorityRequired;
{ w.priority = UILayoutPriorityRequired;
self.nextKeyboardButton.hidden = !self.needsInputModeSwitchKey; [NSLayoutConstraint activateConstraints:@[h, w]];
[super viewWillLayoutSubviews]; // UIInputView
} if ([self.view isKindOfClass:[UIInputView class]]) {
UIInputView *iv = (UIInputView *)self.view;
- (void)textWillChange:(id<UITextInput>)textInput { if ([iv respondsToSelector:@selector(setAllowsSelfSizing:)]) {
// The app is about to change the document's contents. Perform any preparation here. iv.allowsSelfSizing = NO;
} }
- (void)textDidChange:(id<UITextInput>)textInput {
// The app has just changed the document's contents, the document context has been updated.
UIColor *textColor = nil;
if (self.textDocumentProxy.keyboardAppearance == UIKeyboardAppearanceDark) {
textColor = [UIColor whiteColor];
} else {
textColor = [UIColor blackColor];
} }
[self.nextKeyboardButton setTitleColor:textColor forState:UIControlStateNormal]; //
[self.view addSubview:self.contentView];
[self.contentView mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerX.equalTo(self.view);
make.bottom.equalTo(self.view);
self.contentWidthConstraint = make.width.mas_equalTo(portraitWidth);
self.contentHeightConstraint = make.height.mas_equalTo(keyboardHeight);
}];
//
[self.contentView addSubview:self.bgImageView];
[self.bgImageView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(self.contentView);
}];
//
self.functionView.hidden = YES;
[self.contentView addSubview:self.functionView];
[self.functionView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(self.contentView);
}];
[self.contentView addSubview:self.keyBoardMainView];
[self.keyBoardMainView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(self.contentView);
}];
} }
#pragma mark - Private
// MARK: - Suggestions
- (void)kb_updateCurrentWordWithInsertedText:(NSString *)text {
if (text.length == 0) { return; }
if ([self kb_isAlphabeticString:text]) {
NSString *current = self.currentWord ?: @"";
self.currentWord = [current stringByAppendingString:text];
self.suppressSuggestions = NO;
[self kb_updateSuggestionsForCurrentWord];
} else {
[self kb_clearCurrentWord];
}
}
- (void)kb_clearCurrentWord {
self.currentWord = @"";
[self.keyBoardMainView kb_setSuggestions:@[]];
self.suppressSuggestions = NO;
}
- (void)kb_scheduleContextRefreshResetSuppression:(BOOL)resetSuppression {
dispatch_async(dispatch_get_main_queue(), ^{
[self kb_refreshCurrentWordFromDocumentContextResetSuppression:resetSuppression];
});
}
- (void)kb_refreshCurrentWordFromDocumentContextResetSuppression:(BOOL)resetSuppression {
NSString *context = self.textDocumentProxy.documentContextBeforeInput ?: @"";
NSString *word = [self kb_extractTrailingWordFromContext:context];
self.currentWord = word ?: @"";
if (resetSuppression) {
self.suppressSuggestions = NO;
}
[self kb_updateSuggestionsForCurrentWord];
}
- (NSString *)kb_extractTrailingWordFromContext:(NSString *)context {
if (context.length == 0) { return @""; }
static NSCharacterSet *letters = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
letters = [NSCharacterSet characterSetWithCharactersInString:@"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"];
});
NSInteger idx = (NSInteger)context.length - 1;
while (idx >= 0) {
unichar ch = [context characterAtIndex:(NSUInteger)idx];
if (![letters characterIsMember:ch]) {
break;
}
idx -= 1;
}
NSUInteger start = (NSUInteger)(idx + 1);
if (start >= context.length) { return @""; }
return [context substringFromIndex:start];
}
- (BOOL)kb_isAlphabeticString:(NSString *)text {
if (text.length == 0) { return NO; }
static NSCharacterSet *letters = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
letters = [NSCharacterSet characterSetWithCharactersInString:@"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"];
});
for (NSUInteger i = 0; i < text.length; i++) {
if (![letters characterIsMember:[text characterAtIndex:i]]) {
return NO;
}
}
return YES;
}
- (void)kb_updateSuggestionsForCurrentWord {
NSString *prefix = self.currentWord ?: @"";
if (prefix.length == 0) {
[self.keyBoardMainView kb_setSuggestions:@[]];
return;
}
if (self.suppressSuggestions) {
[self.keyBoardMainView kb_setSuggestions:@[]];
return;
}
NSArray<NSString *> *items = [self.suggestionEngine suggestionsForPrefix:prefix limit:5];
NSArray<NSString *> *cased = [self kb_applyCaseToSuggestions:items prefix:prefix];
[self.keyBoardMainView kb_setSuggestions:cased];
}
- (NSArray<NSString *> *)kb_applyCaseToSuggestions:(NSArray<NSString *> *)items prefix:(NSString *)prefix {
if (items.count == 0 || prefix.length == 0) { return items; }
BOOL allUpper = [prefix isEqualToString:prefix.uppercaseString];
BOOL firstUpper = [[prefix substringToIndex:1] isEqualToString:[[prefix substringToIndex:1] uppercaseString]];
if (!allUpper && !firstUpper) { return items; }
NSMutableArray<NSString *> *result = [NSMutableArray arrayWithCapacity:items.count];
for (NSString *word in items) {
if (allUpper) {
[result addObject:word.uppercaseString];
} else {
NSString *first = [[word substringToIndex:1] uppercaseString];
NSString *rest = (word.length > 1) ? [word substringFromIndex:1] : @"";
[result addObject:[first stringByAppendingString:rest]];
}
}
return result.copy;
}
/// /
- (void)showFunctionPanel:(BOOL)show {
//
self.functionView.hidden = !show;
self.keyBoardMainView.hidden = show;
if (show) {
[[KBMaiPointReporter sharedReporter] reportPageExposureWithEventName:@"enter_keyboard_function_panel"
pageId:@"keyboard_function_panel"
extra:nil
completion:nil];
[self hideSubscriptionPanel];
} else {
[[KBMaiPointReporter sharedReporter] reportPageExposureWithEventName:@"enter_keyboard_main_panel"
pageId:@"keyboard_main_panel"
extra:nil
completion:nil];
}
//
if (show) {
[self.contentView bringSubviewToFront:self.functionView];
} else {
[self.contentView bringSubviewToFront:self.keyBoardMainView];
}
}
/// / keyBoardMainView /
- (void)showSettingView:(BOOL)show {
if (show) {
[[KBMaiPointReporter sharedReporter] reportPageExposureWithEventName:@"enter_keyboard_settings"
pageId:@"keyboard_settings"
extra:nil
completion:nil];
// if (!self.settingView) {
self.settingView = [[KBSettingView alloc] init];
self.settingView.hidden = YES;
[self.contentView addSubview:self.settingView];
[self.settingView mas_makeConstraints:^(MASConstraintMaker *make) {
//
make.edges.equalTo(self.contentView);
}];
[self.settingView.backButton addTarget:self action:@selector(onTapSettingsBack) forControlEvents:UIControlEventTouchUpInside];
// }
[self.contentView bringSubviewToFront:self.settingView];
// keyBoardMainView self.view
[self.contentView layoutIfNeeded];
CGFloat w = CGRectGetWidth(self.keyBoardMainView.bounds);
if (w <= 0) { w = CGRectGetWidth(self.contentView.bounds); }
if (w <= 0) { w = [self kb_portraitWidth]; }
self.settingView.transform = CGAffineTransformMakeTranslation(w, 0);
self.settingView.hidden = NO;
[UIView animateWithDuration:0.25 delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{
self.settingView.transform = CGAffineTransformIdentity;
} completion:nil];
} else {
if (!self.settingView || self.settingView.hidden) return;
CGFloat w = CGRectGetWidth(self.keyBoardMainView.bounds);
if (w <= 0) { w = CGRectGetWidth(self.contentView.bounds); }
if (w <= 0) { w = [self kb_portraitWidth]; }
[UIView animateWithDuration:0.22 delay:0 options:UIViewAnimationOptionCurveEaseIn animations:^{
self.settingView.transform = CGAffineTransformMakeTranslation(w, 0);
} completion:^(BOOL finished) {
self.settingView.hidden = YES;
}];
}
}
- (void)showSubscriptionPanel {
// 1) 访
if (![[KBFullAccessManager shared] hasFullAccess]) {
// 访
// [KBHUD showInfo:KBLocalized(@"处理中…")];
[[KBFullAccessManager shared] ensureFullAccessOrGuideInView:self.view];
return;
}
//
// 2) -> App App
if (!KBAuthManager.shared.isLoggedIn) {
NSString *schemeStr = [NSString stringWithFormat:@"%@://login?src=keyboard", KB_APP_SCHEME];
NSURL *scheme = [NSURL URLWithString:schemeStr];
// UIApplication App
BOOL ok = [KBHostAppLauncher openHostAppURL:scheme fromResponder:self.view];
return;
}
[[KBMaiPointReporter sharedReporter] reportPageExposureWithEventName:@"enter_keyboard_subscription_panel"
pageId:@"keyboard_subscription_panel"
extra:nil
completion:nil];
[self showFunctionPanel:NO];
KBKeyboardSubscriptionView *panel = self.subscriptionView;
if (!panel.superview) {
panel.hidden = YES;
[self.contentView addSubview:panel];
[panel mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(self.contentView);
}];
}
[self.contentView bringSubviewToFront:panel];
panel.hidden = NO;
panel.alpha = 0.0;
CGFloat height = CGRectGetHeight(self.contentView.bounds);
if (height <= 0) { height = 260; }
panel.transform = CGAffineTransformMakeTranslation(0, height);
[panel refreshProductsIfNeeded];
[UIView animateWithDuration:0.25 delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{
panel.alpha = 1.0;
panel.transform = CGAffineTransformIdentity;
} completion:nil];
}
- (void)hideSubscriptionPanel {
if (!self.subscriptionView || self.subscriptionView.hidden) { return; }
CGFloat height = CGRectGetHeight(self.subscriptionView.bounds);
if (height <= 0) { height = CGRectGetHeight(self.contentView.bounds); }
KBKeyboardSubscriptionView *panel = self.subscriptionView;
[UIView animateWithDuration:0.22 delay:0 options:UIViewAnimationOptionCurveEaseIn animations:^{
panel.alpha = 0.0;
panel.transform = CGAffineTransformMakeTranslation(0, height);
} completion:^(BOOL finished) {
panel.hidden = YES;
panel.alpha = 1.0;
panel.transform = CGAffineTransformIdentity;
}];
}
// MARK: - KBKeyBoardMainViewDelegate
- (void)keyBoardMainView:(KBKeyBoardMainView *)keyBoardMainView didTapKey:(KBKey *)key {
switch (key.type) {
case KBKeyTypeCharacter: {
[[KBBackspaceUndoManager shared] registerNonClearAction];
NSString *text = key.output ?: key.title ?: @"";
[self.textDocumentProxy insertText:text];
[self kb_updateCurrentWordWithInsertedText:text];
[[KBInputBufferManager shared] appendText:text];
} break;
case KBKeyTypeBackspace:
[[KBInputBufferManager shared] refreshFromProxyIfPossible:self.textDocumentProxy];
[[KBInputBufferManager shared] prepareSnapshotForDeleteWithContextBefore:self.textDocumentProxy.documentContextBeforeInput
after:self.textDocumentProxy.documentContextAfterInput];
[[KBBackspaceUndoManager shared] captureAndDeleteBackwardFromProxy:self.textDocumentProxy count:1];
[self kb_scheduleContextRefreshResetSuppression:NO];
[[KBInputBufferManager shared] applyHoldDeleteCount:1];
break;
case KBKeyTypeSpace:
[[KBBackspaceUndoManager shared] registerNonClearAction];
[self.textDocumentProxy insertText:@" "];
[self kb_clearCurrentWord];
[[KBInputBufferManager shared] appendText:@" "];
break;
case KBKeyTypeReturn:
[[KBBackspaceUndoManager shared] registerNonClearAction];
[self.textDocumentProxy insertText:@"\n"];
[self kb_clearCurrentWord];
[[KBInputBufferManager shared] appendText:@"\n"];
break;
case KBKeyTypeGlobe:
[self advanceToNextInputMode]; break;
case KBKeyTypeCustom:
[[KBBackspaceUndoManager shared] registerNonClearAction];
//
[self showFunctionPanel:YES];
[self kb_clearCurrentWord];
break;
case KBKeyTypeModeChange:
case KBKeyTypeShift:
// KBKeyBoardMainView/KBKeyboardView
break;
}
}
- (void)keyBoardMainView:(KBKeyBoardMainView *)keyBoardMainView didTapToolActionAtIndex:(NSInteger)index {
NSDictionary *extra = @{@"index": @(index)};
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_keyboard_toolbar_action"
pageId:@"keyboard_main_panel"
elementId:@"toolbar_action"
extra:extra
completion:nil];
if (index == 0) {
[self showFunctionPanel:YES];
[self kb_clearCurrentWord];
return;
}
[self showFunctionPanel:NO];
}
- (void)keyBoardMainViewDidTapSettings:(KBKeyBoardMainView *)keyBoardMainView {
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_keyboard_settings_btn"
pageId:@"keyboard_main_panel"
elementId:@"settings_btn"
extra:nil
completion:nil];
[self showSettingView:YES];
}
- (void)keyBoardMainView:(KBKeyBoardMainView *)keyBoardMainView didSelectEmoji:(NSString *)emoji {
if (emoji.length == 0) { return; }
[[KBBackspaceUndoManager shared] registerNonClearAction];
[self.textDocumentProxy insertText:emoji];
[self kb_clearCurrentWord];
[[KBInputBufferManager shared] appendText:emoji];
}
- (void)keyBoardMainViewDidTapUndo:(KBKeyBoardMainView *)keyBoardMainView {
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_keyboard_undo_btn"
pageId:@"keyboard_main_panel"
elementId:@"undo_btn"
extra:nil
completion:nil];
[[KBBackspaceUndoManager shared] performUndoFromResponder:self.view];
[self kb_scheduleContextRefreshResetSuppression:YES];
}
- (void)keyBoardMainViewDidTapEmojiSearch:(KBKeyBoardMainView *)keyBoardMainView {
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_keyboard_emoji_search_btn"
pageId:@"keyboard_main_panel"
elementId:@"emoji_search_btn"
extra:nil
completion:nil];
[KBHUD showInfo:KBLocalized(@"Search coming soon")];
}
- (void)keyBoardMainView:(KBKeyBoardMainView *)keyBoardMainView didSelectSuggestion:(NSString *)suggestion {
if (suggestion.length == 0) { return; }
NSDictionary *extra = @{@"suggestion_len": @(suggestion.length)};
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_keyboard_suggestion_item"
pageId:@"keyboard_main_panel"
elementId:@"suggestion_item"
extra:extra
completion:nil];
[[KBBackspaceUndoManager shared] registerNonClearAction];
NSString *current = self.currentWord ?: @"";
if (current.length > 0) {
for (NSUInteger i = 0; i < current.length; i++) {
[self.textDocumentProxy deleteBackward];
}
}
[self.textDocumentProxy insertText:suggestion];
self.currentWord = suggestion;
[self.suggestionEngine recordSelection:suggestion];
self.suppressSuggestions = YES;
[self.keyBoardMainView kb_setSuggestions:@[]];
[[KBInputBufferManager shared] replaceTailWithText:suggestion deleteCount:current.length];
}
// MARK: - KBFunctionViewDelegate
- (void)functionView:(KBFunctionView *)functionView didTapToolActionAtIndex:(NSInteger)index {
// index == 0
if (index == 0) {
[self showFunctionPanel:NO];
}
}
- (void)functionView:(KBFunctionView *_Nullable)functionView didRightTapToolActionAtIndex:(NSInteger)index{
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_keyboard_function_right_action"
pageId:@"keyboard_function_panel"
elementId:@"right_action"
extra:@{@"action": @"login_or_recharge"}
completion:nil];
if (!KBAuthManager.shared.isLoggedIn) {
NSString *schemeStr = [NSString stringWithFormat:@"%@://login?src=keyboard", KB_APP_SCHEME];
NSURL *scheme = [NSURL URLWithString:schemeStr];
// UIApplication App
BOOL ok = [KBHostAppLauncher openHostAppURL:scheme fromResponder:self.view];
return;
}
NSString *schemeStr = [NSString stringWithFormat:@"%@://recharge?src=keyboard", KB_APP_SCHEME];
NSURL *scheme = [NSURL URLWithString:schemeStr];
//
// if (!ul && !scheme) { return; }
//
// UIApplication App
BOOL ok = [KBHostAppLauncher openHostAppURL:scheme fromResponder:self.view];
if (!ok) {
//
// XXX App /
[KBHUD showInfo:@"请回到桌面手动打开App进行充值"];
}
}
- (void)functionViewDidRequestSubscription:(KBFunctionView *)functionView {
[self showSubscriptionPanel];
}
#pragma mark - KBKeyboardSubscriptionViewDelegate
- (void)subscriptionViewDidTapClose:(KBKeyboardSubscriptionView *)view {
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_keyboard_subscription_close_btn"
pageId:@"keyboard_subscription_panel"
elementId:@"close_btn"
extra:nil
completion:nil];
[self hideSubscriptionPanel];
}
- (void)subscriptionView:(KBKeyboardSubscriptionView *)view didTapPurchaseForProduct:(KBKeyboardSubscriptionProduct *)product {
NSMutableDictionary *extra = [NSMutableDictionary dictionary];
if ([product.productId isKindOfClass:NSString.class] && product.productId.length > 0) {
extra[@"product_id"] = product.productId;
}
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_keyboard_subscription_product_btn"
pageId:@"keyboard_subscription_panel"
elementId:@"product_btn"
extra:extra.copy
completion:nil];
[self hideSubscriptionPanel];
[self kb_openRechargeForProduct:product];
}
#pragma mark - lazy
- (KBKeyBoardMainView *)keyBoardMainView{
if (!_keyBoardMainView) {
_keyBoardMainView = [[KBKeyBoardMainView alloc] init];
_keyBoardMainView.delegate = self;
}
return _keyBoardMainView;
}
- (KBFunctionView *)functionView{
if (!_functionView) {
_functionView = [[KBFunctionView alloc] init];
_functionView.delegate = self; // Bar
}
return _functionView;
}
- (KBSettingView *)settingView {
if (!_settingView) {
_settingView = [[KBSettingView alloc] init];
}
return _settingView;
}
- (KBKeyboardSubscriptionView *)subscriptionView {
if (!_subscriptionView) {
_subscriptionView = [[KBKeyboardSubscriptionView alloc] init];
_subscriptionView.delegate = self;
_subscriptionView.hidden = YES;
_subscriptionView.alpha = 0.0;
}
return _subscriptionView;
}
#pragma mark - Actions
- (void)kb_openRechargeForProduct:(KBKeyboardSubscriptionProduct *)product {
if (![product isKindOfClass:KBKeyboardSubscriptionProduct.class] || product.productId.length == 0) {
[KBHUD showInfo:KBLocalized(@"Product unavailable")];
return;
}
NSString *encodedId = [self.class kb_urlEncodedString:product.productId];
NSString *title = [product displayTitle];
NSString *encodedTitle = [self.class kb_urlEncodedString:title];
NSMutableArray<NSString *> *params = [NSMutableArray arrayWithObjects:@"autoPay=1", @"prefill=1", nil];
if (encodedId.length) {
[params addObject:[NSString stringWithFormat:@"productId=%@", encodedId]];
}
if (encodedTitle.length) {
[params addObject:[NSString stringWithFormat:@"productTitle=%@", encodedTitle]];
}
NSString *query = [params componentsJoinedByString:@"&"];
NSString *urlString = [NSString stringWithFormat:@"%@://recharge?src=keyboard&%@", KB_APP_SCHEME, query];
NSURL *scheme = [NSURL URLWithString:urlString];
BOOL success = [KBHostAppLauncher openHostAppURL:scheme fromResponder:self.view];
if (!success) {
[KBHUD showInfo:KBLocalized(@"Please open the App to finish purchase")];
}
}
+ (NSString *)kb_urlEncodedString:(NSString *)value {
if (value.length == 0) { return @""; }
NSString *reserved = @"!*'();:@&=+$,/?%#[]";
NSMutableCharacterSet *allowed = [[NSCharacterSet URLQueryAllowedCharacterSet] mutableCopy];
[allowed removeCharactersInString:reserved];
return [value stringByAddingPercentEncodingWithAllowedCharacters:allowed] ?: @"";
}
- (void)onTapSettingsBack {
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_keyboard_settings_back_btn"
pageId:@"keyboard_settings"
elementId:@"back_btn"
extra:nil
completion:nil];
[self showSettingView:NO];
}
- (void)dealloc {
CFNotificationCenterRemoveObserver(CFNotificationCenterGetDarwinNotifyCenter(),
(__bridge const void *)(self),
(__bridge CFStringRef)KBDarwinSkinInstallRequestNotification,
NULL);
}
// App App
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
// if (!_kb_didTriggerLoginDeepLinkOnce) {
// _kb_didTriggerLoginDeepLinkOnce = YES;
// // App
// if (!KBAuthManager.shared.isLoggedIn) {
// [self kb_tryOpenContainerForLoginIfNeeded];
// }
// }
}
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
[self kb_updateKeyboardLayoutIfNeeded];
}
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
__weak typeof(self) weakSelf = self;
[coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
[weakSelf kb_updateKeyboardLayoutIfNeeded];
} completion:^(__unused id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
[weakSelf kb_updateKeyboardLayoutIfNeeded];
}];
}
//- (void)kb_tryOpenContainerForLoginIfNeeded {
// // 使 App Scheme
// NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"%@@//login?src=keyboard", KB_APP_SCHEME]];
// if (!url) return;
// KBWeakSelf
// [self.extensionContext openURL:url completionHandler:^(__unused BOOL success) {
// // 使
// __unused typeof(weakSelf) selfStrong = weakSelf;
// }];
//}
#pragma mark - Theme
- (void)kb_applyTheme {
KBSkinTheme *t = [KBSkinManager shared].current;
UIImage *img = [[KBSkinManager shared] currentBackgroundImage];
NSLog(@"⌨️[Keyboard] apply theme id=%@ hasBg=%d", t.skinId, (img != nil));
self.bgImageView.image = img;
BOOL hasImg = (img != nil);
self.view.backgroundColor = hasImg ? [UIColor clearColor] : t.keyboardBackground;
self.contentView.backgroundColor = hasImg ? [UIColor clearColor] : t.keyboardBackground;
self.keyBoardMainView.backgroundColor = hasImg ? [UIColor clearColor] : t.keyboardBackground;
//
if ([self.keyBoardMainView respondsToSelector:@selector(kb_applyTheme)]) {
// method declared in KBKeyBoardMainView.h
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[self.keyBoardMainView performSelector:@selector(kb_applyTheme)];
#pragma clang diagnostic pop
}
if ([self.functionView respondsToSelector:@selector(kb_applyTheme)]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[self.functionView performSelector:@selector(kb_applyTheme)];
#pragma clang diagnostic pop
}
}
- (void)kb_consumePendingShopSkin {
KBWeakSelf
[KBSkinInstallBridge consumePendingRequestFromBundle:NSBundle.mainBundle
completion:^(BOOL success, NSError * _Nullable error) {
if (!success) {
if (error) {
NSLog(@"[Keyboard] skin request failed: %@", error);
[KBHUD showInfo:KBLocalized(@"皮肤资源准备失败,请稍后再试")];
}
return;
}
[weakSelf kb_applyTheme];
[KBHUD showInfo:KBLocalized(@"皮肤已更新,立即体验吧")];
}];
}
#pragma mark - Layout Helpers
- (CGFloat)kb_portraitWidth {
CGSize s = [UIScreen mainScreen].bounds.size;
return MIN(s.width, s.height);
}
- (CGFloat)kb_keyboardHeightForWidth:(CGFloat)width {
if (width <= 0) { width = KB_DESIGN_WIDTH; }
return kKBKeyboardBaseHeight * (width / KB_DESIGN_WIDTH);
}
- (void)kb_updateKeyboardLayoutIfNeeded {
CGFloat portraitWidth = [self kb_portraitWidth];
CGFloat keyboardHeight = [self kb_keyboardHeightForWidth:portraitWidth];
CGFloat containerWidth = CGRectGetWidth(self.view.superview.bounds);
if (containerWidth <= 0) {
containerWidth = CGRectGetWidth(self.view.window.bounds);
}
if (containerWidth <= 0) {
containerWidth = CGRectGetWidth([UIScreen mainScreen].bounds);
}
BOOL widthChanged = (fabs(self.kb_lastPortraitWidth - portraitWidth) >= 0.5);
BOOL heightChanged = (fabs(self.kb_lastKeyboardHeight - keyboardHeight) >= 0.5);
if (!widthChanged && !heightChanged && containerWidth > 0 && self.kb_widthConstraint.constant == containerWidth) {
return;
}
self.kb_lastPortraitWidth = portraitWidth;
self.kb_lastKeyboardHeight = keyboardHeight;
if (self.kb_heightConstraint) {
self.kb_heightConstraint.constant = keyboardHeight;
}
if (containerWidth > 0 && self.kb_widthConstraint) {
self.kb_widthConstraint.constant = containerWidth;
}
if (self.contentWidthConstraint) {
[self.contentWidthConstraint setOffset:portraitWidth];
}
if (self.contentHeightConstraint) {
[self.contentHeightConstraint setOffset:keyboardHeight];
}
[self.view layoutIfNeeded];
}
#pragma mark - Lazy
- (UIView *)contentView {
if (!_contentView) {
_contentView = [[UIView alloc] init];
_contentView.backgroundColor = [UIColor clearColor];
}
return _contentView;
}
- (UIImageView *)bgImageView {
if (!_bgImageView) {
_bgImageView = [[UIImageView alloc] init];
_bgImageView.contentMode = UIViewContentModeScaleAspectFill;
_bgImageView.clipsToBounds = YES;
}
return _bgImageView;
}
@end @end

View File

@@ -0,0 +1,46 @@
//
// KBEmojiDataProvider.h
// CustomKeyboard
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
FOUNDATION_EXPORT NSString * const KBEmojiRecentsDidChangeNotification;
@class KBEmojiCategory, KBEmojiItem;
@interface KBEmojiItem : NSObject <NSCopying>
@property (nonatomic, copy, readonly) NSString *value;
@property (nonatomic, copy, readonly) NSString *name;
- (instancetype)initWithValue:(NSString *)value name:(NSString *)name;
@end
@interface KBEmojiCategory : NSObject
@property (nonatomic, copy, readonly) NSString *identifier;
@property (nonatomic, copy, readonly) NSString *displayTitle;
@property (nonatomic, copy, readonly) NSString *iconSymbol;
@property (nonatomic, assign, readonly, getter=isDynamic) BOOL dynamic;
@property (nonatomic, copy, readonly) NSArray<KBEmojiItem *> *items;
@end
@interface KBEmojiDataProvider : NSObject
+ (instancetype)shared;
/// 所有分类(按系统顺序),包含“常用”。
@property (nonatomic, copy, readonly) NSArray<KBEmojiCategory *> *categories;
/// 记录一次 emoji 选择,并刷新“常用”分类。
- (void)recordEmojiSelection:(NSString *)emoji;
/// 重新加载 JSON若首次调用
- (void)reloadIfNeeded;
/// 更新当前语言对应的分类标题。
- (void)refreshLocalizedTitles;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,270 @@
//
// KBEmojiDataProvider.m
// CustomKeyboard
//
#import "KBEmojiDataProvider.h"
#import "KBLocalizationManager.h"
#import "KBConfig.h"
NSString * const KBEmojiRecentsDidChangeNotification = @"KBEmojiRecentsDidChangeNotification";
static NSString * const kKBEmojiJSONFileName = @"emoji_categories";
static NSString * const kKBEmojiRecentsStoreKey = @"KBEmojiRecentEmojis";
static NSString * const kKBEmojiRecentsCategoryId = @"recents";
static const NSUInteger kKBEmojiRecentsLimit = 32;
#pragma mark - Model Implementations
@interface KBEmojiItem ()
@property (nonatomic, copy, readwrite) NSString *value;
@property (nonatomic, copy, readwrite) NSString *name;
@end
@implementation KBEmojiItem
- (instancetype)initWithValue:(NSString *)value name:(NSString *)name {
if (self = [super init]) {
_value = [value copy];
_name = [name copy];
}
return self;
}
- (id)copyWithZone:(NSZone *)zone {
KBEmojiItem *item = [[[self class] allocWithZone:zone] initWithValue:self.value name:self.name];
return item;
}
@end
@interface KBEmojiCategory ()
@property (nonatomic, copy, readwrite) NSString *identifier;
@property (nonatomic, copy) NSDictionary<NSString *, NSString *> *titleMap;
@property (nonatomic, copy, readwrite) NSString *displayTitle;
@property (nonatomic, copy, readwrite) NSString *iconSymbol;
@property (nonatomic, assign, readwrite, getter=isDynamic) BOOL dynamic;
@property (nonatomic, copy, readwrite) NSArray<KBEmojiItem *> *items;
@end
@implementation KBEmojiCategory
- (void)refreshDisplayTitleForLanguage:(NSString *)lang {
if (lang.length == 0) {
lang = KBLanguageCodeEnglish;
}
NSString *title = self.titleMap[lang];
if (title.length == 0) {
if ([lang.lowercaseString hasPrefix:@"zh"]) {
title = self.titleMap[@"zh-Hans"] ?: self.titleMap[@"zh-hans"];
}
}
if (title.length == 0) {
title = self.titleMap[@"en"];
}
if (title.length == 0) {
title = self.titleMap.allValues.firstObject;
}
self.displayTitle = title ?: @"";
}
@end
#pragma mark - Data Provider
@interface KBEmojiDataProvider ()
@property (nonatomic, copy) NSArray<KBEmojiCategory *> *categoriesInternal;
@property (nonatomic, strong) NSMutableDictionary<NSString *, KBEmojiItem *> *itemLookup;
@property (nonatomic, strong) NSMutableOrderedSet<NSString *> *recentValues;
@end
@implementation KBEmojiDataProvider
+ (instancetype)shared {
static KBEmojiDataProvider *m;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
m = [KBEmojiDataProvider new];
[[NSNotificationCenter defaultCenter] addObserver:m
selector:@selector(onLocalizationChanged:)
name:KBLocalizationDidChangeNotification
object:nil];
});
return m;
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (NSArray<KBEmojiCategory *> *)categories {
[self reloadIfNeeded];
return self.categoriesInternal ?: @[];
}
- (void)reloadIfNeeded {
if (self.categoriesInternal.count > 0) { return; }
[self loadEmojiJSON];
[self refreshLocalizedTitles];
[self loadRecentsFromStore];
[self rebuildRecentsCategory];
}
- (void)loadEmojiJSON {
NSString *path = [[NSBundle mainBundle] pathForResource:kKBEmojiJSONFileName ofType:@"json"];
if (path.length == 0) {
return;
}
NSData *data = [NSData dataWithContentsOfFile:path];
if (data.length == 0) { return; }
NSError *err = nil;
NSDictionary *root = [NSJSONSerialization JSONObjectWithData:data options:0 error:&err];
if (!root || err) {
NSLog(@"[Emoji] failed to parse json: %@", err);
return;
}
NSArray *catArray = root[@"categories"];
if (![catArray isKindOfClass:NSArray.class]) {
return;
}
NSMutableArray<KBEmojiCategory *> *tmpCats = [NSMutableArray arrayWithCapacity:catArray.count];
self.itemLookup = [NSMutableDictionary dictionary];
for (NSDictionary *catDict in catArray) {
if (![catDict isKindOfClass:NSDictionary.class]) continue;
KBEmojiCategory *category = [KBEmojiCategory new];
category.identifier = catDict[@"id"] ?: @"";
NSDictionary *titleMap = catDict[@"title"];
if ([titleMap isKindOfClass:NSDictionary.class]) {
category.titleMap = titleMap;
} else {
category.titleMap = @{};
}
NSString *iconKey = catDict[@"icon"];
category.iconSymbol = [self symbolForIconKey:iconKey];
NSString *type = catDict[@"type"];
category.dynamic = [type.lowercaseString isEqualToString:@"dynamic"];
NSArray *emojiArray = catDict[@"emojis"];
NSMutableArray<KBEmojiItem *> *items = [NSMutableArray arrayWithCapacity:[emojiArray count]];
if ([emojiArray isKindOfClass:NSArray.class]) {
for (NSDictionary *emojiDict in emojiArray) {
if (![emojiDict isKindOfClass:NSDictionary.class]) continue;
NSString *value = emojiDict[@"value"];
if (value.length == 0) continue;
NSString *name = emojiDict[@"name"] ?: @"";
KBEmojiItem *item = [[KBEmojiItem alloc] initWithValue:value name:name];
[items addObject:item];
if (value.length > 0) {
self.itemLookup[value] = item;
}
}
}
category.items = items.copy;
[tmpCats addObject:category];
}
self.categoriesInternal = tmpCats.copy;
}
- (NSString *)symbolForIconKey:(NSString *)key {
static NSDictionary<NSString *, NSString *> *map;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
map = @{
@"emoji_tab_recent": @"🕘",
@"emoji_tab_people": @"😊",
@"emoji_tab_nature": @"🌿",
@"emoji_tab_food": @"🍔",
@"emoji_tab_activity": @"🏀",
@"emoji_tab_travel": @"✈️",
@"emoji_tab_objects": @"💡",
@"emoji_tab_symbols": @"♾",
@"emoji_tab_flags": @"🏳️"
};
});
NSString *symbol = map[key];
return symbol.length ? symbol : @"●";
}
- (void)refreshLocalizedTitles {
NSString *lang = [KBLocalizationManager shared].currentLanguageCode ?: KBLanguageCodeEnglish;
for (KBEmojiCategory *cat in self.categoriesInternal) {
[cat refreshDisplayTitleForLanguage:lang];
}
}
- (void)onLocalizationChanged:(__unused NSNotification *)note {
[self refreshLocalizedTitles];
[[NSNotificationCenter defaultCenter] postNotificationName:KBEmojiRecentsDidChangeNotification object:nil];
}
- (void)recordEmojiSelection:(NSString *)emoji {
if (emoji.length == 0) return;
[self reloadIfNeeded];
if (!self.recentValues) {
self.recentValues = [NSMutableOrderedSet orderedSet];
}
[self.recentValues removeObject:emoji];
[self.recentValues insertObject:emoji atIndex:0];
while (self.recentValues.count > kKBEmojiRecentsLimit) {
[self.recentValues removeObjectAtIndex:self.recentValues.count - 1];
}
[self saveRecentsToStore];
[self rebuildRecentsCategory];
[[NSNotificationCenter defaultCenter] postNotificationName:KBEmojiRecentsDidChangeNotification object:nil];
}
- (void)loadRecentsFromStore {
NSUserDefaults *defs = [[NSUserDefaults alloc] initWithSuiteName:AppGroup];
if (!defs) { defs = NSUserDefaults.standardUserDefaults; }
NSArray *stored = [defs objectForKey:kKBEmojiRecentsStoreKey];
NSMutableOrderedSet *set = [NSMutableOrderedSet orderedSet];
if ([stored isKindOfClass:NSArray.class]) {
for (id obj in stored) {
if (![obj isKindOfClass:NSString.class]) continue;
NSString *str = (NSString *)obj;
if (str.length == 0) continue;
[set addObject:str];
if (set.count >= kKBEmojiRecentsLimit) break;
}
}
self.recentValues = set;
}
- (void)saveRecentsToStore {
if (!self.recentValues) return;
NSArray *arr = self.recentValues.array;
NSUserDefaults *defs = [[NSUserDefaults alloc] initWithSuiteName:AppGroup];
if (!defs) { defs = NSUserDefaults.standardUserDefaults; }
[defs setObject:arr forKey:kKBEmojiRecentsStoreKey];
[defs synchronize];
}
- (void)rebuildRecentsCategory {
KBEmojiCategory *recent = [self categoryForIdentifier:kKBEmojiRecentsCategoryId];
if (!recent) return;
NSArray<NSString *> *values = self.recentValues.array ?: @[];
NSMutableArray<KBEmojiItem *> *items = [NSMutableArray arrayWithCapacity:values.count];
for (NSString *value in values) {
KBEmojiItem *item = self.itemLookup[value];
if (!item) {
item = [[KBEmojiItem alloc] initWithValue:value name:@""];
}
[items addObject:item];
}
recent.items = items.copy;
}
- (KBEmojiCategory *)categoryForIdentifier:(NSString *)identifier {
if (identifier.length == 0) return nil;
for (KBEmojiCategory *cat in self.categoriesInternal) {
if ([cat.identifier isEqualToString:identifier]) {
return cat;
}
}
return nil;
}
@end

View File

@@ -0,0 +1,43 @@
//
// KBFullAccessManager.h
// 统一封装:检测并管理键盘扩展的“允许完全访问”状态
//
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
typedef NS_ENUM(NSInteger, KBFullAccessState) {
KBFullAccessStateUnknown = 0, // 无法确定(降级处理为未开启)
KBFullAccessStateDenied, // 未开启完全访问
KBFullAccessStateGranted // 已开启完全访问
};
/// 状态变更通知(仅扩展进程内广播)
extern NSNotificationName const KBFullAccessChangedNotification;
/// 键盘扩展“完全访问”状态管理
@interface KBFullAccessManager : NSObject
+ (instancetype)shared;
/// 绑定当前的 UIInputViewController用于调用系统私有选择器 hasFullAccess按字符串反射避免编译期引用
- (void)bindInputController:(UIInputViewController *)ivc;
/// 当前状态(内部做缓存;如需强制刷新,调用 refresh
- (KBFullAccessState)currentState;
/// 便捷判断
- (BOOL)hasFullAccess;
/// 立即刷新一次状态(若状态有变化会发送 KBFullAccessChangedNotification
- (void)refresh;
/// 若未开启,则在传入视图上展示引导弹层(使用现有的 KBFullAccessGuideView返回是否已开启
- (BOOL)ensureFullAccessOrGuideInView:(UIView *)parent;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,100 @@
//
// KBFullAccessManager.m
//
// 访
// 1) UIInputViewController hasFullAccess API
// 2) Unknown Denied
//
#import "KBFullAccessManager.h"
#import <objc/message.h>
#if __has_include("KBNetworkManager.h")
#import "KBNetworkManager.h"
#endif
#if __has_include("KBKeyboardPermissionManager.h")
#import "KBKeyboardPermissionManager.h"
#endif
NSNotificationName const KBFullAccessChangedNotification = @"KBFullAccessChangedNotification";
@interface KBFullAccessManager ()
@property (nonatomic, weak) UIInputViewController *ivc;
@property (nonatomic, assign) KBFullAccessState state;
@end
@implementation KBFullAccessManager
+ (instancetype)shared {
static KBFullAccessManager *m; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ m = [KBFullAccessManager new]; });
return m;
}
- (instancetype)init {
if (self = [super init]) {
_state = KBFullAccessStateUnknown;
}
return self;
}
- (void)bindInputController:(UIInputViewController *)ivc {
self.ivc = ivc;
[self refresh];
}
- (KBFullAccessState)currentState { return _state; }
- (BOOL)hasFullAccess { return self.state == KBFullAccessStateGranted; }
- (void)refresh {
KBFullAccessState newState = [self p_detectFullAccessState];
if (newState != self.state) {
self.state = newState;
[[NSNotificationCenter defaultCenter] postNotificationName:KBFullAccessChangedNotification object:nil];
[self p_applySideEffects];
}
}
- (BOOL)ensureFullAccessOrGuideInView:(UIView *)parent {
[self refresh];
if (self.state == KBFullAccessStateGranted) return YES;
#if __has_include("KBFullAccessGuideView.h")
// App
Class guideCls = NSClassFromString(@"KBFullAccessGuideView");
if (guideCls && [guideCls respondsToSelector:NSSelectorFromString(@"showInView:")]) {
SEL sel = NSSelectorFromString(@"showInView:");
((void (*)(id, SEL, UIView *))objc_msgSend)(guideCls, sel, parent);
}
#endif
return NO;
}
#pragma mark - Detect
// hasFullAccess Unknown
- (KBFullAccessState)p_detectFullAccessState {
UIInputViewController *ivc = self.ivc;
if (!ivc) return KBFullAccessStateUnknown;
SEL sel = NSSelectorFromString(@"hasFullAccess");
if ([ivc respondsToSelector:sel]) {
BOOL granted = ((BOOL (*)(id, SEL))objc_msgSend)(ivc, sel);
return granted ? KBFullAccessStateGranted : KBFullAccessStateDenied;
}
// Unknown
return KBFullAccessStateUnknown;
}
#pragma mark - Side Effects
- (void)p_applySideEffects {
#if __has_include("KBNetworkManager.h")
// 访
[KBNetworkManager shared].enabled = (self.state == KBFullAccessStateGranted);
#endif
#if __has_include("KBKeyboardPermissionManager.h")
// App访App
[[KBKeyboardPermissionManager shared] reportFullAccessFromExtension:(self.state == KBFullAccessStateGranted)];
#endif
}
@end

View File

@@ -0,0 +1,23 @@
//
// KBSuggestionEngine.h
// CustomKeyboard
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
/// Simple local suggestion engine (prefix match + lightweight ranking).
@interface KBSuggestionEngine : NSObject
+ (instancetype)shared;
/// Returns suggestions for prefix (lowercase expected), limited by count.
- (NSArray<NSString *> *)suggestionsForPrefix:(NSString *)prefix limit:(NSUInteger)limit;
/// Record a selection to slightly boost ranking next time.
- (void)recordSelection:(NSString *)word;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,167 @@
//
// KBSuggestionEngine.m
// CustomKeyboard
//
#import "KBSuggestionEngine.h"
#import "KBConfig.h"
@interface KBSuggestionEngine ()
@property (nonatomic, copy) NSArray<NSString *> *words;
@property (nonatomic, strong) NSMutableDictionary<NSString *, NSNumber *> *selectionCounts;
@property (nonatomic, strong) NSSet<NSString *> *priorityWords;
@end
@implementation KBSuggestionEngine
+ (instancetype)shared {
static KBSuggestionEngine *engine;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
engine = [[KBSuggestionEngine alloc] init];
});
return engine;
}
- (instancetype)init {
if (self = [super init]) {
_selectionCounts = [NSMutableDictionary dictionary];
NSArray<NSString *> *defaults = [self.class kb_defaultWords];
_priorityWords = [NSSet setWithArray:defaults];
_words = [self kb_loadWords];
}
return self;
}
- (NSArray<NSString *> *)suggestionsForPrefix:(NSString *)prefix limit:(NSUInteger)limit {
if (prefix.length == 0 || limit == 0) { return @[]; }
NSString *lower = prefix.lowercaseString;
NSMutableArray<NSString *> *matches = [NSMutableArray array];
for (NSString *word in self.words) {
if ([word hasPrefix:lower]) {
[matches addObject:word];
if (matches.count >= limit * 3) {
// Avoid scanning too many matches for long lists.
break;
}
}
}
if (matches.count == 0) { return @[]; }
[matches sortUsingComparator:^NSComparisonResult(NSString *a, NSString *b) {
NSInteger ca = self.selectionCounts[a].integerValue;
NSInteger cb = self.selectionCounts[b].integerValue;
if (ca != cb) {
return (cb > ca) ? NSOrderedAscending : NSOrderedDescending;
}
BOOL pa = [self.priorityWords containsObject:a];
BOOL pb = [self.priorityWords containsObject:b];
if (pa != pb) {
return pa ? NSOrderedAscending : NSOrderedDescending;
}
return [a compare:b];
}];
if (matches.count > limit) {
return [matches subarrayWithRange:NSMakeRange(0, limit)];
}
return matches.copy;
}
- (void)recordSelection:(NSString *)word {
if (word.length == 0) { return; }
NSString *key = word.lowercaseString;
NSInteger count = self.selectionCounts[key].integerValue + 1;
self.selectionCounts[key] = @(count);
}
#pragma mark - Defaults
- (NSArray<NSString *> *)kb_loadWords {
NSMutableOrderedSet<NSString *> *set = [[NSMutableOrderedSet alloc] init];
[set addObjectsFromArray:[self.class kb_defaultWords]];
NSArray<NSString *> *paths = [self kb_wordListPaths];
for (NSString *path in paths) {
if (path.length == 0) { continue; }
NSString *content = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
if (content.length == 0) { continue; }
NSArray<NSString *> *lines = [content componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]];
for (NSString *line in lines) {
NSString *word = [self kb_sanitizedWordFromLine:line];
if (word.length == 0) { continue; }
[set addObject:word];
}
}
NSArray<NSString *> *result = set.array ?: @[];
return result;
}
- (NSArray<NSString *> *)kb_wordListPaths {
NSMutableArray<NSString *> *paths = [NSMutableArray array];
// 1) App Group override (allows server-downloaded large list).
NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:AppGroup];
if (containerURL.path.length > 0) {
NSString *groupPath = [[containerURL path] stringByAppendingPathComponent:@"kb_words.txt"];
[paths addObject:groupPath];
}
// 2) Bundle fallback.
NSString *bundlePath = [[NSBundle mainBundle] pathForResource:@"kb_words" ofType:@"txt"];
if (bundlePath.length > 0) {
[paths addObject:bundlePath];
}
return paths;
}
- (NSString *)kb_sanitizedWordFromLine:(NSString *)line {
NSString *trimmed = [[line stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] lowercaseString];
if (trimmed.length == 0) { return @""; }
static NSCharacterSet *letters = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
letters = [NSCharacterSet characterSetWithCharactersInString:@"abcdefghijklmnopqrstuvwxyz"];
});
for (NSUInteger i = 0; i < trimmed.length; i++) {
if (![letters characterIsMember:[trimmed characterAtIndex:i]]) {
return @"";
}
}
return trimmed;
}
+ (NSArray<NSString *> *)kb_defaultWords {
return @[
@"a", @"an", @"and", @"are", @"as", @"at",
@"app", @"ap", @"apple", @"apply", @"april", @"application",
@"about", @"above", @"after", @"again", @"against", @"all",
@"am", @"among", @"amount", @"any", @"around",
@"be", @"because", @"been", @"before", @"being", @"below",
@"best", @"between", @"both", @"but", @"by",
@"can", @"could", @"come", @"common", @"case",
@"do", @"does", @"down", @"day",
@"each", @"early", @"end", @"even", @"every",
@"for", @"from", @"first", @"found", @"free",
@"get", @"good", @"great", @"go",
@"have", @"has", @"had", @"help", @"how",
@"in", @"is", @"it", @"if", @"into",
@"just", @"keep", @"kind", @"know",
@"like", @"look", @"long", @"last",
@"make", @"more", @"most", @"my",
@"new", @"no", @"not", @"now",
@"of", @"on", @"one", @"or", @"other", @"our", @"out",
@"people", @"place", @"please",
@"quick", @"quite",
@"right", @"read", @"real",
@"see", @"say", @"some", @"such", @"so",
@"the", @"to", @"this", @"that", @"them", @"then", @"there", @"they", @"these", @"time",
@"use", @"up", @"under",
@"very",
@"we", @"with", @"what", @"when", @"where", @"who", @"why", @"will", @"would",
@"you", @"your"
];
}
@end

View File

@@ -1,26 +0,0 @@
//
// MASCompositeConstraint.h
// Masonry
//
// Created by Jonas Budelmann on 21/07/13.
// Copyright (c) 2013 cloudling. All rights reserved.
//
#import "MASConstraint.h"
#import "MASUtilities.h"
/**
* A group of MASConstraint objects
*/
@interface MASCompositeConstraint : MASConstraint
/**
* Creates a composite with a predefined array of children
*
* @param children child MASConstraints
*
* @return a composite constraint
*/
- (id)initWithChildren:(NSArray *)children;
@end

View File

@@ -1,183 +0,0 @@
//
// MASCompositeConstraint.m
// Masonry
//
// Created by Jonas Budelmann on 21/07/13.
// Copyright (c) 2013 cloudling. All rights reserved.
//
#import "MASCompositeConstraint.h"
#import "MASConstraint+Private.h"
@interface MASCompositeConstraint () <MASConstraintDelegate>
@property (nonatomic, strong) id mas_key;
@property (nonatomic, strong) NSMutableArray *childConstraints;
@end
@implementation MASCompositeConstraint
- (id)initWithChildren:(NSArray *)children {
self = [super init];
if (!self) return nil;
_childConstraints = [children mutableCopy];
for (MASConstraint *constraint in _childConstraints) {
constraint.delegate = self;
}
return self;
}
#pragma mark - MASConstraintDelegate
- (void)constraint:(MASConstraint *)constraint shouldBeReplacedWithConstraint:(MASConstraint *)replacementConstraint {
NSUInteger index = [self.childConstraints indexOfObject:constraint];
NSAssert(index != NSNotFound, @"Could not find constraint %@", constraint);
[self.childConstraints replaceObjectAtIndex:index withObject:replacementConstraint];
}
- (MASConstraint *)constraint:(MASConstraint __unused *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
id<MASConstraintDelegate> strongDelegate = self.delegate;
MASConstraint *newConstraint = [strongDelegate constraint:self addConstraintWithLayoutAttribute:layoutAttribute];
newConstraint.delegate = self;
[self.childConstraints addObject:newConstraint];
return newConstraint;
}
#pragma mark - NSLayoutConstraint multiplier proxies
- (MASConstraint * (^)(CGFloat))multipliedBy {
return ^id(CGFloat multiplier) {
for (MASConstraint *constraint in self.childConstraints) {
constraint.multipliedBy(multiplier);
}
return self;
};
}
- (MASConstraint * (^)(CGFloat))dividedBy {
return ^id(CGFloat divider) {
for (MASConstraint *constraint in self.childConstraints) {
constraint.dividedBy(divider);
}
return self;
};
}
#pragma mark - MASLayoutPriority proxy
- (MASConstraint * (^)(MASLayoutPriority))priority {
return ^id(MASLayoutPriority priority) {
for (MASConstraint *constraint in self.childConstraints) {
constraint.priority(priority);
}
return self;
};
}
#pragma mark - NSLayoutRelation proxy
- (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation {
return ^id(id attr, NSLayoutRelation relation) {
for (MASConstraint *constraint in self.childConstraints.copy) {
constraint.equalToWithRelation(attr, relation);
}
return self;
};
}
#pragma mark - attribute chaining
- (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
[self constraint:self addConstraintWithLayoutAttribute:layoutAttribute];
return self;
}
#pragma mark - Animator proxy
#if TARGET_OS_MAC && !(TARGET_OS_IPHONE || TARGET_OS_TV)
- (MASConstraint *)animator {
for (MASConstraint *constraint in self.childConstraints) {
[constraint animator];
}
return self;
}
#endif
#pragma mark - debug helpers
- (MASConstraint * (^)(id))key {
return ^id(id key) {
self.mas_key = key;
int i = 0;
for (MASConstraint *constraint in self.childConstraints) {
constraint.key([NSString stringWithFormat:@"%@[%d]", key, i++]);
}
return self;
};
}
#pragma mark - NSLayoutConstraint constant setters
- (void)setInsets:(MASEdgeInsets)insets {
for (MASConstraint *constraint in self.childConstraints) {
constraint.insets = insets;
}
}
- (void)setInset:(CGFloat)inset {
for (MASConstraint *constraint in self.childConstraints) {
constraint.inset = inset;
}
}
- (void)setOffset:(CGFloat)offset {
for (MASConstraint *constraint in self.childConstraints) {
constraint.offset = offset;
}
}
- (void)setSizeOffset:(CGSize)sizeOffset {
for (MASConstraint *constraint in self.childConstraints) {
constraint.sizeOffset = sizeOffset;
}
}
- (void)setCenterOffset:(CGPoint)centerOffset {
for (MASConstraint *constraint in self.childConstraints) {
constraint.centerOffset = centerOffset;
}
}
#pragma mark - MASConstraint
- (void)activate {
for (MASConstraint *constraint in self.childConstraints) {
[constraint activate];
}
}
- (void)deactivate {
for (MASConstraint *constraint in self.childConstraints) {
[constraint deactivate];
}
}
- (void)install {
for (MASConstraint *constraint in self.childConstraints) {
constraint.updateExisting = self.updateExisting;
[constraint install];
}
}
- (void)uninstall {
for (MASConstraint *constraint in self.childConstraints) {
[constraint uninstall];
}
}
@end

View File

@@ -1,66 +0,0 @@
//
// MASConstraint+Private.h
// Masonry
//
// Created by Nick Tymchenko on 29/04/14.
// Copyright (c) 2014 cloudling. All rights reserved.
//
#import "MASConstraint.h"
@protocol MASConstraintDelegate;
@interface MASConstraint ()
/**
* Whether or not to check for an existing constraint instead of adding constraint
*/
@property (nonatomic, assign) BOOL updateExisting;
/**
* Usually MASConstraintMaker but could be a parent MASConstraint
*/
@property (nonatomic, weak) id<MASConstraintDelegate> delegate;
/**
* Based on a provided value type, is equal to calling:
* NSNumber - setOffset:
* NSValue with CGPoint - setPointOffset:
* NSValue with CGSize - setSizeOffset:
* NSValue with MASEdgeInsets - setInsets:
*/
- (void)setLayoutConstantWithValue:(NSValue *)value;
@end
@interface MASConstraint (Abstract)
/**
* Sets the constraint relation to given NSLayoutRelation
* returns a block which accepts one of the following:
* MASViewAttribute, UIView, NSValue, NSArray
* see readme for more details.
*/
- (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation;
/**
* Override to set a custom chaining behaviour
*/
- (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute;
@end
@protocol MASConstraintDelegate <NSObject>
/**
* Notifies the delegate when the constraint needs to be replaced with another constraint. For example
* A MASViewConstraint may turn into a MASCompositeConstraint when an array is passed to one of the equality blocks
*/
- (void)constraint:(MASConstraint *)constraint shouldBeReplacedWithConstraint:(MASConstraint *)replacementConstraint;
- (MASConstraint *)constraint:(MASConstraint *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute;
@end

View File

@@ -1,272 +0,0 @@
//
// MASConstraint.h
// Masonry
//
// Created by Jonas Budelmann on 22/07/13.
// Copyright (c) 2013 cloudling. All rights reserved.
//
#import "MASUtilities.h"
/**
* Enables Constraints to be created with chainable syntax
* Constraint can represent single NSLayoutConstraint (MASViewConstraint)
* or a group of NSLayoutConstraints (MASComposisteConstraint)
*/
@interface MASConstraint : NSObject
// Chaining Support
/**
* Modifies the NSLayoutConstraint constant,
* only affects MASConstraints in which the first item's NSLayoutAttribute is one of the following
* NSLayoutAttributeTop, NSLayoutAttributeLeft, NSLayoutAttributeBottom, NSLayoutAttributeRight
*/
- (MASConstraint * (^)(MASEdgeInsets insets))insets;
/**
* Modifies the NSLayoutConstraint constant,
* only affects MASConstraints in which the first item's NSLayoutAttribute is one of the following
* NSLayoutAttributeTop, NSLayoutAttributeLeft, NSLayoutAttributeBottom, NSLayoutAttributeRight
*/
- (MASConstraint * (^)(CGFloat inset))inset;
/**
* Modifies the NSLayoutConstraint constant,
* only affects MASConstraints in which the first item's NSLayoutAttribute is one of the following
* NSLayoutAttributeWidth, NSLayoutAttributeHeight
*/
- (MASConstraint * (^)(CGSize offset))sizeOffset;
/**
* Modifies the NSLayoutConstraint constant,
* only affects MASConstraints in which the first item's NSLayoutAttribute is one of the following
* NSLayoutAttributeCenterX, NSLayoutAttributeCenterY
*/
- (MASConstraint * (^)(CGPoint offset))centerOffset;
/**
* Modifies the NSLayoutConstraint constant
*/
- (MASConstraint * (^)(CGFloat offset))offset;
/**
* Modifies the NSLayoutConstraint constant based on a value type
*/
- (MASConstraint * (^)(NSValue *value))valueOffset;
/**
* Sets the NSLayoutConstraint multiplier property
*/
- (MASConstraint * (^)(CGFloat multiplier))multipliedBy;
/**
* Sets the NSLayoutConstraint multiplier to 1.0/dividedBy
*/
- (MASConstraint * (^)(CGFloat divider))dividedBy;
/**
* Sets the NSLayoutConstraint priority to a float or MASLayoutPriority
*/
- (MASConstraint * (^)(MASLayoutPriority priority))priority;
/**
* Sets the NSLayoutConstraint priority to MASLayoutPriorityLow
*/
- (MASConstraint * (^)(void))priorityLow;
/**
* Sets the NSLayoutConstraint priority to MASLayoutPriorityMedium
*/
- (MASConstraint * (^)(void))priorityMedium;
/**
* Sets the NSLayoutConstraint priority to MASLayoutPriorityHigh
*/
- (MASConstraint * (^)(void))priorityHigh;
/**
* Sets the constraint relation to NSLayoutRelationEqual
* returns a block which accepts one of the following:
* MASViewAttribute, UIView, NSValue, NSArray
* see readme for more details.
*/
- (MASConstraint * (^)(id attr))equalTo;
/**
* Sets the constraint relation to NSLayoutRelationGreaterThanOrEqual
* returns a block which accepts one of the following:
* MASViewAttribute, UIView, NSValue, NSArray
* see readme for more details.
*/
- (MASConstraint * (^)(id attr))greaterThanOrEqualTo;
/**
* Sets the constraint relation to NSLayoutRelationLessThanOrEqual
* returns a block which accepts one of the following:
* MASViewAttribute, UIView, NSValue, NSArray
* see readme for more details.
*/
- (MASConstraint * (^)(id attr))lessThanOrEqualTo;
/**
* Optional semantic property which has no effect but improves the readability of constraint
*/
- (MASConstraint *)with;
/**
* Optional semantic property which has no effect but improves the readability of constraint
*/
- (MASConstraint *)and;
/**
* Creates a new MASCompositeConstraint with the called attribute and reciever
*/
- (MASConstraint *)left;
- (MASConstraint *)top;
- (MASConstraint *)right;
- (MASConstraint *)bottom;
- (MASConstraint *)leading;
- (MASConstraint *)trailing;
- (MASConstraint *)width;
- (MASConstraint *)height;
- (MASConstraint *)centerX;
- (MASConstraint *)centerY;
- (MASConstraint *)baseline;
#if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000) || (__MAC_OS_X_VERSION_MIN_REQUIRED >= 101100)
- (MASConstraint *)firstBaseline;
- (MASConstraint *)lastBaseline;
#endif
#if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000)
- (MASConstraint *)leftMargin;
- (MASConstraint *)rightMargin;
- (MASConstraint *)topMargin;
- (MASConstraint *)bottomMargin;
- (MASConstraint *)leadingMargin;
- (MASConstraint *)trailingMargin;
- (MASConstraint *)centerXWithinMargins;
- (MASConstraint *)centerYWithinMargins;
#endif
/**
* Sets the constraint debug name
*/
- (MASConstraint * (^)(id key))key;
// NSLayoutConstraint constant Setters
// for use outside of mas_updateConstraints/mas_makeConstraints blocks
/**
* Modifies the NSLayoutConstraint constant,
* only affects MASConstraints in which the first item's NSLayoutAttribute is one of the following
* NSLayoutAttributeTop, NSLayoutAttributeLeft, NSLayoutAttributeBottom, NSLayoutAttributeRight
*/
- (void)setInsets:(MASEdgeInsets)insets;
/**
* Modifies the NSLayoutConstraint constant,
* only affects MASConstraints in which the first item's NSLayoutAttribute is one of the following
* NSLayoutAttributeTop, NSLayoutAttributeLeft, NSLayoutAttributeBottom, NSLayoutAttributeRight
*/
- (void)setInset:(CGFloat)inset;
/**
* Modifies the NSLayoutConstraint constant,
* only affects MASConstraints in which the first item's NSLayoutAttribute is one of the following
* NSLayoutAttributeWidth, NSLayoutAttributeHeight
*/
- (void)setSizeOffset:(CGSize)sizeOffset;
/**
* Modifies the NSLayoutConstraint constant,
* only affects MASConstraints in which the first item's NSLayoutAttribute is one of the following
* NSLayoutAttributeCenterX, NSLayoutAttributeCenterY
*/
- (void)setCenterOffset:(CGPoint)centerOffset;
/**
* Modifies the NSLayoutConstraint constant
*/
- (void)setOffset:(CGFloat)offset;
// NSLayoutConstraint Installation support
#if TARGET_OS_MAC && !(TARGET_OS_IPHONE || TARGET_OS_TV)
/**
* Whether or not to go through the animator proxy when modifying the constraint
*/
@property (nonatomic, copy, readonly) MASConstraint *animator;
#endif
/**
* Activates an NSLayoutConstraint if it's supported by an OS.
* Invokes install otherwise.
*/
- (void)activate;
/**
* Deactivates previously installed/activated NSLayoutConstraint.
*/
- (void)deactivate;
/**
* Creates a NSLayoutConstraint and adds it to the appropriate view.
*/
- (void)install;
/**
* Removes previously installed NSLayoutConstraint
*/
- (void)uninstall;
@end
/**
* Convenience auto-boxing macros for MASConstraint methods.
*
* Defining MAS_SHORTHAND_GLOBALS will turn on auto-boxing for default syntax.
* A potential drawback of this is that the unprefixed macros will appear in global scope.
*/
#define mas_equalTo(...) equalTo(MASBoxValue((__VA_ARGS__)))
#define mas_greaterThanOrEqualTo(...) greaterThanOrEqualTo(MASBoxValue((__VA_ARGS__)))
#define mas_lessThanOrEqualTo(...) lessThanOrEqualTo(MASBoxValue((__VA_ARGS__)))
#define mas_offset(...) valueOffset(MASBoxValue((__VA_ARGS__)))
#ifdef MAS_SHORTHAND_GLOBALS
#define equalTo(...) mas_equalTo(__VA_ARGS__)
#define greaterThanOrEqualTo(...) mas_greaterThanOrEqualTo(__VA_ARGS__)
#define lessThanOrEqualTo(...) mas_lessThanOrEqualTo(__VA_ARGS__)
#define offset(...) mas_offset(__VA_ARGS__)
#endif
@interface MASConstraint (AutoboxingSupport)
/**
* Aliases to corresponding relation methods (for shorthand macros)
* Also needed to aid autocompletion
*/
- (MASConstraint * (^)(id attr))mas_equalTo;
- (MASConstraint * (^)(id attr))mas_greaterThanOrEqualTo;
- (MASConstraint * (^)(id attr))mas_lessThanOrEqualTo;
/**
* A dummy method to aid autocompletion
*/
- (MASConstraint * (^)(id offset))mas_offset;
@end

View File

@@ -1,301 +0,0 @@
//
// MASConstraint.m
// Masonry
//
// Created by Nick Tymchenko on 1/20/14.
//
#import "MASConstraint.h"
#import "MASConstraint+Private.h"
#define MASMethodNotImplemented() \
@throw [NSException exceptionWithName:NSInternalInconsistencyException \
reason:[NSString stringWithFormat:@"You must override %@ in a subclass.", NSStringFromSelector(_cmd)] \
userInfo:nil]
@implementation MASConstraint
#pragma mark - Init
- (id)init {
NSAssert(![self isMemberOfClass:[MASConstraint class]], @"MASConstraint is an abstract class, you should not instantiate it directly.");
return [super init];
}
#pragma mark - NSLayoutRelation proxies
- (MASConstraint * (^)(id))equalTo {
return ^id(id attribute) {
return self.equalToWithRelation(attribute, NSLayoutRelationEqual);
};
}
- (MASConstraint * (^)(id))mas_equalTo {
return ^id(id attribute) {
return self.equalToWithRelation(attribute, NSLayoutRelationEqual);
};
}
- (MASConstraint * (^)(id))greaterThanOrEqualTo {
return ^id(id attribute) {
return self.equalToWithRelation(attribute, NSLayoutRelationGreaterThanOrEqual);
};
}
- (MASConstraint * (^)(id))mas_greaterThanOrEqualTo {
return ^id(id attribute) {
return self.equalToWithRelation(attribute, NSLayoutRelationGreaterThanOrEqual);
};
}
- (MASConstraint * (^)(id))lessThanOrEqualTo {
return ^id(id attribute) {
return self.equalToWithRelation(attribute, NSLayoutRelationLessThanOrEqual);
};
}
- (MASConstraint * (^)(id))mas_lessThanOrEqualTo {
return ^id(id attribute) {
return self.equalToWithRelation(attribute, NSLayoutRelationLessThanOrEqual);
};
}
#pragma mark - MASLayoutPriority proxies
- (MASConstraint * (^)(void))priorityLow {
return ^id{
self.priority(MASLayoutPriorityDefaultLow);
return self;
};
}
- (MASConstraint * (^)(void))priorityMedium {
return ^id{
self.priority(MASLayoutPriorityDefaultMedium);
return self;
};
}
- (MASConstraint * (^)(void))priorityHigh {
return ^id{
self.priority(MASLayoutPriorityDefaultHigh);
return self;
};
}
#pragma mark - NSLayoutConstraint constant proxies
- (MASConstraint * (^)(MASEdgeInsets))insets {
return ^id(MASEdgeInsets insets){
self.insets = insets;
return self;
};
}
- (MASConstraint * (^)(CGFloat))inset {
return ^id(CGFloat inset){
self.inset = inset;
return self;
};
}
- (MASConstraint * (^)(CGSize))sizeOffset {
return ^id(CGSize offset) {
self.sizeOffset = offset;
return self;
};
}
- (MASConstraint * (^)(CGPoint))centerOffset {
return ^id(CGPoint offset) {
self.centerOffset = offset;
return self;
};
}
- (MASConstraint * (^)(CGFloat))offset {
return ^id(CGFloat offset){
self.offset = offset;
return self;
};
}
- (MASConstraint * (^)(NSValue *value))valueOffset {
return ^id(NSValue *offset) {
NSAssert([offset isKindOfClass:NSValue.class], @"expected an NSValue offset, got: %@", offset);
[self setLayoutConstantWithValue:offset];
return self;
};
}
- (MASConstraint * (^)(id offset))mas_offset {
// Will never be called due to macro
return nil;
}
#pragma mark - NSLayoutConstraint constant setter
- (void)setLayoutConstantWithValue:(NSValue *)value {
if ([value isKindOfClass:NSNumber.class]) {
self.offset = [(NSNumber *)value doubleValue];
} else if (strcmp(value.objCType, @encode(CGPoint)) == 0) {
CGPoint point;
[value getValue:&point];
self.centerOffset = point;
} else if (strcmp(value.objCType, @encode(CGSize)) == 0) {
CGSize size;
[value getValue:&size];
self.sizeOffset = size;
} else if (strcmp(value.objCType, @encode(MASEdgeInsets)) == 0) {
MASEdgeInsets insets;
[value getValue:&insets];
self.insets = insets;
} else {
NSAssert(NO, @"attempting to set layout constant with unsupported value: %@", value);
}
}
#pragma mark - Semantic properties
- (MASConstraint *)with {
return self;
}
- (MASConstraint *)and {
return self;
}
#pragma mark - Chaining
- (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute __unused)layoutAttribute {
MASMethodNotImplemented();
}
- (MASConstraint *)left {
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeft];
}
- (MASConstraint *)top {
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeTop];
}
- (MASConstraint *)right {
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeRight];
}
- (MASConstraint *)bottom {
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeBottom];
}
- (MASConstraint *)leading {
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeading];
}
- (MASConstraint *)trailing {
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeTrailing];
}
- (MASConstraint *)width {
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeWidth];
}
- (MASConstraint *)height {
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeHeight];
}
- (MASConstraint *)centerX {
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeCenterX];
}
- (MASConstraint *)centerY {
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeCenterY];
}
- (MASConstraint *)baseline {
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeBaseline];
}
#if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000) || (__MAC_OS_X_VERSION_MIN_REQUIRED >= 101100)
- (MASConstraint *)firstBaseline {
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeFirstBaseline];
}
- (MASConstraint *)lastBaseline {
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLastBaseline];
}
#endif
#if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000)
- (MASConstraint *)leftMargin {
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeftMargin];
}
- (MASConstraint *)rightMargin {
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeRightMargin];
}
- (MASConstraint *)topMargin {
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeTopMargin];
}
- (MASConstraint *)bottomMargin {
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeBottomMargin];
}
- (MASConstraint *)leadingMargin {
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeadingMargin];
}
- (MASConstraint *)trailingMargin {
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeTrailingMargin];
}
- (MASConstraint *)centerXWithinMargins {
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeCenterXWithinMargins];
}
- (MASConstraint *)centerYWithinMargins {
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeCenterYWithinMargins];
}
#endif
#pragma mark - Abstract
- (MASConstraint * (^)(CGFloat multiplier))multipliedBy { MASMethodNotImplemented(); }
- (MASConstraint * (^)(CGFloat divider))dividedBy { MASMethodNotImplemented(); }
- (MASConstraint * (^)(MASLayoutPriority priority))priority { MASMethodNotImplemented(); }
- (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation { MASMethodNotImplemented(); }
- (MASConstraint * (^)(id key))key { MASMethodNotImplemented(); }
- (void)setInsets:(MASEdgeInsets __unused)insets { MASMethodNotImplemented(); }
- (void)setInset:(CGFloat __unused)inset { MASMethodNotImplemented(); }
- (void)setSizeOffset:(CGSize __unused)sizeOffset { MASMethodNotImplemented(); }
- (void)setCenterOffset:(CGPoint __unused)centerOffset { MASMethodNotImplemented(); }
- (void)setOffset:(CGFloat __unused)offset { MASMethodNotImplemented(); }
#if TARGET_OS_MAC && !(TARGET_OS_IPHONE || TARGET_OS_TV)
- (MASConstraint *)animator { MASMethodNotImplemented(); }
#endif
- (void)activate { MASMethodNotImplemented(); }
- (void)deactivate { MASMethodNotImplemented(); }
- (void)install { MASMethodNotImplemented(); }
- (void)uninstall { MASMethodNotImplemented(); }
@end

View File

@@ -1,146 +0,0 @@
//
// MASConstraintMaker.h
// Masonry
//
// Created by Jonas Budelmann on 20/07/13.
// Copyright (c) 2013 cloudling. All rights reserved.
//
#import "MASConstraint.h"
#import "MASUtilities.h"
typedef NS_OPTIONS(NSInteger, MASAttribute) {
MASAttributeLeft = 1 << NSLayoutAttributeLeft,
MASAttributeRight = 1 << NSLayoutAttributeRight,
MASAttributeTop = 1 << NSLayoutAttributeTop,
MASAttributeBottom = 1 << NSLayoutAttributeBottom,
MASAttributeLeading = 1 << NSLayoutAttributeLeading,
MASAttributeTrailing = 1 << NSLayoutAttributeTrailing,
MASAttributeWidth = 1 << NSLayoutAttributeWidth,
MASAttributeHeight = 1 << NSLayoutAttributeHeight,
MASAttributeCenterX = 1 << NSLayoutAttributeCenterX,
MASAttributeCenterY = 1 << NSLayoutAttributeCenterY,
MASAttributeBaseline = 1 << NSLayoutAttributeBaseline,
#if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000) || (__MAC_OS_X_VERSION_MIN_REQUIRED >= 101100)
MASAttributeFirstBaseline = 1 << NSLayoutAttributeFirstBaseline,
MASAttributeLastBaseline = 1 << NSLayoutAttributeLastBaseline,
#endif
#if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000)
MASAttributeLeftMargin = 1 << NSLayoutAttributeLeftMargin,
MASAttributeRightMargin = 1 << NSLayoutAttributeRightMargin,
MASAttributeTopMargin = 1 << NSLayoutAttributeTopMargin,
MASAttributeBottomMargin = 1 << NSLayoutAttributeBottomMargin,
MASAttributeLeadingMargin = 1 << NSLayoutAttributeLeadingMargin,
MASAttributeTrailingMargin = 1 << NSLayoutAttributeTrailingMargin,
MASAttributeCenterXWithinMargins = 1 << NSLayoutAttributeCenterXWithinMargins,
MASAttributeCenterYWithinMargins = 1 << NSLayoutAttributeCenterYWithinMargins,
#endif
};
/**
* Provides factory methods for creating MASConstraints.
* Constraints are collected until they are ready to be installed
*
*/
@interface MASConstraintMaker : NSObject
/**
* The following properties return a new MASViewConstraint
* with the first item set to the makers associated view and the appropriate MASViewAttribute
*/
@property (nonatomic, strong, readonly) MASConstraint *left;
@property (nonatomic, strong, readonly) MASConstraint *top;
@property (nonatomic, strong, readonly) MASConstraint *right;
@property (nonatomic, strong, readonly) MASConstraint *bottom;
@property (nonatomic, strong, readonly) MASConstraint *leading;
@property (nonatomic, strong, readonly) MASConstraint *trailing;
@property (nonatomic, strong, readonly) MASConstraint *width;
@property (nonatomic, strong, readonly) MASConstraint *height;
@property (nonatomic, strong, readonly) MASConstraint *centerX;
@property (nonatomic, strong, readonly) MASConstraint *centerY;
@property (nonatomic, strong, readonly) MASConstraint *baseline;
#if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000) || (__MAC_OS_X_VERSION_MIN_REQUIRED >= 101100)
@property (nonatomic, strong, readonly) MASConstraint *firstBaseline;
@property (nonatomic, strong, readonly) MASConstraint *lastBaseline;
#endif
#if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000)
@property (nonatomic, strong, readonly) MASConstraint *leftMargin;
@property (nonatomic, strong, readonly) MASConstraint *rightMargin;
@property (nonatomic, strong, readonly) MASConstraint *topMargin;
@property (nonatomic, strong, readonly) MASConstraint *bottomMargin;
@property (nonatomic, strong, readonly) MASConstraint *leadingMargin;
@property (nonatomic, strong, readonly) MASConstraint *trailingMargin;
@property (nonatomic, strong, readonly) MASConstraint *centerXWithinMargins;
@property (nonatomic, strong, readonly) MASConstraint *centerYWithinMargins;
#endif
/**
* Returns a block which creates a new MASCompositeConstraint with the first item set
* to the makers associated view and children corresponding to the set bits in the
* MASAttribute parameter. Combine multiple attributes via binary-or.
*/
@property (nonatomic, strong, readonly) MASConstraint *(^attributes)(MASAttribute attrs);
/**
* Creates a MASCompositeConstraint with type MASCompositeConstraintTypeEdges
* which generates the appropriate MASViewConstraint children (top, left, bottom, right)
* with the first item set to the makers associated view
*/
@property (nonatomic, strong, readonly) MASConstraint *edges;
/**
* Creates a MASCompositeConstraint with type MASCompositeConstraintTypeSize
* which generates the appropriate MASViewConstraint children (width, height)
* with the first item set to the makers associated view
*/
@property (nonatomic, strong, readonly) MASConstraint *size;
/**
* Creates a MASCompositeConstraint with type MASCompositeConstraintTypeCenter
* which generates the appropriate MASViewConstraint children (centerX, centerY)
* with the first item set to the makers associated view
*/
@property (nonatomic, strong, readonly) MASConstraint *center;
/**
* Whether or not to check for an existing constraint instead of adding constraint
*/
@property (nonatomic, assign) BOOL updateExisting;
/**
* Whether or not to remove existing constraints prior to installing
*/
@property (nonatomic, assign) BOOL removeExisting;
/**
* initialises the maker with a default view
*
* @param view any MASConstraint are created with this view as the first item
*
* @return a new MASConstraintMaker
*/
- (id)initWithView:(MAS_VIEW *)view;
/**
* Calls install method on any MASConstraints which have been created by this maker
*
* @return an array of all the installed MASConstraints
*/
- (NSArray *)install;
- (MASConstraint * (^)(dispatch_block_t))group;
@end

View File

@@ -1,273 +0,0 @@
//
// MASConstraintMaker.m
// Masonry
//
// Created by Jonas Budelmann on 20/07/13.
// Copyright (c) 2013 cloudling. All rights reserved.
//
#import "MASConstraintMaker.h"
#import "MASViewConstraint.h"
#import "MASCompositeConstraint.h"
#import "MASConstraint+Private.h"
#import "MASViewAttribute.h"
#import "View+MASAdditions.h"
@interface MASConstraintMaker () <MASConstraintDelegate>
@property (nonatomic, weak) MAS_VIEW *view;
@property (nonatomic, strong) NSMutableArray *constraints;
@end
@implementation MASConstraintMaker
- (id)initWithView:(MAS_VIEW *)view {
self = [super init];
if (!self) return nil;
self.view = view;
self.constraints = NSMutableArray.new;
return self;
}
- (NSArray *)install {
if (self.removeExisting) {
NSArray *installedConstraints = [MASViewConstraint installedConstraintsForView:self.view];
for (MASConstraint *constraint in installedConstraints) {
[constraint uninstall];
}
}
NSArray *constraints = self.constraints.copy;
for (MASConstraint *constraint in constraints) {
constraint.updateExisting = self.updateExisting;
[constraint install];
}
[self.constraints removeAllObjects];
return constraints;
}
#pragma mark - MASConstraintDelegate
- (void)constraint:(MASConstraint *)constraint shouldBeReplacedWithConstraint:(MASConstraint *)replacementConstraint {
NSUInteger index = [self.constraints indexOfObject:constraint];
NSAssert(index != NSNotFound, @"Could not find constraint %@", constraint);
[self.constraints replaceObjectAtIndex:index withObject:replacementConstraint];
}
- (MASConstraint *)constraint:(MASConstraint *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
MASViewAttribute *viewAttribute = [[MASViewAttribute alloc] initWithView:self.view layoutAttribute:layoutAttribute];
MASViewConstraint *newConstraint = [[MASViewConstraint alloc] initWithFirstViewAttribute:viewAttribute];
if ([constraint isKindOfClass:MASViewConstraint.class]) {
//replace with composite constraint
NSArray *children = @[constraint, newConstraint];
MASCompositeConstraint *compositeConstraint = [[MASCompositeConstraint alloc] initWithChildren:children];
compositeConstraint.delegate = self;
[self constraint:constraint shouldBeReplacedWithConstraint:compositeConstraint];
return compositeConstraint;
}
if (!constraint) {
newConstraint.delegate = self;
[self.constraints addObject:newConstraint];
}
return newConstraint;
}
- (MASConstraint *)addConstraintWithAttributes:(MASAttribute)attrs {
__unused MASAttribute anyAttribute = (MASAttributeLeft | MASAttributeRight | MASAttributeTop | MASAttributeBottom | MASAttributeLeading
| MASAttributeTrailing | MASAttributeWidth | MASAttributeHeight | MASAttributeCenterX
| MASAttributeCenterY | MASAttributeBaseline
#if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000) || (__MAC_OS_X_VERSION_MIN_REQUIRED >= 101100)
| MASAttributeFirstBaseline | MASAttributeLastBaseline
#endif
#if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000)
| MASAttributeLeftMargin | MASAttributeRightMargin | MASAttributeTopMargin | MASAttributeBottomMargin
| MASAttributeLeadingMargin | MASAttributeTrailingMargin | MASAttributeCenterXWithinMargins
| MASAttributeCenterYWithinMargins
#endif
);
NSAssert((attrs & anyAttribute) != 0, @"You didn't pass any attribute to make.attributes(...)");
NSMutableArray *attributes = [NSMutableArray array];
if (attrs & MASAttributeLeft) [attributes addObject:self.view.mas_left];
if (attrs & MASAttributeRight) [attributes addObject:self.view.mas_right];
if (attrs & MASAttributeTop) [attributes addObject:self.view.mas_top];
if (attrs & MASAttributeBottom) [attributes addObject:self.view.mas_bottom];
if (attrs & MASAttributeLeading) [attributes addObject:self.view.mas_leading];
if (attrs & MASAttributeTrailing) [attributes addObject:self.view.mas_trailing];
if (attrs & MASAttributeWidth) [attributes addObject:self.view.mas_width];
if (attrs & MASAttributeHeight) [attributes addObject:self.view.mas_height];
if (attrs & MASAttributeCenterX) [attributes addObject:self.view.mas_centerX];
if (attrs & MASAttributeCenterY) [attributes addObject:self.view.mas_centerY];
if (attrs & MASAttributeBaseline) [attributes addObject:self.view.mas_baseline];
#if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000) || (__MAC_OS_X_VERSION_MIN_REQUIRED >= 101100)
if (attrs & MASAttributeFirstBaseline) [attributes addObject:self.view.mas_firstBaseline];
if (attrs & MASAttributeLastBaseline) [attributes addObject:self.view.mas_lastBaseline];
#endif
#if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000)
if (attrs & MASAttributeLeftMargin) [attributes addObject:self.view.mas_leftMargin];
if (attrs & MASAttributeRightMargin) [attributes addObject:self.view.mas_rightMargin];
if (attrs & MASAttributeTopMargin) [attributes addObject:self.view.mas_topMargin];
if (attrs & MASAttributeBottomMargin) [attributes addObject:self.view.mas_bottomMargin];
if (attrs & MASAttributeLeadingMargin) [attributes addObject:self.view.mas_leadingMargin];
if (attrs & MASAttributeTrailingMargin) [attributes addObject:self.view.mas_trailingMargin];
if (attrs & MASAttributeCenterXWithinMargins) [attributes addObject:self.view.mas_centerXWithinMargins];
if (attrs & MASAttributeCenterYWithinMargins) [attributes addObject:self.view.mas_centerYWithinMargins];
#endif
NSMutableArray *children = [NSMutableArray arrayWithCapacity:attributes.count];
for (MASViewAttribute *a in attributes) {
[children addObject:[[MASViewConstraint alloc] initWithFirstViewAttribute:a]];
}
MASCompositeConstraint *constraint = [[MASCompositeConstraint alloc] initWithChildren:children];
constraint.delegate = self;
[self.constraints addObject:constraint];
return constraint;
}
#pragma mark - standard Attributes
- (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
return [self constraint:nil addConstraintWithLayoutAttribute:layoutAttribute];
}
- (MASConstraint *)left {
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeft];
}
- (MASConstraint *)top {
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeTop];
}
- (MASConstraint *)right {
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeRight];
}
- (MASConstraint *)bottom {
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeBottom];
}
- (MASConstraint *)leading {
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeading];
}
- (MASConstraint *)trailing {
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeTrailing];
}
- (MASConstraint *)width {
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeWidth];
}
- (MASConstraint *)height {
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeHeight];
}
- (MASConstraint *)centerX {
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeCenterX];
}
- (MASConstraint *)centerY {
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeCenterY];
}
- (MASConstraint *)baseline {
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeBaseline];
}
- (MASConstraint *(^)(MASAttribute))attributes {
return ^(MASAttribute attrs){
return [self addConstraintWithAttributes:attrs];
};
}
#if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000) || (__MAC_OS_X_VERSION_MIN_REQUIRED >= 101100)
- (MASConstraint *)firstBaseline {
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeFirstBaseline];
}
- (MASConstraint *)lastBaseline {
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLastBaseline];
}
#endif
#if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000)
- (MASConstraint *)leftMargin {
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeftMargin];
}
- (MASConstraint *)rightMargin {
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeRightMargin];
}
- (MASConstraint *)topMargin {
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeTopMargin];
}
- (MASConstraint *)bottomMargin {
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeBottomMargin];
}
- (MASConstraint *)leadingMargin {
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeadingMargin];
}
- (MASConstraint *)trailingMargin {
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeTrailingMargin];
}
- (MASConstraint *)centerXWithinMargins {
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeCenterXWithinMargins];
}
- (MASConstraint *)centerYWithinMargins {
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeCenterYWithinMargins];
}
#endif
#pragma mark - composite Attributes
- (MASConstraint *)edges {
return [self addConstraintWithAttributes:MASAttributeTop | MASAttributeLeft | MASAttributeRight | MASAttributeBottom];
}
- (MASConstraint *)size {
return [self addConstraintWithAttributes:MASAttributeWidth | MASAttributeHeight];
}
- (MASConstraint *)center {
return [self addConstraintWithAttributes:MASAttributeCenterX | MASAttributeCenterY];
}
#pragma mark - grouping
- (MASConstraint *(^)(dispatch_block_t group))group {
return ^id(dispatch_block_t group) {
NSInteger previousCount = self.constraints.count;
group();
NSArray *children = [self.constraints subarrayWithRange:NSMakeRange(previousCount, self.constraints.count - previousCount)];
MASCompositeConstraint *constraint = [[MASCompositeConstraint alloc] initWithChildren:children];
constraint.delegate = self;
return constraint;
};
}
@end

View File

@@ -1,22 +0,0 @@
//
// MASLayoutConstraint.h
// Masonry
//
// Created by Jonas Budelmann on 3/08/13.
// Copyright (c) 2013 Jonas Budelmann. All rights reserved.
//
#import "MASUtilities.h"
/**
* When you are debugging or printing the constraints attached to a view this subclass
* makes it easier to identify which constraints have been created via Masonry
*/
@interface MASLayoutConstraint : NSLayoutConstraint
/**
* a key to associate with this constraint
*/
@property (nonatomic, strong) id mas_key;
@end

View File

@@ -1,13 +0,0 @@
//
// MASLayoutConstraint.m
// Masonry
//
// Created by Jonas Budelmann on 3/08/13.
// Copyright (c) 2013 Jonas Budelmann. All rights reserved.
//
#import "MASLayoutConstraint.h"
@implementation MASLayoutConstraint
@end

View File

@@ -1,136 +0,0 @@
//
// MASUtilities.h
// Masonry
//
// Created by Jonas Budelmann on 19/08/13.
// Copyright (c) 2013 Jonas Budelmann. All rights reserved.
//
#import <Foundation/Foundation.h>
#if TARGET_OS_IPHONE || TARGET_OS_TV
#import <UIKit/UIKit.h>
#define MAS_VIEW UIView
#define MAS_VIEW_CONTROLLER UIViewController
#define MASEdgeInsets UIEdgeInsets
typedef UILayoutPriority MASLayoutPriority;
static const MASLayoutPriority MASLayoutPriorityRequired = UILayoutPriorityRequired;
static const MASLayoutPriority MASLayoutPriorityDefaultHigh = UILayoutPriorityDefaultHigh;
static const MASLayoutPriority MASLayoutPriorityDefaultMedium = 500;
static const MASLayoutPriority MASLayoutPriorityDefaultLow = UILayoutPriorityDefaultLow;
static const MASLayoutPriority MASLayoutPriorityFittingSizeLevel = UILayoutPriorityFittingSizeLevel;
#elif TARGET_OS_MAC
#import <AppKit/AppKit.h>
#define MAS_VIEW NSView
#define MASEdgeInsets NSEdgeInsets
typedef NSLayoutPriority MASLayoutPriority;
static const MASLayoutPriority MASLayoutPriorityRequired = NSLayoutPriorityRequired;
static const MASLayoutPriority MASLayoutPriorityDefaultHigh = NSLayoutPriorityDefaultHigh;
static const MASLayoutPriority MASLayoutPriorityDragThatCanResizeWindow = NSLayoutPriorityDragThatCanResizeWindow;
static const MASLayoutPriority MASLayoutPriorityDefaultMedium = 501;
static const MASLayoutPriority MASLayoutPriorityWindowSizeStayPut = NSLayoutPriorityWindowSizeStayPut;
static const MASLayoutPriority MASLayoutPriorityDragThatCannotResizeWindow = NSLayoutPriorityDragThatCannotResizeWindow;
static const MASLayoutPriority MASLayoutPriorityDefaultLow = NSLayoutPriorityDefaultLow;
static const MASLayoutPriority MASLayoutPriorityFittingSizeCompression = NSLayoutPriorityFittingSizeCompression;
#endif
/**
* Allows you to attach keys to objects matching the variable names passed.
*
* view1.mas_key = @"view1", view2.mas_key = @"view2";
*
* is equivalent to:
*
* MASAttachKeys(view1, view2);
*/
#define MASAttachKeys(...) \
{ \
NSDictionary *keyPairs = NSDictionaryOfVariableBindings(__VA_ARGS__); \
for (id key in keyPairs.allKeys) { \
id obj = keyPairs[key]; \
NSAssert([obj respondsToSelector:@selector(setMas_key:)], \
@"Cannot attach mas_key to %@", obj); \
[obj setMas_key:key]; \
} \
}
/**
* Used to create object hashes
* Based on http://www.mikeash.com/pyblog/friday-qa-2010-06-18-implementing-equality-and-hashing.html
*/
#define MAS_NSUINT_BIT (CHAR_BIT * sizeof(NSUInteger))
#define MAS_NSUINTROTATE(val, howmuch) ((((NSUInteger)val) << howmuch) | (((NSUInteger)val) >> (MAS_NSUINT_BIT - howmuch)))
/**
* Given a scalar or struct value, wraps it in NSValue
* Based on EXPObjectify: https://github.com/specta/expecta
*/
static inline id _MASBoxValue(const char *type, ...) {
va_list v;
va_start(v, type);
id obj = nil;
if (strcmp(type, @encode(id)) == 0) {
id actual = va_arg(v, id);
obj = actual;
} else if (strcmp(type, @encode(CGPoint)) == 0) {
CGPoint actual = (CGPoint)va_arg(v, CGPoint);
obj = [NSValue value:&actual withObjCType:type];
} else if (strcmp(type, @encode(CGSize)) == 0) {
CGSize actual = (CGSize)va_arg(v, CGSize);
obj = [NSValue value:&actual withObjCType:type];
} else if (strcmp(type, @encode(MASEdgeInsets)) == 0) {
MASEdgeInsets actual = (MASEdgeInsets)va_arg(v, MASEdgeInsets);
obj = [NSValue value:&actual withObjCType:type];
} else if (strcmp(type, @encode(double)) == 0) {
double actual = (double)va_arg(v, double);
obj = [NSNumber numberWithDouble:actual];
} else if (strcmp(type, @encode(float)) == 0) {
float actual = (float)va_arg(v, double);
obj = [NSNumber numberWithFloat:actual];
} else if (strcmp(type, @encode(int)) == 0) {
int actual = (int)va_arg(v, int);
obj = [NSNumber numberWithInt:actual];
} else if (strcmp(type, @encode(long)) == 0) {
long actual = (long)va_arg(v, long);
obj = [NSNumber numberWithLong:actual];
} else if (strcmp(type, @encode(long long)) == 0) {
long long actual = (long long)va_arg(v, long long);
obj = [NSNumber numberWithLongLong:actual];
} else if (strcmp(type, @encode(short)) == 0) {
short actual = (short)va_arg(v, int);
obj = [NSNumber numberWithShort:actual];
} else if (strcmp(type, @encode(char)) == 0) {
char actual = (char)va_arg(v, int);
obj = [NSNumber numberWithChar:actual];
} else if (strcmp(type, @encode(bool)) == 0) {
bool actual = (bool)va_arg(v, int);
obj = [NSNumber numberWithBool:actual];
} else if (strcmp(type, @encode(unsigned char)) == 0) {
unsigned char actual = (unsigned char)va_arg(v, unsigned int);
obj = [NSNumber numberWithUnsignedChar:actual];
} else if (strcmp(type, @encode(unsigned int)) == 0) {
unsigned int actual = (unsigned int)va_arg(v, unsigned int);
obj = [NSNumber numberWithUnsignedInt:actual];
} else if (strcmp(type, @encode(unsigned long)) == 0) {
unsigned long actual = (unsigned long)va_arg(v, unsigned long);
obj = [NSNumber numberWithUnsignedLong:actual];
} else if (strcmp(type, @encode(unsigned long long)) == 0) {
unsigned long long actual = (unsigned long long)va_arg(v, unsigned long long);
obj = [NSNumber numberWithUnsignedLongLong:actual];
} else if (strcmp(type, @encode(unsigned short)) == 0) {
unsigned short actual = (unsigned short)va_arg(v, unsigned int);
obj = [NSNumber numberWithUnsignedShort:actual];
}
va_end(v);
return obj;
}
#define MASBoxValue(value) _MASBoxValue(@encode(__typeof__((value))), (value))

View File

@@ -1,49 +0,0 @@
//
// MASViewAttribute.h
// Masonry
//
// Created by Jonas Budelmann on 21/07/13.
// Copyright (c) 2013 cloudling. All rights reserved.
//
#import "MASUtilities.h"
/**
* An immutable tuple which stores the view and the related NSLayoutAttribute.
* Describes part of either the left or right hand side of a constraint equation
*/
@interface MASViewAttribute : NSObject
/**
* The view which the reciever relates to. Can be nil if item is not a view.
*/
@property (nonatomic, weak, readonly) MAS_VIEW *view;
/**
* The item which the reciever relates to.
*/
@property (nonatomic, weak, readonly) id item;
/**
* The attribute which the reciever relates to
*/
@property (nonatomic, assign, readonly) NSLayoutAttribute layoutAttribute;
/**
* Convenience initializer.
*/
- (id)initWithView:(MAS_VIEW *)view layoutAttribute:(NSLayoutAttribute)layoutAttribute;
/**
* The designated initializer.
*/
- (id)initWithView:(MAS_VIEW *)view item:(id)item layoutAttribute:(NSLayoutAttribute)layoutAttribute;
/**
* Determine whether the layoutAttribute is a size attribute
*
* @return YES if layoutAttribute is equal to NSLayoutAttributeWidth or NSLayoutAttributeHeight
*/
- (BOOL)isSizeAttribute;
@end

View File

@@ -1,46 +0,0 @@
//
// MASViewAttribute.m
// Masonry
//
// Created by Jonas Budelmann on 21/07/13.
// Copyright (c) 2013 cloudling. All rights reserved.
//
#import "MASViewAttribute.h"
@implementation MASViewAttribute
- (id)initWithView:(MAS_VIEW *)view layoutAttribute:(NSLayoutAttribute)layoutAttribute {
self = [self initWithView:view item:view layoutAttribute:layoutAttribute];
return self;
}
- (id)initWithView:(MAS_VIEW *)view item:(id)item layoutAttribute:(NSLayoutAttribute)layoutAttribute {
self = [super init];
if (!self) return nil;
_view = view;
_item = item;
_layoutAttribute = layoutAttribute;
return self;
}
- (BOOL)isSizeAttribute {
return self.layoutAttribute == NSLayoutAttributeWidth
|| self.layoutAttribute == NSLayoutAttributeHeight;
}
- (BOOL)isEqual:(MASViewAttribute *)viewAttribute {
if ([viewAttribute isKindOfClass:self.class]) {
return self.view == viewAttribute.view
&& self.layoutAttribute == viewAttribute.layoutAttribute;
}
return [super isEqual:viewAttribute];
}
- (NSUInteger)hash {
return MAS_NSUINTROTATE([self.view hash], MAS_NSUINT_BIT / 2) ^ self.layoutAttribute;
}
@end

View File

@@ -1,48 +0,0 @@
//
// MASViewConstraint.h
// Masonry
//
// Created by Jonas Budelmann on 20/07/13.
// Copyright (c) 2013 cloudling. All rights reserved.
//
#import "MASViewAttribute.h"
#import "MASConstraint.h"
#import "MASLayoutConstraint.h"
#import "MASUtilities.h"
/**
* A single constraint.
* Contains the attributes neccessary for creating a NSLayoutConstraint and adding it to the appropriate view
*/
@interface MASViewConstraint : MASConstraint <NSCopying>
/**
* First item/view and first attribute of the NSLayoutConstraint
*/
@property (nonatomic, strong, readonly) MASViewAttribute *firstViewAttribute;
/**
* Second item/view and second attribute of the NSLayoutConstraint
*/
@property (nonatomic, strong, readonly) MASViewAttribute *secondViewAttribute;
/**
* initialises the MASViewConstraint with the first part of the equation
*
* @param firstViewAttribute view.mas_left, view.mas_width etc.
*
* @return a new view constraint
*/
- (id)initWithFirstViewAttribute:(MASViewAttribute *)firstViewAttribute;
/**
* Returns all MASViewConstraints installed with this view as a first item.
*
* @param view A view to retrieve constraints for.
*
* @return An array of MASViewConstraints.
*/
+ (NSArray *)installedConstraintsForView:(MAS_VIEW *)view;
@end

View File

@@ -1,401 +0,0 @@
//
// MASViewConstraint.m
// Masonry
//
// Created by Jonas Budelmann on 20/07/13.
// Copyright (c) 2013 cloudling. All rights reserved.
//
#import "MASViewConstraint.h"
#import "MASConstraint+Private.h"
#import "MASCompositeConstraint.h"
#import "MASLayoutConstraint.h"
#import "View+MASAdditions.h"
#import <objc/runtime.h>
@interface MAS_VIEW (MASConstraints)
@property (nonatomic, readonly) NSMutableSet *mas_installedConstraints;
@end
@implementation MAS_VIEW (MASConstraints)
static char kInstalledConstraintsKey;
- (NSMutableSet *)mas_installedConstraints {
NSMutableSet *constraints = objc_getAssociatedObject(self, &kInstalledConstraintsKey);
if (!constraints) {
constraints = [NSMutableSet set];
objc_setAssociatedObject(self, &kInstalledConstraintsKey, constraints, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
return constraints;
}
@end
@interface MASViewConstraint ()
@property (nonatomic, strong, readwrite) MASViewAttribute *secondViewAttribute;
@property (nonatomic, weak) MAS_VIEW *installedView;
@property (nonatomic, weak) MASLayoutConstraint *layoutConstraint;
@property (nonatomic, assign) NSLayoutRelation layoutRelation;
@property (nonatomic, assign) MASLayoutPriority layoutPriority;
@property (nonatomic, assign) CGFloat layoutMultiplier;
@property (nonatomic, assign) CGFloat layoutConstant;
@property (nonatomic, assign) BOOL hasLayoutRelation;
@property (nonatomic, strong) id mas_key;
@property (nonatomic, assign) BOOL useAnimator;
@end
@implementation MASViewConstraint
- (id)initWithFirstViewAttribute:(MASViewAttribute *)firstViewAttribute {
self = [super init];
if (!self) return nil;
_firstViewAttribute = firstViewAttribute;
self.layoutPriority = MASLayoutPriorityRequired;
self.layoutMultiplier = 1;
return self;
}
#pragma mark - NSCoping
- (id)copyWithZone:(NSZone __unused *)zone {
MASViewConstraint *constraint = [[MASViewConstraint alloc] initWithFirstViewAttribute:self.firstViewAttribute];
constraint.layoutConstant = self.layoutConstant;
constraint.layoutRelation = self.layoutRelation;
constraint.layoutPriority = self.layoutPriority;
constraint.layoutMultiplier = self.layoutMultiplier;
constraint.delegate = self.delegate;
return constraint;
}
#pragma mark - Public
+ (NSArray *)installedConstraintsForView:(MAS_VIEW *)view {
return [view.mas_installedConstraints allObjects];
}
#pragma mark - Private
- (void)setLayoutConstant:(CGFloat)layoutConstant {
_layoutConstant = layoutConstant;
#if TARGET_OS_MAC && !(TARGET_OS_IPHONE || TARGET_OS_TV)
if (self.useAnimator) {
[self.layoutConstraint.animator setConstant:layoutConstant];
} else {
self.layoutConstraint.constant = layoutConstant;
}
#else
self.layoutConstraint.constant = layoutConstant;
#endif
}
- (void)setLayoutRelation:(NSLayoutRelation)layoutRelation {
_layoutRelation = layoutRelation;
self.hasLayoutRelation = YES;
}
- (BOOL)supportsActiveProperty {
return [self.layoutConstraint respondsToSelector:@selector(isActive)];
}
- (BOOL)isActive {
BOOL active = YES;
if ([self supportsActiveProperty]) {
active = [self.layoutConstraint isActive];
}
return active;
}
- (BOOL)hasBeenInstalled {
return (self.layoutConstraint != nil) && [self isActive];
}
- (void)setSecondViewAttribute:(id)secondViewAttribute {
if ([secondViewAttribute isKindOfClass:NSValue.class]) {
[self setLayoutConstantWithValue:secondViewAttribute];
} else if ([secondViewAttribute isKindOfClass:MAS_VIEW.class]) {
_secondViewAttribute = [[MASViewAttribute alloc] initWithView:secondViewAttribute layoutAttribute:self.firstViewAttribute.layoutAttribute];
} else if ([secondViewAttribute isKindOfClass:MASViewAttribute.class]) {
_secondViewAttribute = secondViewAttribute;
} else {
NSAssert(NO, @"attempting to add unsupported attribute: %@", secondViewAttribute);
}
}
#pragma mark - NSLayoutConstraint multiplier proxies
- (MASConstraint * (^)(CGFloat))multipliedBy {
return ^id(CGFloat multiplier) {
NSAssert(!self.hasBeenInstalled,
@"Cannot modify constraint multiplier after it has been installed");
self.layoutMultiplier = multiplier;
return self;
};
}
- (MASConstraint * (^)(CGFloat))dividedBy {
return ^id(CGFloat divider) {
NSAssert(!self.hasBeenInstalled,
@"Cannot modify constraint multiplier after it has been installed");
self.layoutMultiplier = 1.0/divider;
return self;
};
}
#pragma mark - MASLayoutPriority proxy
- (MASConstraint * (^)(MASLayoutPriority))priority {
return ^id(MASLayoutPriority priority) {
NSAssert(!self.hasBeenInstalled,
@"Cannot modify constraint priority after it has been installed");
self.layoutPriority = priority;
return self;
};
}
#pragma mark - NSLayoutRelation proxy
- (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation {
return ^id(id attribute, NSLayoutRelation relation) {
if ([attribute isKindOfClass:NSArray.class]) {
NSAssert(!self.hasLayoutRelation, @"Redefinition of constraint relation");
NSMutableArray *children = NSMutableArray.new;
for (id attr in attribute) {
MASViewConstraint *viewConstraint = [self copy];
viewConstraint.layoutRelation = relation;
viewConstraint.secondViewAttribute = attr;
[children addObject:viewConstraint];
}
MASCompositeConstraint *compositeConstraint = [[MASCompositeConstraint alloc] initWithChildren:children];
compositeConstraint.delegate = self.delegate;
[self.delegate constraint:self shouldBeReplacedWithConstraint:compositeConstraint];
return compositeConstraint;
} else {
NSAssert(!self.hasLayoutRelation || self.layoutRelation == relation && [attribute isKindOfClass:NSValue.class], @"Redefinition of constraint relation");
self.layoutRelation = relation;
self.secondViewAttribute = attribute;
return self;
}
};
}
#pragma mark - Semantic properties
- (MASConstraint *)with {
return self;
}
- (MASConstraint *)and {
return self;
}
#pragma mark - attribute chaining
- (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
NSAssert(!self.hasLayoutRelation, @"Attributes should be chained before defining the constraint relation");
return [self.delegate constraint:self addConstraintWithLayoutAttribute:layoutAttribute];
}
#pragma mark - Animator proxy
#if TARGET_OS_MAC && !(TARGET_OS_IPHONE || TARGET_OS_TV)
- (MASConstraint *)animator {
self.useAnimator = YES;
return self;
}
#endif
#pragma mark - debug helpers
- (MASConstraint * (^)(id))key {
return ^id(id key) {
self.mas_key = key;
return self;
};
}
#pragma mark - NSLayoutConstraint constant setters
- (void)setInsets:(MASEdgeInsets)insets {
NSLayoutAttribute layoutAttribute = self.firstViewAttribute.layoutAttribute;
switch (layoutAttribute) {
case NSLayoutAttributeLeft:
case NSLayoutAttributeLeading:
self.layoutConstant = insets.left;
break;
case NSLayoutAttributeTop:
self.layoutConstant = insets.top;
break;
case NSLayoutAttributeBottom:
self.layoutConstant = -insets.bottom;
break;
case NSLayoutAttributeRight:
case NSLayoutAttributeTrailing:
self.layoutConstant = -insets.right;
break;
default:
break;
}
}
- (void)setInset:(CGFloat)inset {
[self setInsets:(MASEdgeInsets){.top = inset, .left = inset, .bottom = inset, .right = inset}];
}
- (void)setOffset:(CGFloat)offset {
self.layoutConstant = offset;
}
- (void)setSizeOffset:(CGSize)sizeOffset {
NSLayoutAttribute layoutAttribute = self.firstViewAttribute.layoutAttribute;
switch (layoutAttribute) {
case NSLayoutAttributeWidth:
self.layoutConstant = sizeOffset.width;
break;
case NSLayoutAttributeHeight:
self.layoutConstant = sizeOffset.height;
break;
default:
break;
}
}
- (void)setCenterOffset:(CGPoint)centerOffset {
NSLayoutAttribute layoutAttribute = self.firstViewAttribute.layoutAttribute;
switch (layoutAttribute) {
case NSLayoutAttributeCenterX:
self.layoutConstant = centerOffset.x;
break;
case NSLayoutAttributeCenterY:
self.layoutConstant = centerOffset.y;
break;
default:
break;
}
}
#pragma mark - MASConstraint
- (void)activate {
[self install];
}
- (void)deactivate {
[self uninstall];
}
- (void)install {
if (self.hasBeenInstalled) {
return;
}
if ([self supportsActiveProperty] && self.layoutConstraint) {
self.layoutConstraint.active = YES;
[self.firstViewAttribute.view.mas_installedConstraints addObject:self];
return;
}
MAS_VIEW *firstLayoutItem = self.firstViewAttribute.item;
NSLayoutAttribute firstLayoutAttribute = self.firstViewAttribute.layoutAttribute;
MAS_VIEW *secondLayoutItem = self.secondViewAttribute.item;
NSLayoutAttribute secondLayoutAttribute = self.secondViewAttribute.layoutAttribute;
// alignment attributes must have a secondViewAttribute
// therefore we assume that is refering to superview
// eg make.left.equalTo(@10)
if (!self.firstViewAttribute.isSizeAttribute && !self.secondViewAttribute) {
secondLayoutItem = self.firstViewAttribute.view.superview;
secondLayoutAttribute = firstLayoutAttribute;
}
MASLayoutConstraint *layoutConstraint
= [MASLayoutConstraint constraintWithItem:firstLayoutItem
attribute:firstLayoutAttribute
relatedBy:self.layoutRelation
toItem:secondLayoutItem
attribute:secondLayoutAttribute
multiplier:self.layoutMultiplier
constant:self.layoutConstant];
layoutConstraint.priority = self.layoutPriority;
layoutConstraint.mas_key = self.mas_key;
if (self.secondViewAttribute.view) {
MAS_VIEW *closestCommonSuperview = [self.firstViewAttribute.view mas_closestCommonSuperview:self.secondViewAttribute.view];
NSAssert(closestCommonSuperview,
@"couldn't find a common superview for %@ and %@",
self.firstViewAttribute.view, self.secondViewAttribute.view);
self.installedView = closestCommonSuperview;
} else if (self.firstViewAttribute.isSizeAttribute) {
self.installedView = self.firstViewAttribute.view;
} else {
self.installedView = self.firstViewAttribute.view.superview;
}
MASLayoutConstraint *existingConstraint = nil;
if (self.updateExisting) {
existingConstraint = [self layoutConstraintSimilarTo:layoutConstraint];
}
if (existingConstraint) {
// just update the constant
existingConstraint.constant = layoutConstraint.constant;
self.layoutConstraint = existingConstraint;
} else {
[self.installedView addConstraint:layoutConstraint];
self.layoutConstraint = layoutConstraint;
[firstLayoutItem.mas_installedConstraints addObject:self];
}
}
- (MASLayoutConstraint *)layoutConstraintSimilarTo:(MASLayoutConstraint *)layoutConstraint {
// check if any constraints are the same apart from the only mutable property constant
// go through constraints in reverse as we do not want to match auto-resizing or interface builder constraints
// and they are likely to be added first.
for (NSLayoutConstraint *existingConstraint in self.installedView.constraints.reverseObjectEnumerator) {
if (![existingConstraint isKindOfClass:MASLayoutConstraint.class]) continue;
if (existingConstraint.firstItem != layoutConstraint.firstItem) continue;
if (existingConstraint.secondItem != layoutConstraint.secondItem) continue;
if (existingConstraint.firstAttribute != layoutConstraint.firstAttribute) continue;
if (existingConstraint.secondAttribute != layoutConstraint.secondAttribute) continue;
if (existingConstraint.relation != layoutConstraint.relation) continue;
if (existingConstraint.multiplier != layoutConstraint.multiplier) continue;
if (existingConstraint.priority != layoutConstraint.priority) continue;
return (id)existingConstraint;
}
return nil;
}
- (void)uninstall {
if ([self supportsActiveProperty]) {
self.layoutConstraint.active = NO;
[self.firstViewAttribute.view.mas_installedConstraints removeObject:self];
return;
}
[self.installedView removeConstraint:self.layoutConstraint];
self.layoutConstraint = nil;
self.installedView = nil;
[self.firstViewAttribute.view.mas_installedConstraints removeObject:self];
}
@end

View File

@@ -1,29 +0,0 @@
//
// Masonry.h
// Masonry
//
// Created by Jonas Budelmann on 20/07/13.
// Copyright (c) 2013 cloudling. All rights reserved.
//
#import <Foundation/Foundation.h>
//! Project version number for Masonry.
FOUNDATION_EXPORT double MasonryVersionNumber;
//! Project version string for Masonry.
FOUNDATION_EXPORT const unsigned char MasonryVersionString[];
#import "MASUtilities.h"
#import "View+MASAdditions.h"
#import "View+MASShorthandAdditions.h"
#import "ViewController+MASAdditions.h"
#import "NSArray+MASAdditions.h"
#import "NSArray+MASShorthandAdditions.h"
#import "MASConstraint.h"
#import "MASCompositeConstraint.h"
#import "MASViewAttribute.h"
#import "MASViewConstraint.h"
#import "MASConstraintMaker.h"
#import "MASLayoutConstraint.h"
#import "NSLayoutConstraint+MASDebugAdditions.h"

View File

@@ -1,72 +0,0 @@
//
// NSArray+MASAdditions.h
//
//
// Created by Daniel Hammond on 11/26/13.
//
//
#import "MASUtilities.h"
#import "MASConstraintMaker.h"
#import "MASViewAttribute.h"
typedef NS_ENUM(NSUInteger, MASAxisType) {
MASAxisTypeHorizontal,
MASAxisTypeVertical
};
@interface NSArray (MASAdditions)
/**
* Creates a MASConstraintMaker with each view in the callee.
* Any constraints defined are added to the view or the appropriate superview once the block has finished executing on each view
*
* @param block scope within which you can build up the constraints which you wish to apply to each view.
*
* @return Array of created MASConstraints
*/
- (NSArray *)mas_makeConstraints:(void (NS_NOESCAPE ^)(MASConstraintMaker *make))block;
/**
* Creates a MASConstraintMaker with each view in the callee.
* Any constraints defined are added to each view or the appropriate superview once the block has finished executing on each view.
* If an existing constraint exists then it will be updated instead.
*
* @param block scope within which you can build up the constraints which you wish to apply to each view.
*
* @return Array of created/updated MASConstraints
*/
- (NSArray *)mas_updateConstraints:(void (NS_NOESCAPE ^)(MASConstraintMaker *make))block;
/**
* Creates a MASConstraintMaker with each view in the callee.
* Any constraints defined are added to each view or the appropriate superview once the block has finished executing on each view.
* All constraints previously installed for the views will be removed.
*
* @param block scope within which you can build up the constraints which you wish to apply to each view.
*
* @return Array of created/updated MASConstraints
*/
- (NSArray *)mas_remakeConstraints:(void (NS_NOESCAPE ^)(MASConstraintMaker *make))block;
/**
* distribute with fixed spacing
*
* @param axisType which axis to distribute items along
* @param fixedSpacing the spacing between each item
* @param leadSpacing the spacing before the first item and the container
* @param tailSpacing the spacing after the last item and the container
*/
- (void)mas_distributeViewsAlongAxis:(MASAxisType)axisType withFixedSpacing:(CGFloat)fixedSpacing leadSpacing:(CGFloat)leadSpacing tailSpacing:(CGFloat)tailSpacing;
/**
* distribute with fixed item size
*
* @param axisType which axis to distribute items along
* @param fixedItemLength the fixed length of each item
* @param leadSpacing the spacing before the first item and the container
* @param tailSpacing the spacing after the last item and the container
*/
- (void)mas_distributeViewsAlongAxis:(MASAxisType)axisType withFixedItemLength:(CGFloat)fixedItemLength leadSpacing:(CGFloat)leadSpacing tailSpacing:(CGFloat)tailSpacing;
@end

View File

@@ -1,162 +0,0 @@
//
// NSArray+MASAdditions.m
//
//
// Created by Daniel Hammond on 11/26/13.
//
//
#import "NSArray+MASAdditions.h"
#import "View+MASAdditions.h"
@implementation NSArray (MASAdditions)
- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *make))block {
NSMutableArray *constraints = [NSMutableArray array];
for (MAS_VIEW *view in self) {
NSAssert([view isKindOfClass:[MAS_VIEW class]], @"All objects in the array must be views");
[constraints addObjectsFromArray:[view mas_makeConstraints:block]];
}
return constraints;
}
- (NSArray *)mas_updateConstraints:(void(^)(MASConstraintMaker *make))block {
NSMutableArray *constraints = [NSMutableArray array];
for (MAS_VIEW *view in self) {
NSAssert([view isKindOfClass:[MAS_VIEW class]], @"All objects in the array must be views");
[constraints addObjectsFromArray:[view mas_updateConstraints:block]];
}
return constraints;
}
- (NSArray *)mas_remakeConstraints:(void(^)(MASConstraintMaker *make))block {
NSMutableArray *constraints = [NSMutableArray array];
for (MAS_VIEW *view in self) {
NSAssert([view isKindOfClass:[MAS_VIEW class]], @"All objects in the array must be views");
[constraints addObjectsFromArray:[view mas_remakeConstraints:block]];
}
return constraints;
}
- (void)mas_distributeViewsAlongAxis:(MASAxisType)axisType withFixedSpacing:(CGFloat)fixedSpacing leadSpacing:(CGFloat)leadSpacing tailSpacing:(CGFloat)tailSpacing {
if (self.count < 2) {
NSAssert(self.count>1,@"views to distribute need to bigger than one");
return;
}
MAS_VIEW *tempSuperView = [self mas_commonSuperviewOfViews];
if (axisType == MASAxisTypeHorizontal) {
MAS_VIEW *prev;
for (int i = 0; i < self.count; i++) {
MAS_VIEW *v = self[i];
[v mas_makeConstraints:^(MASConstraintMaker *make) {
if (prev) {
make.width.equalTo(prev);
make.left.equalTo(prev.mas_right).offset(fixedSpacing);
if (i == self.count - 1) {//last one
make.right.equalTo(tempSuperView).offset(-tailSpacing);
}
}
else {//first one
make.left.equalTo(tempSuperView).offset(leadSpacing);
}
}];
prev = v;
}
}
else {
MAS_VIEW *prev;
for (int i = 0; i < self.count; i++) {
MAS_VIEW *v = self[i];
[v mas_makeConstraints:^(MASConstraintMaker *make) {
if (prev) {
make.height.equalTo(prev);
make.top.equalTo(prev.mas_bottom).offset(fixedSpacing);
if (i == self.count - 1) {//last one
make.bottom.equalTo(tempSuperView).offset(-tailSpacing);
}
}
else {//first one
make.top.equalTo(tempSuperView).offset(leadSpacing);
}
}];
prev = v;
}
}
}
- (void)mas_distributeViewsAlongAxis:(MASAxisType)axisType withFixedItemLength:(CGFloat)fixedItemLength leadSpacing:(CGFloat)leadSpacing tailSpacing:(CGFloat)tailSpacing {
if (self.count < 2) {
NSAssert(self.count>1,@"views to distribute need to bigger than one");
return;
}
MAS_VIEW *tempSuperView = [self mas_commonSuperviewOfViews];
if (axisType == MASAxisTypeHorizontal) {
MAS_VIEW *prev;
for (int i = 0; i < self.count; i++) {
MAS_VIEW *v = self[i];
[v mas_makeConstraints:^(MASConstraintMaker *make) {
make.width.equalTo(@(fixedItemLength));
if (prev) {
if (i == self.count - 1) {//last one
make.right.equalTo(tempSuperView).offset(-tailSpacing);
}
else {
CGFloat offset = (1-(i/((CGFloat)self.count-1)))*(fixedItemLength+leadSpacing)-i*tailSpacing/(((CGFloat)self.count-1));
make.right.equalTo(tempSuperView).multipliedBy(i/((CGFloat)self.count-1)).with.offset(offset);
}
}
else {//first one
make.left.equalTo(tempSuperView).offset(leadSpacing);
}
}];
prev = v;
}
}
else {
MAS_VIEW *prev;
for (int i = 0; i < self.count; i++) {
MAS_VIEW *v = self[i];
[v mas_makeConstraints:^(MASConstraintMaker *make) {
make.height.equalTo(@(fixedItemLength));
if (prev) {
if (i == self.count - 1) {//last one
make.bottom.equalTo(tempSuperView).offset(-tailSpacing);
}
else {
CGFloat offset = (1-(i/((CGFloat)self.count-1)))*(fixedItemLength+leadSpacing)-i*tailSpacing/(((CGFloat)self.count-1));
make.bottom.equalTo(tempSuperView).multipliedBy(i/((CGFloat)self.count-1)).with.offset(offset);
}
}
else {//first one
make.top.equalTo(tempSuperView).offset(leadSpacing);
}
}];
prev = v;
}
}
}
- (MAS_VIEW *)mas_commonSuperviewOfViews
{
MAS_VIEW *commonSuperview = nil;
MAS_VIEW *previousView = nil;
for (id object in self) {
if ([object isKindOfClass:[MAS_VIEW class]]) {
MAS_VIEW *view = (MAS_VIEW *)object;
if (previousView) {
commonSuperview = [view mas_closestCommonSuperview:commonSuperview];
} else {
commonSuperview = view;
}
previousView = view;
}
}
NSAssert(commonSuperview, @"Can't constrain views that do not share a common superview. Make sure that all the views in this array have been added into the same view hierarchy.");
return commonSuperview;
}
@end

View File

@@ -1,41 +0,0 @@
//
// NSArray+MASShorthandAdditions.h
// Masonry
//
// Created by Jonas Budelmann on 22/07/13.
// Copyright (c) 2013 Jonas Budelmann. All rights reserved.
//
#import "NSArray+MASAdditions.h"
#ifdef MAS_SHORTHAND
/**
* Shorthand array additions without the 'mas_' prefixes,
* only enabled if MAS_SHORTHAND is defined
*/
@interface NSArray (MASShorthandAdditions)
- (NSArray *)makeConstraints:(void(^)(MASConstraintMaker *make))block;
- (NSArray *)updateConstraints:(void(^)(MASConstraintMaker *make))block;
- (NSArray *)remakeConstraints:(void(^)(MASConstraintMaker *make))block;
@end
@implementation NSArray (MASShorthandAdditions)
- (NSArray *)makeConstraints:(void(^)(MASConstraintMaker *))block {
return [self mas_makeConstraints:block];
}
- (NSArray *)updateConstraints:(void(^)(MASConstraintMaker *))block {
return [self mas_updateConstraints:block];
}
- (NSArray *)remakeConstraints:(void(^)(MASConstraintMaker *))block {
return [self mas_remakeConstraints:block];
}
@end
#endif

View File

@@ -1,16 +0,0 @@
//
// NSLayoutConstraint+MASDebugAdditions.h
// Masonry
//
// Created by Jonas Budelmann on 3/08/13.
// Copyright (c) 2013 Jonas Budelmann. All rights reserved.
//
#import "MASUtilities.h"
/**
* makes debug and log output of NSLayoutConstraints more readable
*/
@interface NSLayoutConstraint (MASDebugAdditions)
@end

View File

@@ -1,146 +0,0 @@
//
// NSLayoutConstraint+MASDebugAdditions.m
// Masonry
//
// Created by Jonas Budelmann on 3/08/13.
// Copyright (c) 2013 Jonas Budelmann. All rights reserved.
//
#import "NSLayoutConstraint+MASDebugAdditions.h"
#import "MASConstraint.h"
#import "MASLayoutConstraint.h"
@implementation NSLayoutConstraint (MASDebugAdditions)
#pragma mark - description maps
+ (NSDictionary *)layoutRelationDescriptionsByValue {
static dispatch_once_t once;
static NSDictionary *descriptionMap;
dispatch_once(&once, ^{
descriptionMap = @{
@(NSLayoutRelationEqual) : @"==",
@(NSLayoutRelationGreaterThanOrEqual) : @">=",
@(NSLayoutRelationLessThanOrEqual) : @"<=",
};
});
return descriptionMap;
}
+ (NSDictionary *)layoutAttributeDescriptionsByValue {
static dispatch_once_t once;
static NSDictionary *descriptionMap;
dispatch_once(&once, ^{
descriptionMap = @{
@(NSLayoutAttributeTop) : @"top",
@(NSLayoutAttributeLeft) : @"left",
@(NSLayoutAttributeBottom) : @"bottom",
@(NSLayoutAttributeRight) : @"right",
@(NSLayoutAttributeLeading) : @"leading",
@(NSLayoutAttributeTrailing) : @"trailing",
@(NSLayoutAttributeWidth) : @"width",
@(NSLayoutAttributeHeight) : @"height",
@(NSLayoutAttributeCenterX) : @"centerX",
@(NSLayoutAttributeCenterY) : @"centerY",
@(NSLayoutAttributeBaseline) : @"baseline",
#if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000) || (__MAC_OS_X_VERSION_MIN_REQUIRED >= 101100)
@(NSLayoutAttributeFirstBaseline) : @"firstBaseline",
@(NSLayoutAttributeLastBaseline) : @"lastBaseline",
#endif
#if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000)
@(NSLayoutAttributeLeftMargin) : @"leftMargin",
@(NSLayoutAttributeRightMargin) : @"rightMargin",
@(NSLayoutAttributeTopMargin) : @"topMargin",
@(NSLayoutAttributeBottomMargin) : @"bottomMargin",
@(NSLayoutAttributeLeadingMargin) : @"leadingMargin",
@(NSLayoutAttributeTrailingMargin) : @"trailingMargin",
@(NSLayoutAttributeCenterXWithinMargins) : @"centerXWithinMargins",
@(NSLayoutAttributeCenterYWithinMargins) : @"centerYWithinMargins",
#endif
};
});
return descriptionMap;
}
+ (NSDictionary *)layoutPriorityDescriptionsByValue {
static dispatch_once_t once;
static NSDictionary *descriptionMap;
dispatch_once(&once, ^{
#if TARGET_OS_IPHONE || TARGET_OS_TV
descriptionMap = @{
@(MASLayoutPriorityDefaultHigh) : @"high",
@(MASLayoutPriorityDefaultLow) : @"low",
@(MASLayoutPriorityDefaultMedium) : @"medium",
@(MASLayoutPriorityRequired) : @"required",
@(MASLayoutPriorityFittingSizeLevel) : @"fitting size",
};
#elif TARGET_OS_MAC
descriptionMap = @{
@(MASLayoutPriorityDefaultHigh) : @"high",
@(MASLayoutPriorityDragThatCanResizeWindow) : @"drag can resize window",
@(MASLayoutPriorityDefaultMedium) : @"medium",
@(MASLayoutPriorityWindowSizeStayPut) : @"window size stay put",
@(MASLayoutPriorityDragThatCannotResizeWindow) : @"drag cannot resize window",
@(MASLayoutPriorityDefaultLow) : @"low",
@(MASLayoutPriorityFittingSizeCompression) : @"fitting size",
@(MASLayoutPriorityRequired) : @"required",
};
#endif
});
return descriptionMap;
}
#pragma mark - description override
+ (NSString *)descriptionForObject:(id)obj {
if ([obj respondsToSelector:@selector(mas_key)] && [obj mas_key]) {
return [NSString stringWithFormat:@"%@:%@", [obj class], [obj mas_key]];
}
return [NSString stringWithFormat:@"%@:%p", [obj class], obj];
}
- (NSString *)description {
NSMutableString *description = [[NSMutableString alloc] initWithString:@"<"];
[description appendString:[self.class descriptionForObject:self]];
[description appendFormat:@" %@", [self.class descriptionForObject:self.firstItem]];
if (self.firstAttribute != NSLayoutAttributeNotAnAttribute) {
[description appendFormat:@".%@", self.class.layoutAttributeDescriptionsByValue[@(self.firstAttribute)]];
}
[description appendFormat:@" %@", self.class.layoutRelationDescriptionsByValue[@(self.relation)]];
if (self.secondItem) {
[description appendFormat:@" %@", [self.class descriptionForObject:self.secondItem]];
}
if (self.secondAttribute != NSLayoutAttributeNotAnAttribute) {
[description appendFormat:@".%@", self.class.layoutAttributeDescriptionsByValue[@(self.secondAttribute)]];
}
if (self.multiplier != 1) {
[description appendFormat:@" * %g", self.multiplier];
}
if (self.secondAttribute == NSLayoutAttributeNotAnAttribute) {
[description appendFormat:@" %g", self.constant];
} else {
if (self.constant) {
[description appendFormat:@" %@ %g", (self.constant < 0 ? @"-" : @"+"), ABS(self.constant)];
}
}
if (self.priority != MASLayoutPriorityRequired) {
[description appendFormat:@" ^%@", self.class.layoutPriorityDescriptionsByValue[@(self.priority)] ?: [NSNumber numberWithDouble:self.priority]];
}
[description appendString:@">"];
return description;
}
@end

View File

@@ -1,111 +0,0 @@
//
// UIView+MASAdditions.h
// Masonry
//
// Created by Jonas Budelmann on 20/07/13.
// Copyright (c) 2013 cloudling. All rights reserved.
//
#import "MASUtilities.h"
#import "MASConstraintMaker.h"
#import "MASViewAttribute.h"
/**
* Provides constraint maker block
* and convience methods for creating MASViewAttribute which are view + NSLayoutAttribute pairs
*/
@interface MAS_VIEW (MASAdditions)
/**
* following properties return a new MASViewAttribute with current view and appropriate NSLayoutAttribute
*/
@property (nonatomic, strong, readonly) MASViewAttribute *mas_left;
@property (nonatomic, strong, readonly) MASViewAttribute *mas_top;
@property (nonatomic, strong, readonly) MASViewAttribute *mas_right;
@property (nonatomic, strong, readonly) MASViewAttribute *mas_bottom;
@property (nonatomic, strong, readonly) MASViewAttribute *mas_leading;
@property (nonatomic, strong, readonly) MASViewAttribute *mas_trailing;
@property (nonatomic, strong, readonly) MASViewAttribute *mas_width;
@property (nonatomic, strong, readonly) MASViewAttribute *mas_height;
@property (nonatomic, strong, readonly) MASViewAttribute *mas_centerX;
@property (nonatomic, strong, readonly) MASViewAttribute *mas_centerY;
@property (nonatomic, strong, readonly) MASViewAttribute *mas_baseline;
@property (nonatomic, strong, readonly) MASViewAttribute *(^mas_attribute)(NSLayoutAttribute attr);
#if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000) || (__MAC_OS_X_VERSION_MIN_REQUIRED >= 101100)
@property (nonatomic, strong, readonly) MASViewAttribute *mas_firstBaseline;
@property (nonatomic, strong, readonly) MASViewAttribute *mas_lastBaseline;
#endif
#if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000)
@property (nonatomic, strong, readonly) MASViewAttribute *mas_leftMargin;
@property (nonatomic, strong, readonly) MASViewAttribute *mas_rightMargin;
@property (nonatomic, strong, readonly) MASViewAttribute *mas_topMargin;
@property (nonatomic, strong, readonly) MASViewAttribute *mas_bottomMargin;
@property (nonatomic, strong, readonly) MASViewAttribute *mas_leadingMargin;
@property (nonatomic, strong, readonly) MASViewAttribute *mas_trailingMargin;
@property (nonatomic, strong, readonly) MASViewAttribute *mas_centerXWithinMargins;
@property (nonatomic, strong, readonly) MASViewAttribute *mas_centerYWithinMargins;
#endif
#if (__IPHONE_OS_VERSION_MAX_ALLOWED >= 110000) || (__TV_OS_VERSION_MAX_ALLOWED >= 110000)
@property (nonatomic, strong, readonly) MASViewAttribute *mas_safeAreaLayoutGuide API_AVAILABLE(ios(11.0),tvos(11.0));
@property (nonatomic, strong, readonly) MASViewAttribute *mas_safeAreaLayoutGuideTop API_AVAILABLE(ios(11.0),tvos(11.0));
@property (nonatomic, strong, readonly) MASViewAttribute *mas_safeAreaLayoutGuideBottom API_AVAILABLE(ios(11.0),tvos(11.0));
@property (nonatomic, strong, readonly) MASViewAttribute *mas_safeAreaLayoutGuideLeft API_AVAILABLE(ios(11.0),tvos(11.0));
@property (nonatomic, strong, readonly) MASViewAttribute *mas_safeAreaLayoutGuideRight API_AVAILABLE(ios(11.0),tvos(11.0));
#endif
/**
* a key to associate with this view
*/
@property (nonatomic, strong) id mas_key;
/**
* Finds the closest common superview between this view and another view
*
* @param view other view
*
* @return returns nil if common superview could not be found
*/
- (instancetype)mas_closestCommonSuperview:(MAS_VIEW *)view;
/**
* Creates a MASConstraintMaker with the callee view.
* Any constraints defined are added to the view or the appropriate superview once the block has finished executing
*
* @param block scope within which you can build up the constraints which you wish to apply to the view.
*
* @return Array of created MASConstraints
*/
- (NSArray *)mas_makeConstraints:(void(NS_NOESCAPE ^)(MASConstraintMaker *make))block;
/**
* Creates a MASConstraintMaker with the callee view.
* Any constraints defined are added to the view or the appropriate superview once the block has finished executing.
* If an existing constraint exists then it will be updated instead.
*
* @param block scope within which you can build up the constraints which you wish to apply to the view.
*
* @return Array of created/updated MASConstraints
*/
- (NSArray *)mas_updateConstraints:(void(NS_NOESCAPE ^)(MASConstraintMaker *make))block;
/**
* Creates a MASConstraintMaker with the callee view.
* Any constraints defined are added to the view or the appropriate superview once the block has finished executing.
* All constraints previously installed for the view will be removed.
*
* @param block scope within which you can build up the constraints which you wish to apply to the view.
*
* @return Array of created/updated MASConstraints
*/
- (NSArray *)mas_remakeConstraints:(void(NS_NOESCAPE ^)(MASConstraintMaker *make))block;
@end

View File

@@ -1,186 +0,0 @@
//
// UIView+MASAdditions.m
// Masonry
//
// Created by Jonas Budelmann on 20/07/13.
// Copyright (c) 2013 cloudling. All rights reserved.
//
#import "View+MASAdditions.h"
#import <objc/runtime.h>
@implementation MAS_VIEW (MASAdditions)
- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
self.translatesAutoresizingMaskIntoConstraints = NO;
MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
block(constraintMaker);
return [constraintMaker install];
}
- (NSArray *)mas_updateConstraints:(void(^)(MASConstraintMaker *))block {
self.translatesAutoresizingMaskIntoConstraints = NO;
MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
constraintMaker.updateExisting = YES;
block(constraintMaker);
return [constraintMaker install];
}
- (NSArray *)mas_remakeConstraints:(void(^)(MASConstraintMaker *make))block {
self.translatesAutoresizingMaskIntoConstraints = NO;
MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
constraintMaker.removeExisting = YES;
block(constraintMaker);
return [constraintMaker install];
}
#pragma mark - NSLayoutAttribute properties
- (MASViewAttribute *)mas_left {
return [[MASViewAttribute alloc] initWithView:self layoutAttribute:NSLayoutAttributeLeft];
}
- (MASViewAttribute *)mas_top {
return [[MASViewAttribute alloc] initWithView:self layoutAttribute:NSLayoutAttributeTop];
}
- (MASViewAttribute *)mas_right {
return [[MASViewAttribute alloc] initWithView:self layoutAttribute:NSLayoutAttributeRight];
}
- (MASViewAttribute *)mas_bottom {
return [[MASViewAttribute alloc] initWithView:self layoutAttribute:NSLayoutAttributeBottom];
}
- (MASViewAttribute *)mas_leading {
return [[MASViewAttribute alloc] initWithView:self layoutAttribute:NSLayoutAttributeLeading];
}
- (MASViewAttribute *)mas_trailing {
return [[MASViewAttribute alloc] initWithView:self layoutAttribute:NSLayoutAttributeTrailing];
}
- (MASViewAttribute *)mas_width {
return [[MASViewAttribute alloc] initWithView:self layoutAttribute:NSLayoutAttributeWidth];
}
- (MASViewAttribute *)mas_height {
return [[MASViewAttribute alloc] initWithView:self layoutAttribute:NSLayoutAttributeHeight];
}
- (MASViewAttribute *)mas_centerX {
return [[MASViewAttribute alloc] initWithView:self layoutAttribute:NSLayoutAttributeCenterX];
}
- (MASViewAttribute *)mas_centerY {
return [[MASViewAttribute alloc] initWithView:self layoutAttribute:NSLayoutAttributeCenterY];
}
- (MASViewAttribute *)mas_baseline {
return [[MASViewAttribute alloc] initWithView:self layoutAttribute:NSLayoutAttributeBaseline];
}
- (MASViewAttribute *(^)(NSLayoutAttribute))mas_attribute
{
return ^(NSLayoutAttribute attr) {
return [[MASViewAttribute alloc] initWithView:self layoutAttribute:attr];
};
}
#if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000) || (__MAC_OS_X_VERSION_MIN_REQUIRED >= 101100)
- (MASViewAttribute *)mas_firstBaseline {
return [[MASViewAttribute alloc] initWithView:self layoutAttribute:NSLayoutAttributeFirstBaseline];
}
- (MASViewAttribute *)mas_lastBaseline {
return [[MASViewAttribute alloc] initWithView:self layoutAttribute:NSLayoutAttributeLastBaseline];
}
#endif
#if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000)
- (MASViewAttribute *)mas_leftMargin {
return [[MASViewAttribute alloc] initWithView:self layoutAttribute:NSLayoutAttributeLeftMargin];
}
- (MASViewAttribute *)mas_rightMargin {
return [[MASViewAttribute alloc] initWithView:self layoutAttribute:NSLayoutAttributeRightMargin];
}
- (MASViewAttribute *)mas_topMargin {
return [[MASViewAttribute alloc] initWithView:self layoutAttribute:NSLayoutAttributeTopMargin];
}
- (MASViewAttribute *)mas_bottomMargin {
return [[MASViewAttribute alloc] initWithView:self layoutAttribute:NSLayoutAttributeBottomMargin];
}
- (MASViewAttribute *)mas_leadingMargin {
return [[MASViewAttribute alloc] initWithView:self layoutAttribute:NSLayoutAttributeLeadingMargin];
}
- (MASViewAttribute *)mas_trailingMargin {
return [[MASViewAttribute alloc] initWithView:self layoutAttribute:NSLayoutAttributeTrailingMargin];
}
- (MASViewAttribute *)mas_centerXWithinMargins {
return [[MASViewAttribute alloc] initWithView:self layoutAttribute:NSLayoutAttributeCenterXWithinMargins];
}
- (MASViewAttribute *)mas_centerYWithinMargins {
return [[MASViewAttribute alloc] initWithView:self layoutAttribute:NSLayoutAttributeCenterYWithinMargins];
}
#endif
#if (__IPHONE_OS_VERSION_MAX_ALLOWED >= 110000) || (__TV_OS_VERSION_MAX_ALLOWED >= 110000)
- (MASViewAttribute *)mas_safeAreaLayoutGuide {
return [[MASViewAttribute alloc] initWithView:self item:self.safeAreaLayoutGuide layoutAttribute:NSLayoutAttributeBottom];
}
- (MASViewAttribute *)mas_safeAreaLayoutGuideTop {
return [[MASViewAttribute alloc] initWithView:self item:self.safeAreaLayoutGuide layoutAttribute:NSLayoutAttributeTop];
}
- (MASViewAttribute *)mas_safeAreaLayoutGuideBottom {
return [[MASViewAttribute alloc] initWithView:self item:self.safeAreaLayoutGuide layoutAttribute:NSLayoutAttributeBottom];
}
- (MASViewAttribute *)mas_safeAreaLayoutGuideLeft {
return [[MASViewAttribute alloc] initWithView:self item:self.safeAreaLayoutGuide layoutAttribute:NSLayoutAttributeLeft];
}
- (MASViewAttribute *)mas_safeAreaLayoutGuideRight {
return [[MASViewAttribute alloc] initWithView:self item:self.safeAreaLayoutGuide layoutAttribute:NSLayoutAttributeRight];
}
#endif
#pragma mark - associated properties
- (id)mas_key {
return objc_getAssociatedObject(self, @selector(mas_key));
}
- (void)setMas_key:(id)key {
objc_setAssociatedObject(self, @selector(mas_key), key, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
#pragma mark - heirachy
- (instancetype)mas_closestCommonSuperview:(MAS_VIEW *)view {
MAS_VIEW *closestCommonSuperview = nil;
MAS_VIEW *secondViewSuperview = view;
while (!closestCommonSuperview && secondViewSuperview) {
MAS_VIEW *firstViewSuperview = self;
while (!closestCommonSuperview && firstViewSuperview) {
if (secondViewSuperview == firstViewSuperview) {
closestCommonSuperview = secondViewSuperview;
}
firstViewSuperview = firstViewSuperview.superview;
}
secondViewSuperview = secondViewSuperview.superview;
}
return closestCommonSuperview;
}
@end

View File

@@ -1,133 +0,0 @@
//
// UIView+MASShorthandAdditions.h
// Masonry
//
// Created by Jonas Budelmann on 22/07/13.
// Copyright (c) 2013 Jonas Budelmann. All rights reserved.
//
#import "View+MASAdditions.h"
#ifdef MAS_SHORTHAND
/**
* Shorthand view additions without the 'mas_' prefixes,
* only enabled if MAS_SHORTHAND is defined
*/
@interface MAS_VIEW (MASShorthandAdditions)
@property (nonatomic, strong, readonly) MASViewAttribute *left;
@property (nonatomic, strong, readonly) MASViewAttribute *top;
@property (nonatomic, strong, readonly) MASViewAttribute *right;
@property (nonatomic, strong, readonly) MASViewAttribute *bottom;
@property (nonatomic, strong, readonly) MASViewAttribute *leading;
@property (nonatomic, strong, readonly) MASViewAttribute *trailing;
@property (nonatomic, strong, readonly) MASViewAttribute *width;
@property (nonatomic, strong, readonly) MASViewAttribute *height;
@property (nonatomic, strong, readonly) MASViewAttribute *centerX;
@property (nonatomic, strong, readonly) MASViewAttribute *centerY;
@property (nonatomic, strong, readonly) MASViewAttribute *baseline;
@property (nonatomic, strong, readonly) MASViewAttribute *(^attribute)(NSLayoutAttribute attr);
#if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000) || (__MAC_OS_X_VERSION_MIN_REQUIRED >= 101100)
@property (nonatomic, strong, readonly) MASViewAttribute *firstBaseline;
@property (nonatomic, strong, readonly) MASViewAttribute *lastBaseline;
#endif
#if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000)
@property (nonatomic, strong, readonly) MASViewAttribute *leftMargin;
@property (nonatomic, strong, readonly) MASViewAttribute *rightMargin;
@property (nonatomic, strong, readonly) MASViewAttribute *topMargin;
@property (nonatomic, strong, readonly) MASViewAttribute *bottomMargin;
@property (nonatomic, strong, readonly) MASViewAttribute *leadingMargin;
@property (nonatomic, strong, readonly) MASViewAttribute *trailingMargin;
@property (nonatomic, strong, readonly) MASViewAttribute *centerXWithinMargins;
@property (nonatomic, strong, readonly) MASViewAttribute *centerYWithinMargins;
#endif
#if (__IPHONE_OS_VERSION_MAX_ALLOWED >= 110000) || (__TV_OS_VERSION_MAX_ALLOWED >= 110000)
@property (nonatomic, strong, readonly) MASViewAttribute *safeAreaLayoutGuideTop API_AVAILABLE(ios(11.0),tvos(11.0));
@property (nonatomic, strong, readonly) MASViewAttribute *safeAreaLayoutGuideBottom API_AVAILABLE(ios(11.0),tvos(11.0));
@property (nonatomic, strong, readonly) MASViewAttribute *safeAreaLayoutGuideLeft API_AVAILABLE(ios(11.0),tvos(11.0));
@property (nonatomic, strong, readonly) MASViewAttribute *safeAreaLayoutGuideRight API_AVAILABLE(ios(11.0),tvos(11.0));
#endif
- (NSArray *)makeConstraints:(void(^)(MASConstraintMaker *make))block;
- (NSArray *)updateConstraints:(void(^)(MASConstraintMaker *make))block;
- (NSArray *)remakeConstraints:(void(^)(MASConstraintMaker *make))block;
@end
#define MAS_ATTR_FORWARD(attr) \
- (MASViewAttribute *)attr { \
return [self mas_##attr]; \
}
@implementation MAS_VIEW (MASShorthandAdditions)
MAS_ATTR_FORWARD(top);
MAS_ATTR_FORWARD(left);
MAS_ATTR_FORWARD(bottom);
MAS_ATTR_FORWARD(right);
MAS_ATTR_FORWARD(leading);
MAS_ATTR_FORWARD(trailing);
MAS_ATTR_FORWARD(width);
MAS_ATTR_FORWARD(height);
MAS_ATTR_FORWARD(centerX);
MAS_ATTR_FORWARD(centerY);
MAS_ATTR_FORWARD(baseline);
#if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000) || (__MAC_OS_X_VERSION_MIN_REQUIRED >= 101100)
MAS_ATTR_FORWARD(firstBaseline);
MAS_ATTR_FORWARD(lastBaseline);
#endif
#if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000)
MAS_ATTR_FORWARD(leftMargin);
MAS_ATTR_FORWARD(rightMargin);
MAS_ATTR_FORWARD(topMargin);
MAS_ATTR_FORWARD(bottomMargin);
MAS_ATTR_FORWARD(leadingMargin);
MAS_ATTR_FORWARD(trailingMargin);
MAS_ATTR_FORWARD(centerXWithinMargins);
MAS_ATTR_FORWARD(centerYWithinMargins);
#endif
#if (__IPHONE_OS_VERSION_MAX_ALLOWED >= 110000) || (__TV_OS_VERSION_MAX_ALLOWED >= 110000)
MAS_ATTR_FORWARD(safeAreaLayoutGuideTop);
MAS_ATTR_FORWARD(safeAreaLayoutGuideBottom);
MAS_ATTR_FORWARD(safeAreaLayoutGuideLeft);
MAS_ATTR_FORWARD(safeAreaLayoutGuideRight);
#endif
- (MASViewAttribute *(^)(NSLayoutAttribute))attribute {
return [self mas_attribute];
}
- (NSArray *)makeConstraints:(void(NS_NOESCAPE ^)(MASConstraintMaker *))block {
return [self mas_makeConstraints:block];
}
- (NSArray *)updateConstraints:(void(NS_NOESCAPE ^)(MASConstraintMaker *))block {
return [self mas_updateConstraints:block];
}
- (NSArray *)remakeConstraints:(void(NS_NOESCAPE ^)(MASConstraintMaker *))block {
return [self mas_remakeConstraints:block];
}
@end
#endif

View File

@@ -1,30 +0,0 @@
//
// UIViewController+MASAdditions.h
// Masonry
//
// Created by Craig Siemens on 2015-06-23.
//
//
#import "MASUtilities.h"
#import "MASConstraintMaker.h"
#import "MASViewAttribute.h"
#ifdef MAS_VIEW_CONTROLLER
@interface MAS_VIEW_CONTROLLER (MASAdditions)
/**
* following properties return a new MASViewAttribute with appropriate UILayoutGuide and NSLayoutAttribute
*/
@property (nonatomic, strong, readonly) MASViewAttribute *mas_topLayoutGuide;
@property (nonatomic, strong, readonly) MASViewAttribute *mas_bottomLayoutGuide;
@property (nonatomic, strong, readonly) MASViewAttribute *mas_topLayoutGuideTop;
@property (nonatomic, strong, readonly) MASViewAttribute *mas_topLayoutGuideBottom;
@property (nonatomic, strong, readonly) MASViewAttribute *mas_bottomLayoutGuideTop;
@property (nonatomic, strong, readonly) MASViewAttribute *mas_bottomLayoutGuideBottom;
@end
#endif

View File

@@ -1,39 +0,0 @@
//
// UIViewController+MASAdditions.m
// Masonry
//
// Created by Craig Siemens on 2015-06-23.
//
//
#import "ViewController+MASAdditions.h"
#ifdef MAS_VIEW_CONTROLLER
@implementation MAS_VIEW_CONTROLLER (MASAdditions)
- (MASViewAttribute *)mas_topLayoutGuide {
return [[MASViewAttribute alloc] initWithView:self.view item:self.topLayoutGuide layoutAttribute:NSLayoutAttributeBottom];
}
- (MASViewAttribute *)mas_topLayoutGuideTop {
return [[MASViewAttribute alloc] initWithView:self.view item:self.topLayoutGuide layoutAttribute:NSLayoutAttributeTop];
}
- (MASViewAttribute *)mas_topLayoutGuideBottom {
return [[MASViewAttribute alloc] initWithView:self.view item:self.topLayoutGuide layoutAttribute:NSLayoutAttributeBottom];
}
- (MASViewAttribute *)mas_bottomLayoutGuide {
return [[MASViewAttribute alloc] initWithView:self.view item:self.bottomLayoutGuide layoutAttribute:NSLayoutAttributeTop];
}
- (MASViewAttribute *)mas_bottomLayoutGuideTop {
return [[MASViewAttribute alloc] initWithView:self.view item:self.bottomLayoutGuide layoutAttribute:NSLayoutAttributeTop];
}
- (MASViewAttribute *)mas_bottomLayoutGuideBottom {
return [[MASViewAttribute alloc] initWithView:self.view item:self.bottomLayoutGuide layoutAttribute:NSLayoutAttributeBottom];
}
@end
#endif

View File

@@ -1,415 +0,0 @@
# Masonry [![Build Status](https://travis-ci.org/SnapKit/Masonry.svg?branch=master)](https://travis-ci.org/SnapKit/Masonry) [![Coverage Status](https://img.shields.io/coveralls/SnapKit/Masonry.svg?style=flat-square)](https://coveralls.io/r/SnapKit/Masonry) [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) ![Pod Version](https://img.shields.io/cocoapods/v/Masonry.svg?style=flat)
**Masonry is still actively maintained, we are committed to fixing bugs and merging good quality PRs from the wider community. However if you're using Swift in your project, we recommend using [SnapKit](https://github.com/SnapKit/SnapKit) as it provides better type safety with a simpler API.**
Masonry is a light-weight layout framework which wraps AutoLayout with a nicer syntax. Masonry has its own layout DSL which provides a chainable way of describing your NSLayoutConstraints which results in layout code that is more concise and readable.
Masonry supports iOS and Mac OS X.
For examples take a look at the **Masonry iOS Examples** project in the Masonry workspace. You will need to run `pod install` after downloading.
## What's wrong with NSLayoutConstraints?
Under the hood Auto Layout is a powerful and flexible way of organising and laying out your views. However creating constraints from code is verbose and not very descriptive.
Imagine a simple example in which you want to have a view fill its superview but inset by 10 pixels on every side
```obj-c
UIView *superview = self.view;
UIView *view1 = [[UIView alloc] init];
view1.translatesAutoresizingMaskIntoConstraints = NO;
view1.backgroundColor = [UIColor greenColor];
[superview addSubview:view1];
UIEdgeInsets padding = UIEdgeInsetsMake(10, 10, 10, 10);
[superview addConstraints:@[
//view1 constraints
[NSLayoutConstraint constraintWithItem:view1
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:superview
attribute:NSLayoutAttributeTop
multiplier:1.0
constant:padding.top],
[NSLayoutConstraint constraintWithItem:view1
attribute:NSLayoutAttributeLeft
relatedBy:NSLayoutRelationEqual
toItem:superview
attribute:NSLayoutAttributeLeft
multiplier:1.0
constant:padding.left],
[NSLayoutConstraint constraintWithItem:view1
attribute:NSLayoutAttributeBottom
relatedBy:NSLayoutRelationEqual
toItem:superview
attribute:NSLayoutAttributeBottom
multiplier:1.0
constant:-padding.bottom],
[NSLayoutConstraint constraintWithItem:view1
attribute:NSLayoutAttributeRight
relatedBy:NSLayoutRelationEqual
toItem:superview
attribute:NSLayoutAttributeRight
multiplier:1
constant:-padding.right],
]];
```
Even with such a simple example the code needed is quite verbose and quickly becomes unreadable when you have more than 2 or 3 views.
Another option is to use Visual Format Language (VFL), which is a bit less long winded.
However the ASCII type syntax has its own pitfalls and its also a bit harder to animate as `NSLayoutConstraint constraintsWithVisualFormat:` returns an array.
## Prepare to meet your Maker!
Heres the same constraints created using MASConstraintMaker
```obj-c
UIEdgeInsets padding = UIEdgeInsetsMake(10, 10, 10, 10);
[view1 mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(superview.mas_top).with.offset(padding.top); //with is an optional semantic filler
make.left.equalTo(superview.mas_left).with.offset(padding.left);
make.bottom.equalTo(superview.mas_bottom).with.offset(-padding.bottom);
make.right.equalTo(superview.mas_right).with.offset(-padding.right);
}];
```
Or even shorter
```obj-c
[view1 mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(superview).with.insets(padding);
}];
```
Also note in the first example we had to add the constraints to the superview `[superview addConstraints:...`.
Masonry however will automagically add constraints to the appropriate view.
Masonry will also call `view1.translatesAutoresizingMaskIntoConstraints = NO;` for you.
## Not all things are created equal
> `.equalTo` equivalent to **NSLayoutRelationEqual**
> `.lessThanOrEqualTo` equivalent to **NSLayoutRelationLessThanOrEqual**
> `.greaterThanOrEqualTo` equivalent to **NSLayoutRelationGreaterThanOrEqual**
These three equality constraints accept one argument which can be any of the following:
#### 1. MASViewAttribute
```obj-c
make.centerX.lessThanOrEqualTo(view2.mas_left);
```
MASViewAttribute | NSLayoutAttribute
------------------------- | --------------------------
view.mas_left | NSLayoutAttributeLeft
view.mas_right | NSLayoutAttributeRight
view.mas_top | NSLayoutAttributeTop
view.mas_bottom | NSLayoutAttributeBottom
view.mas_leading | NSLayoutAttributeLeading
view.mas_trailing | NSLayoutAttributeTrailing
view.mas_width | NSLayoutAttributeWidth
view.mas_height | NSLayoutAttributeHeight
view.mas_centerX | NSLayoutAttributeCenterX
view.mas_centerY | NSLayoutAttributeCenterY
view.mas_baseline | NSLayoutAttributeBaseline
#### 2. UIView/NSView
if you want view.left to be greater than or equal to label.left :
```obj-c
//these two constraints are exactly the same
make.left.greaterThanOrEqualTo(label);
make.left.greaterThanOrEqualTo(label.mas_left);
```
#### 3. NSNumber
Auto Layout allows width and height to be set to constant values.
if you want to set view to have a minimum and maximum width you could pass a number to the equality blocks:
```obj-c
//width >= 200 && width <= 400
make.width.greaterThanOrEqualTo(@200);
make.width.lessThanOrEqualTo(@400)
```
However Auto Layout does not allow alignment attributes such as left, right, centerY etc to be set to constant values.
So if you pass a NSNumber for these attributes Masonry will turn these into constraints relative to the view&rsquo;s superview ie:
```obj-c
//creates view.left = view.superview.left + 10
make.left.lessThanOrEqualTo(@10)
```
Instead of using NSNumber, you can use primitives and structs to build your constraints, like so:
```obj-c
make.top.mas_equalTo(42);
make.height.mas_equalTo(20);
make.size.mas_equalTo(CGSizeMake(50, 100));
make.edges.mas_equalTo(UIEdgeInsetsMake(10, 0, 10, 0));
make.left.mas_equalTo(view).mas_offset(UIEdgeInsetsMake(10, 0, 10, 0));
```
By default, macros which support [autoboxing](https://en.wikipedia.org/wiki/Autoboxing#Autoboxing) are prefixed with `mas_`. Unprefixed versions are available by defining `MAS_SHORTHAND_GLOBALS` before importing Masonry.
#### 4. NSArray
An array of a mixture of any of the previous types
```obj-c
make.height.equalTo(@[view1.mas_height, view2.mas_height]);
make.height.equalTo(@[view1, view2]);
make.left.equalTo(@[view1, @100, view3.right]);
````
## Learn to prioritize
> `.priority` allows you to specify an exact priority
> `.priorityHigh` equivalent to **UILayoutPriorityDefaultHigh**
> `.priorityMedium` is half way between high and low
> `.priorityLow` equivalent to **UILayoutPriorityDefaultLow**
Priorities are can be tacked on to the end of a constraint chain like so:
```obj-c
make.left.greaterThanOrEqualTo(label.mas_left).with.priorityLow();
make.top.equalTo(label.mas_top).with.priority(600);
```
## Composition, composition, composition
Masonry also gives you a few convenience methods which create multiple constraints at the same time. These are called MASCompositeConstraints
#### edges
```obj-c
// make top, left, bottom, right equal view2
make.edges.equalTo(view2);
// make top = superview.top + 5, left = superview.left + 10,
// bottom = superview.bottom - 15, right = superview.right - 20
make.edges.equalTo(superview).insets(UIEdgeInsetsMake(5, 10, 15, 20))
```
#### size
```obj-c
// make width and height greater than or equal to titleLabel
make.size.greaterThanOrEqualTo(titleLabel)
// make width = superview.width + 100, height = superview.height - 50
make.size.equalTo(superview).sizeOffset(CGSizeMake(100, -50))
```
#### center
```obj-c
// make centerX and centerY = button1
make.center.equalTo(button1)
// make centerX = superview.centerX - 5, centerY = superview.centerY + 10
make.center.equalTo(superview).centerOffset(CGPointMake(-5, 10))
```
You can chain view attributes for increased readability:
```obj-c
// All edges but the top should equal those of the superview
make.left.right.and.bottom.equalTo(superview);
make.top.equalTo(otherView);
```
## Hold on for dear life
Sometimes you need modify existing constraints in order to animate or remove/replace constraints.
In Masonry there are a few different approaches to updating constraints.
#### 1. References
You can hold on to a reference of a particular constraint by assigning the result of a constraint make expression to a local variable or a class property.
You could also reference multiple constraints by storing them away in an array.
```obj-c
// in public/private interface
@property (nonatomic, strong) MASConstraint *topConstraint;
...
// when making constraints
[view1 mas_makeConstraints:^(MASConstraintMaker *make) {
self.topConstraint = make.top.equalTo(superview.mas_top).with.offset(padding.top);
make.left.equalTo(superview.mas_left).with.offset(padding.left);
}];
...
// then later you can call
[self.topConstraint uninstall];
```
#### 2. mas_updateConstraints
Alternatively if you are only updating the constant value of the constraint you can use the convience method `mas_updateConstraints` instead of `mas_makeConstraints`
```obj-c
// this is Apple's recommended place for adding/updating constraints
// this method can get called multiple times in response to setNeedsUpdateConstraints
// which can be called by UIKit internally or in your code if you need to trigger an update to your constraints
- (void)updateConstraints {
[self.growingButton mas_updateConstraints:^(MASConstraintMaker *make) {
make.center.equalTo(self);
make.width.equalTo(@(self.buttonSize.width)).priorityLow();
make.height.equalTo(@(self.buttonSize.height)).priorityLow();
make.width.lessThanOrEqualTo(self);
make.height.lessThanOrEqualTo(self);
}];
//according to apple super should be called at end of method
[super updateConstraints];
}
```
### 3. mas_remakeConstraints
`mas_updateConstraints` is useful for updating a set of constraints, but doing anything beyond updating constant values can get exhausting. That's where `mas_remakeConstraints` comes in.
`mas_remakeConstraints` is similar to `mas_updateConstraints`, but instead of updating constant values, it will remove all of its constraints before installing them again. This lets you provide different constraints without having to keep around references to ones which you want to remove.
```obj-c
- (void)changeButtonPosition {
[self.button mas_remakeConstraints:^(MASConstraintMaker *make) {
make.size.equalTo(self.buttonSize);
if (topLeft) {
make.top.and.left.offset(10);
} else {
make.bottom.and.right.offset(-10);
}
}];
}
```
You can find more detailed examples of all three approaches in the **Masonry iOS Examples** project.
## When the ^&*!@ hits the fan!
Laying out your views doesn't always goto plan. So when things literally go pear shaped, you don't want to be looking at console output like this:
```obj-c
Unable to simultaneously satisfy constraints.....blah blah blah....
(
"<NSLayoutConstraint:0x7189ac0 V:[UILabel:0x7186980(>=5000)]>",
"<NSAutoresizingMaskLayoutConstraint:0x839ea20 h=--& v=--& V:[MASExampleDebuggingView:0x7186560(416)]>",
"<NSLayoutConstraint:0x7189c70 UILabel:0x7186980.bottom == MASExampleDebuggingView:0x7186560.bottom - 10>",
"<NSLayoutConstraint:0x7189560 V:|-(1)-[UILabel:0x7186980] (Names: '|':MASExampleDebuggingView:0x7186560 )>"
)
Will attempt to recover by breaking constraint
<NSLayoutConstraint:0x7189ac0 V:[UILabel:0x7186980(>=5000)]>
```
Masonry adds a category to NSLayoutConstraint which overrides the default implementation of `- (NSString *)description`.
Now you can give meaningful names to views and constraints, and also easily pick out the constraints created by Masonry.
which means your console output can now look like this:
```obj-c
Unable to simultaneously satisfy constraints......blah blah blah....
(
"<NSAutoresizingMaskLayoutConstraint:0x8887740 MASExampleDebuggingView:superview.height == 416>",
"<MASLayoutConstraint:ConstantConstraint UILabel:messageLabel.height >= 5000>",
"<MASLayoutConstraint:BottomConstraint UILabel:messageLabel.bottom == MASExampleDebuggingView:superview.bottom - 10>",
"<MASLayoutConstraint:ConflictingConstraint[0] UILabel:messageLabel.top == MASExampleDebuggingView:superview.top + 1>"
)
Will attempt to recover by breaking constraint
<MASLayoutConstraint:ConstantConstraint UILabel:messageLabel.height >= 5000>
```
For an example of how to set this up take a look at the **Masonry iOS Examples** project in the Masonry workspace.
## Where should I create my constraints?
```objc
@implementation DIYCustomView
- (id)init {
self = [super init];
if (!self) return nil;
// --- Create your views here ---
self.button = [[UIButton alloc] init];
return self;
}
// tell UIKit that you are using AutoLayout
+ (BOOL)requiresConstraintBasedLayout {
return YES;
}
// this is Apple's recommended place for adding/updating constraints
- (void)updateConstraints {
// --- remake/update constraints here
[self.button remakeConstraints:^(MASConstraintMaker *make) {
make.width.equalTo(@(self.buttonSize.width));
make.height.equalTo(@(self.buttonSize.height));
}];
//according to apple super should be called at end of method
[super updateConstraints];
}
- (void)didTapButton:(UIButton *)button {
// --- Do your changes ie change variables that affect your layout etc ---
self.buttonSize = CGSize(200, 200);
// tell constraints they need updating
[self setNeedsUpdateConstraints];
}
@end
```
## Installation
Use the [orsome](http://www.youtube.com/watch?v=YaIZF8uUTtk) [CocoaPods](http://github.com/CocoaPods/CocoaPods).
In your Podfile
>`pod 'Masonry'`
If you want to use masonry without all those pesky 'mas_' prefixes. Add #define MAS_SHORTHAND to your prefix.pch before importing Masonry
>`#define MAS_SHORTHAND`
Get busy Masoning
>`#import "Masonry.h"`
## Code Snippets
Copy the included code snippets to ``~/Library/Developer/Xcode/UserData/CodeSnippets`` to write your masonry blocks at lightning speed!
`mas_make` -> ` [<#view#> mas_makeConstraints:^(MASConstraintMaker *make) {
<#code#>
}];`
`mas_update` -> ` [<#view#> mas_updateConstraints:^(MASConstraintMaker *make) {
<#code#>
}];`
`mas_remake` -> ` [<#view#> mas_remakeConstraints:^(MASConstraintMaker *make) {
<#code#>
}];`
## Features
* Not limited to subset of Auto Layout. Anything NSLayoutConstraint can do, Masonry can do too!
* Great debug support, give your views and constraints meaningful names.
* Constraints read like sentences.
* No crazy macro magic. Masonry won't pollute the global namespace with macros.
* Not string or dictionary based and hence you get compile time checking.
## TODO
* Eye candy
* Mac example project
* More tests and examples

View File

@@ -0,0 +1,50 @@
//
// KBKey.h
// CustomKeyboard
//
// 简单的键位数据模型,用于描述键盘上的一个键。
//
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
typedef NS_ENUM(NSInteger, KBKeyType) {
KBKeyTypeCharacter = 0, // 普通字符输出
KBKeyTypeBackspace, // 删除
KBKeyTypeShift, // 大小写切换
KBKeyTypeModeChange, // 模式切换(如 123/ABC
KBKeyTypeSpace, // 空格
KBKeyTypeReturn, // 回车/发送
KBKeyTypeGlobe, // 系统地球键
KBKeyTypeCustom, // 自定义功能占位(如 AI/Emoji
KBKeyTypeSymbolsToggle // 数字面板内的“#+=/123”切换
};
FOUNDATION_EXPORT NSString * const KBKeyIdentifierEmojiPanel;
/// 字母键的大小写变体标记(非字母键使用 KBKeyCaseVariantNone
typedef NS_ENUM(NSInteger, KBKeyCaseVariant) {
KBKeyCaseVariantNone = 0,
KBKeyCaseVariantLower = 1,
KBKeyCaseVariantUpper = 2,
};
@interface KBKey : NSObject
@property (nonatomic, assign) KBKeyType type;
@property (nonatomic, copy) NSString *title; // 显示标题
@property (nonatomic, copy) NSString *output; // 字符键插入的文本
/// 逻辑按键标识,用于皮肤映射(如 @"letter_q" @"space" @"backspace"
@property (nonatomic, copy, nullable) NSString *identifier;
/// 字母键的大小写变体(便于皮肤为大小写准备不同图)
@property (nonatomic, assign) KBKeyCaseVariant caseVariant;
+ (instancetype)keyWithTitle:(NSString *)title output:(NSString *)output;
+ (instancetype)keyWithTitle:(NSString *)title type:(KBKeyType)type;
/// 通用构造方法:用于指定 identifier便于皮肤做精细控制
+ (instancetype)keyWithIdentifier:(nullable NSString *)identifier
title:(NSString *)title
output:(NSString *)output
type:(KBKeyType)type;
@end

View File

@@ -0,0 +1,43 @@
//
// KBKey.m
// CustomKeyboard
//
#import "KBKey.h"
NSString * const KBKeyIdentifierEmojiPanel = @"emoji_panel";
@implementation KBKey
+ (instancetype)keyWithTitle:(NSString *)title output:(NSString *)output {
KBKey *k = [[KBKey alloc] init];
k.type = KBKeyTypeCharacter;
k.title = title ?: @"";
k.output = output ?: title ?: @"";
k.caseVariant = KBKeyCaseVariantNone;
return k;
}
+ (instancetype)keyWithTitle:(NSString *)title type:(KBKeyType)type {
KBKey *k = [[KBKey alloc] init];
k.type = type;
k.title = title ?: @"";
k.output = @"";
k.caseVariant = KBKeyCaseVariantNone;
return k;
}
+ (instancetype)keyWithIdentifier:(NSString *)identifier
title:(NSString *)title
output:(NSString *)output
type:(KBKeyType)type {
KBKey *k = [[KBKey alloc] init];
k.type = type;
k.identifier = identifier;
k.title = title ?: @"";
k.output = output ?: @"";
k.caseVariant = KBKeyCaseVariantNone;
return k;
}
@end

View File

@@ -0,0 +1,43 @@
//
// KBKeyboardSubscriptionProduct.h
// CustomKeyboard
//
// 订阅商品模型(键盘扩展专用),用于展示与主 App 相同的订阅列表。
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface KBKeyboardSubscriptionProduct : NSObject
/// 主键 id
@property (nonatomic, assign) NSInteger identifier;
/// Apple 商品编号
@property (nonatomic, copy, nullable) NSString *productId;
/// 商品名称,如 Monthly
@property (nonatomic, copy, nullable) NSString *name;
/// 单位,如 Subscription
@property (nonatomic, copy, nullable) NSString *unit;
/// 商品描述
@property (nonatomic, copy, nullable) NSString *productDescription;
/// 货币符号
@property (nonatomic, copy, nullable) NSString *currency;
/// 现价
@property (nonatomic, assign) double price;
/// 原价(如接口未返回,则回退为 price 的 1.25 倍)
@property (nonatomic, assign) double originPrice;
/// 有效期数值
@property (nonatomic, assign) NSInteger durationValue;
/// 有效期单位
@property (nonatomic, copy, nullable) NSString *durationUnit;
/// 标题(描述 > name+unit > name > unit
- (NSString *)displayTitle;
/// 当前价格文本
- (NSString *)priceDisplayText;
/// 划线价文本
- (nullable NSString *)strikePriceDisplayText;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,55 @@
//
// KBKeyboardSubscriptionProduct.m
// CustomKeyboard
//
#import "KBKeyboardSubscriptionProduct.h"
#import <MJExtension/MJExtension.h>
#import "KBLocalizationManager.h"
@implementation KBKeyboardSubscriptionProduct
+ (NSDictionary *)mj_replacedKeyFromPropertyName {
return @{
@"identifier": @"id",
@"productDescription": @"description",
};
}
- (NSString *)displayTitle {
if (self.productDescription.length > 0) {
return self.productDescription;
}
NSString *name = self.name ?: @"";
NSString *unit = self.unit ?: @"";
if (name.length && unit.length) {
return [NSString stringWithFormat:@"%@ %@", name, unit];
}
if (name.length) { return name; }
if (unit.length) { return unit; }
if (self.durationValue > 0 && self.durationUnit.length > 0) {
return [NSString stringWithFormat:@"%ld %@", (long)self.durationValue, self.durationUnit];
}
return KBLocalized(@"Subscription");
}
- (NSString *)priceDisplayText {
double priceValue = self.price;
if (priceValue <= 0) {
return @"$0.00";
}
NSString *currency = self.currency.length ? self.currency : @"$";
return [NSString stringWithFormat:@"%@%.2f", currency, priceValue];
}
- (nullable NSString *)strikePriceDisplayText {
double rawValue = self.originPrice;
if (rawValue <= 0 && self.price > 0) {
rawValue = self.price * 1.25;
}
if (rawValue <= 0) { return nil; }
NSString *currency = self.currency.length ? self.currency : @"$";
return [NSString stringWithFormat:@"%@%.2f", currency, rawValue];
}
@end

View File

@@ -0,0 +1,69 @@
//
// KBNetworkManager.h
// CustomKeyboard
//
// 轻量网络层封装(扩展安全)。支持 GET/POST(JSON)。
// 注意:键盘扩展需要"允许完全访问"后才可联网,
// 建议由宿主控制器在确认后调用 `setEnabled:YES` 再发起请求。
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
extern NSErrorDomain const KBNetworkErrorDomain;
typedef NS_ERROR_ENUM(KBNetworkErrorDomain, KBNetworkError) {
KBNetworkErrorDisabled = 1, // 未启用网络(例如未开启完全访问)
KBNetworkErrorInvalidURL = 2,
KBNetworkErrorInvalidResponse = 3,
KBNetworkErrorDecodeFailed = 4,
};
/// JSON 回调(扩展侧目前很少使用 JSON可按需扩展
typedef void(^KBNetworkCompletion)(NSDictionary *_Nullable json,
NSURLResponse * _Nullable response,
NSError * _Nullable error);
/// 二进制回调:用于下载 zip、图片等原始数据
typedef void(^KBNetworkDataCompletion)(NSData *_Nullable data,
NSURLResponse *_Nullable response,
NSError *_Nullable error);
@interface KBNetworkManager : NSObject
/// 单例
+ (instancetype)shared;
/// 是否允许网络(默认为 NO宿主在合适时机置 YES
@property (atomic, assign, getter=isEnabled) BOOL enabled;
/// 可选的基础域名,例如 https://api.example.com
@property (nonatomic, strong, nullable) NSURL *baseURL;
/// 全局默认请求头(每次请求会与局部 headers 合并,局部优先)
@property (nonatomic, copy) NSDictionary<NSString *, NSString *> *defaultHeaders;
/// 超时时间(默认 10s
@property (nonatomic, assign) NSTimeInterval timeout;
/// GET 请求parameters 会拼到 URL 上
- (nullable NSURLSessionDataTask *)GET:(NSString *)path
parameters:(nullable NSDictionary *)parameters
headers:(nullable NSDictionary<NSString *, NSString *> *)headers
completion:(KBNetworkCompletion)completion;
/// GET 原始二进制数据(不做 JSON 解析)
- (nullable NSURLSessionDataTask *)GETData:(NSString *)path
parameters:(nullable NSDictionary *)parameters
headers:(nullable NSDictionary<NSString *, NSString *> *)headers
completion:(KBNetworkDataCompletion)completion;
/// POST JSON 请求jsonBody 会以 application/json 发送
- (nullable NSURLSessionDataTask *)POST:(NSString *)path
jsonBody:(nullable id)jsonBody
headers:(nullable NSDictionary<NSString *, NSString *> *)headers
completion:(KBNetworkCompletion)completion;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,306 @@
//
// KBNetworkManager.m
// CustomKeyboard
//
#import "KBNetworkManager.h"
#import "AFNetworking.h"
#import "KBAuthManager.h"
//#import "KBUserSessionManager.h"
#import "KBSignUtils.h"
NSErrorDomain const KBNetworkErrorDomain = @"com.company.keyboard.network";
@interface KBNetworkManager ()
@property (nonatomic, strong) AFHTTPSessionManager *manager; // AFN ephemeral
//
- (void)fail:(KBNetworkError)code completion:(KBNetworkCompletion)completion;
@end
@implementation KBNetworkManager
+ (instancetype)shared {
static KBNetworkManager *m; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ m = [KBNetworkManager new]; });
return m;
}
- (instancetype)init {
if (self = [super init]) {
_enabled = NO; //
_timeout = 10.0;
// Accept + 使 Accept-Language
NSString *lang = [KBLocalizationManager shared].currentLanguageCode ?: KBLanguageCodeEnglish;
// NSString *token = [KBUserSessionManager shared].accessToken ? [KBUserSessionManager shared].accessToken : @"";
_defaultHeaders = @{
@"Accept": @"*/*",
@"Accept-Language": lang
};
//
_baseURL = [NSURL URLWithString:KB_BASE_URL];
}
return self;
}
- (void)getSignWithParare:(NSDictionary *)bodyParams{
NSString *appId = @"loveKeyboard";
NSString *secret = @"kZJM39HYvhxwbJkG1fmquQRVkQiLAh2H"; //
NSString *timestamp = [KBSignUtils currentTimestamp];
NSString *nonce = [KBSignUtils generateNonceWithLength:16];
// 1.
NSMutableDictionary<NSString *, NSString *> *signParams = [NSMutableDictionary dictionary];
signParams[@"appId"] = appId;
signParams[@"timestamp"] = timestamp;
signParams[@"nonce"] = nonce;
// body
[bodyParams enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
if ([obj isKindOfClass:[NSString class]]) {
signParams[key] = obj;
} else {
signParams[key] = [obj description];
}
}];
NSString *sign = [KBSignUtils signWithParams:signParams secret:secret];
//
NSMutableDictionary<NSString *, NSString *> *headers =
[self.defaultHeaders mutableCopy] ?: [NSMutableDictionary dictionary];
if (sign.length > 0) {
headers[@"X-Sign"] = sign;
}
headers[@"X-App-Id"] = appId;
headers[@"X-Timestamp"] = timestamp;
headers[@"X-Nonce"] = nonce;
// copy
self.defaultHeaders = headers;
}
#pragma mark - Public
- (NSURLSessionDataTask *)GET:(NSString *)path
parameters:(NSDictionary *)parameters
headers:(NSDictionary<NSString *,NSString *> *)headers
completion:(KBNetworkCompletion)completion {
[self getSignWithParare:parameters];
if (![self ensureEnabled:completion]) return nil;
NSString *urlString = [self buildURLStringWithPath:path];
if (!urlString) { [self fail:KBNetworkErrorInvalidURL completion:completion]; return nil; }
// 使 AFHTTPRequestSerializer
AFHTTPRequestSerializer *serializer = [AFHTTPRequestSerializer serializer];
serializer.timeoutInterval = self.timeout;
NSError *serror = nil;
NSMutableURLRequest *req = [serializer requestWithMethod:@"GET"
URLString:urlString
parameters:parameters
error:&serror];
if (serror || !req) {
if (completion) completion(nil, nil, serror ?: [NSError errorWithDomain:KBNetworkErrorDomain code:KBNetworkErrorInvalidURL userInfo:@{NSLocalizedDescriptionKey: KBLocalized(@"Invalid URL")}]);
return nil;
}
[self applyHeaders:headers toMutableRequest:req contentType:nil];
return [self startAFJSONTaskWithRequest:req completion:completion];
}
- (NSURLSessionDataTask *)POST:(NSString *)path
jsonBody:(id)jsonBody
headers:(NSDictionary<NSString *,NSString *> *)headers
completion:(KBNetworkCompletion)completion {
[self getSignWithParare:jsonBody];
if (![self ensureEnabled:completion]) return nil;
NSString *urlString = [self buildURLStringWithPath:path];
if (!urlString) { [self fail:KBNetworkErrorInvalidURL completion:completion]; return nil; }
// JSON JSON Body
AFJSONRequestSerializer *serializer = [AFJSONRequestSerializer serializer];
serializer.timeoutInterval = self.timeout;
NSError *error = nil;
NSMutableURLRequest *req = [serializer requestWithMethod:@"POST"
URLString:urlString
parameters:jsonBody
error:&error];
if (error) { if (completion) completion(nil, nil, error); return nil; }
[self applyHeaders:headers toMutableRequest:req contentType:nil];
return [self startAFJSONTaskWithRequest:req completion:completion];
}
- (NSURLSessionDataTask *)GETData:(NSString *)path
parameters:(NSDictionary *)parameters
headers:(NSDictionary<NSString *,NSString *> *)headers
completion:(KBNetworkDataCompletion)completion {
[self getSignWithParare:parameters];
if (!self.isEnabled) {
NSError *e = [NSError errorWithDomain:KBNetworkErrorDomain
code:KBNetworkErrorDisabled
userInfo:@{NSLocalizedDescriptionKey: KBLocalized(@"Network disabled (Full Access may be off)")}];
if (completion) completion(nil, nil, e);
return nil;
}
NSString *urlString = [self buildURLStringWithPath:path];
if (!urlString) {
NSError *e = [NSError errorWithDomain:KBNetworkErrorDomain
code:KBNetworkErrorInvalidURL
userInfo:@{NSLocalizedDescriptionKey: KBLocalized(@"Invalid URL")}];
if (completion) completion(nil, nil, e);
return nil;
}
AFHTTPRequestSerializer *serializer = [AFHTTPRequestSerializer serializer];
serializer.timeoutInterval = self.timeout;
NSError *serror = nil;
NSMutableURLRequest *req = [serializer requestWithMethod:@"GET"
URLString:urlString
parameters:parameters
error:&serror];
if (serror || !req) {
if (completion) completion(nil, nil, serror ?: [NSError errorWithDomain:KBNetworkErrorDomain code:KBNetworkErrorInvalidURL userInfo:@{NSLocalizedDescriptionKey: KBLocalized(@"Invalid URL")}]);
return nil;
}
[self applyHeaders:headers toMutableRequest:req contentType:nil];
return [self startAFDataTaskWithRequest:req completion:completion];
}
#pragma mark - Core
- (BOOL)ensureEnabled:(KBNetworkCompletion)completion {
if (!self.isEnabled) {
NSError *e = [NSError errorWithDomain:KBNetworkErrorDomain code:KBNetworkErrorDisabled userInfo:@{NSLocalizedDescriptionKey: KBLocalized(@"Network disabled (Full Access may be off)")}];
if (completion) completion(nil, nil, e);
return NO;
}
return YES;
}
- (NSString *)buildURLStringWithPath:(NSString *)path {
if (path.length == 0) return nil;
if ([path hasPrefix:@"http://"] || [path hasPrefix:@"https://"]) {
return path;
}
if (self.baseURL) {
// base / path / base
NSString *base = self.baseURL.absoluteString ?: @"";
if (![base hasSuffix:@"/"]) { base = [base stringByAppendingString:@"/"]; }
NSURL *dirBase = [NSURL URLWithString:base];
NSString *relative = ([path hasPrefix:@"/"]) ? [path substringFromIndex:1] : path;
return [NSURL URLWithString:relative relativeToURL:dirBase].absoluteURL.absoluteString;
}
return path; // baseURL path URL AFN
}
- (void)applyHeaders:(NSDictionary<NSString *,NSString *> *)headers toMutableRequest:(NSMutableURLRequest *)req contentType:(NSString *)contentType {
//
NSMutableDictionary *all = [self.defaultHeaders mutableCopy] ?: [NSMutableDictionary new];
NSString *token = [KBAuthManager shared].current.accessToken;
if (token.length > 0) {
all[@"auth-token"] = token;
} else {
[all removeObjectForKey:@"auth-token"];
}
NSDictionary *auth = [[KBAuthManager shared] authorizationHeader];
[auth enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *obj, BOOL *stop) { all[key] = obj; }];
if (contentType) all[@"Content-Type"] = contentType;
[headers enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *obj, BOOL *stop) { all[key] = obj; }];
[all enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *obj, BOOL *stop) { [req setValue:obj forHTTPHeaderField:key]; }];
}
- (NSURLSessionDataTask *)startAFJSONTaskWithRequest:(NSURLRequest *)req completion:(KBNetworkCompletion)completion {
// Content-Type JSON
self.manager.responseSerializer = [AFHTTPResponseSerializer serializer];
NSURLSessionDataTask *task = [self.manager dataTaskWithRequest:req uploadProgress:nil downloadProgress:nil completionHandler:^(NSURLResponse *response, id responseObject, NSError *error) {
// AFN 2xx error
if (error) {
if (completion) completion(nil, response, error);
return;
}
NSData *data = (NSData *)responseObject;
if (![data isKindOfClass:[NSData class]]) {
if (completion) completion(nil, response, [NSError errorWithDomain:KBNetworkErrorDomain code:KBNetworkErrorInvalidResponse userInfo:@{NSLocalizedDescriptionKey:KBLocalized(@"No data")}]);
return;
}
NSString *ct = nil;
if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
ct = ((NSHTTPURLResponse *)response).allHeaderFields[@"Content-Type"];
}
// JSON Content-Type json { / [
BOOL looksJSON = (ct && [[ct lowercaseString] containsString:@"json"]);
if (!looksJSON) {
//
const unsigned char *bytes = data.bytes;
NSUInteger len = data.length;
for (NSUInteger i = 0; !looksJSON && i < len; i++) {
unsigned char c = bytes[i];
if (c == ' ' || c == '\n' || c == '\r' || c == '\t') continue;
looksJSON = (c == '{' || c == '[');
break;
}
}
if (looksJSON) {
NSError *jsonErr = nil;
id json = [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonErr];
if (jsonErr) {
if (completion) completion(nil, response, [NSError errorWithDomain:KBNetworkErrorDomain code:KBNetworkErrorDecodeFailed userInfo:@{NSLocalizedDescriptionKey:KBLocalized(@"Failed to parse JSON")}]);
return;
}
if (![json isKindOfClass:[NSDictionary class]]) {
if (completion) completion(nil, response, [NSError errorWithDomain:KBNetworkErrorDomain code:KBNetworkErrorInvalidResponse userInfo:@{NSLocalizedDescriptionKey:KBLocalized(@"Invalid response")}]);
return;
}
if (completion) completion((NSDictionary *)json, response, nil);
} else {
if (completion) completion(nil, response, [NSError errorWithDomain:KBNetworkErrorDomain code:KBNetworkErrorInvalidResponse userInfo:@{NSLocalizedDescriptionKey:KBLocalized(@"Invalid response")}]);
}
}];
[task resume];
return task;
}
- (NSURLSessionDataTask *)startAFDataTaskWithRequest:(NSURLRequest *)req completion:(KBNetworkDataCompletion)completion {
self.manager.responseSerializer = [AFHTTPResponseSerializer serializer];
NSURLSessionDataTask *task = [self.manager dataTaskWithRequest:req uploadProgress:nil downloadProgress:nil completionHandler:^(NSURLResponse *response, id responseObject, NSError *error) {
if (error) {
if (completion) completion(nil, response, error);
return;
}
NSData *data = (NSData *)responseObject;
if (![data isKindOfClass:[NSData class]]) {
if (completion) completion(nil, response, [NSError errorWithDomain:KBNetworkErrorDomain code:KBNetworkErrorInvalidResponse userInfo:@{NSLocalizedDescriptionKey:KBLocalized(@"No data")}]);
return;
}
if (completion) completion(data, response, nil);
}];
[task resume];
return task;
}
#pragma mark - AFHTTPSessionManager
- (AFHTTPSessionManager *)manager {
if (!_manager) {
NSURLSessionConfiguration *cfg = [NSURLSessionConfiguration ephemeralSessionConfiguration];
cfg.requestCachePolicy = NSURLRequestReloadIgnoringLocalCacheData;
// per-request serializer.timeoutInterval
if (@available(iOS 11.0, *)) { cfg.waitsForConnectivity = YES; }
_manager = [[AFHTTPSessionManager alloc] initWithBaseURL:nil sessionConfiguration:cfg];
// 使 JSON
_manager.responseSerializer = [AFHTTPResponseSerializer serializer];
}
return _manager;
}
#pragma mark - Private helpers
- (void)fail:(KBNetworkError)code completion:(KBNetworkCompletion)completion {
NSString *msg = KBLocalized(@"Network error");
switch (code) {
case KBNetworkErrorDisabled: msg = KBLocalized(@"Network disabled (Full Access may be off)"); break;
case KBNetworkErrorInvalidURL: msg = KBLocalized(@"Invalid URL"); break;
case KBNetworkErrorInvalidResponse: msg = KBLocalized(@"Invalid response"); break;
case KBNetworkErrorDecodeFailed: msg = KBLocalized(@"Parse failed"); break;
default: break;
}
NSError *e = [NSError errorWithDomain:KBNetworkErrorDomain
code:code
userInfo:@{NSLocalizedDescriptionKey: msg}];
if (completion) completion(nil, nil, e);
}
@end

View File

@@ -0,0 +1,60 @@
//
// KBStreamFetcher.h
// CustomKeyboard
//
// 轻量网络流拉取器:支持纯文本分块与 SSE(text/event-stream) 两种形式的“边下边显”。
// - 增量解码:按 UTF-8 安全前缀逐步转成字符串,避免半个多字节字符导致阻塞/乱码
// - SSE 解析:按 \n\n 切事件,合并 data: 行,移除前缀,仅回传正文
// - 兼容后端“/t”作为分段标记可自动替换为制表符“\t”
// - 首段去首个“\t”若首次正文以一个制表符起始允许前导空白可只移除“一个”\t
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
typedef void (^KBStreamFetcherChunkHandler)(NSString *chunk);
typedef void (^KBStreamFetcherFinishHandler)(NSError *_Nullable error);
@interface KBStreamFetcher : NSObject <NSURLSessionDataDelegate>
// 便利构造
+ (instancetype)fetcherWithURL:(NSURL *)url;
// 必填:请求地址
@property (nonatomic, strong) NSURL *url;
/// HTTP Method默认为 GET
@property (nonatomic, copy, nullable) NSString *httpMethod;
/// 自定义请求体(例如 POST 的 JSON body
@property (nonatomic, strong, nullable) NSData *httpBody;
// 可选 Header
@property (nonatomic, copy, nullable) NSDictionary<NSString *, NSString *> *extraHeaders;
// 配置项(默认值见注释)
@property (nonatomic, assign) BOOL acceptEventStream; // 默认 NO置 YES 时发送 Accept: text/event-stream
@property (nonatomic, assign) BOOL disableCompression; // 默认 YES发送 Accept-Encoding: identity
@property (nonatomic, assign) BOOL treatSlashTAsTab; // 默认 YES将“/t”替换为“\t”
@property (nonatomic, assign) BOOL trimLeadingTabOnce; // 默认 YES首次正文起始的“\t”删一个忽略前导空白
@property (nonatomic, assign) NSTimeInterval requestTimeout; // 默认 30s
/// UI 刷新节奏:当一次回调内解析出多段(如多条 SSE 事件)时,按该间隔逐条回调(默认 0.10s)。
@property (nonatomic, assign) NSTimeInterval flushInterval;
/// 非 SSE 且一次性拿到大段文本时,是否按空格切词逐条回调(模拟“逐词流式”),默认 YES。
@property (nonatomic, assign) BOOL splitLargeDeltasOnWhitespace;
/// 调试日志:默认 YES。输出起止时刻、首包耗时、各分片内容截断等关键信息。
@property (nonatomic, assign) BOOL loggingEnabled;
// 回调(统一在主线程触发)
@property (nonatomic, copy, nullable) KBStreamFetcherChunkHandler onChunk;
@property (nonatomic, copy, nullable) KBStreamFetcherFinishHandler onFinish;
// 控制
- (void)start;
- (void)cancel;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,519 @@
//
// KBStreamFetcher.m
//
#import "KBStreamFetcher.h"
#import "KBLocalizationManager.h"
@interface KBStreamFetcher () <NSURLSessionDataDelegate>
@property (nonatomic, strong) NSURLSession *session;
@property (nonatomic, strong) NSURLSessionDataTask *task;
@property (nonatomic, strong) NSMutableData *buffer; //
@property (nonatomic, assign) NSStringEncoding textEncoding; // UTF-8
@property (nonatomic, assign) BOOL isSSE; // SSE
@property (nonatomic, strong) NSMutableString *sseTextBuffer; // SSE
@property (nonatomic, assign) NSInteger decodedPrefixBytes; // sseTextBuffer SSE
@property (nonatomic, assign) NSInteger deliveredCharCount; // SSE
@property (nonatomic, assign) BOOL hasEmitted; // 1 \t
@property (nonatomic, assign) BOOL lastChunkEndedWithTab; // "\t" \t
@property (nonatomic, strong) NSMutableArray<NSString *> *pendingQueue; //
@property (nonatomic, strong) NSTimer *flushTimer; //
@property (nonatomic, strong, nullable) NSError *finishError; //
@property (nonatomic, copy, nullable) NSString *pendingSplitTokenPrefix; // `<SPLIT>`
// Metrics
@property (nonatomic, assign) CFAbsoluteTime tStart; // start()
@property (nonatomic, assign) CFAbsoluteTime tFirstByte; //
@property (nonatomic, assign) CFAbsoluteTime tFinish; // /
@property (nonatomic, assign) NSInteger emittedChunkCount; //
@end
// UTF-8
static NSUInteger kb_validUTF8PrefixLen(NSData *data) {
const unsigned char *bytes = (const unsigned char *)data.bytes;
NSUInteger n = data.length;
if (n == 0) return 0;
NSInteger i = (NSInteger)n - 1;
while (i >= 0 && (bytes[i] & 0xC0) == 0x80) { i--; } // 10xxxxxx
if (i < 0) return 0; //
unsigned char b = bytes[i];
NSUInteger expected = 1;
if ((b & 0x80) == 0x00) expected = 1; // 0xxxxxxx
else if ((b & 0xE0) == 0xC0) expected = 2; // 110xxxxx
else if ((b & 0xF0) == 0xE0) expected = 3; // 1110xxxx
else if ((b & 0xF8) == 0xF0) expected = 4; // 11110xxx
else return (NSUInteger)i; // i
NSUInteger remain = n - (NSUInteger)i;
return (remain >= expected) ? n : (NSUInteger)i;
}
static NSString * const kKBStreamSplitToken = @"<SPLIT>";
@implementation KBStreamFetcher
+ (instancetype)fetcherWithURL:(NSURL *)url {
KBStreamFetcher *f = [[self alloc] init];
f.url = url;
return f;
}
- (instancetype)init {
if (self = [super init]) {
_httpMethod = @"GET";
_acceptEventStream = NO;
_disableCompression = YES;
_treatSlashTAsTab = YES;
_trimLeadingTabOnce = YES;
_requestTimeout = 30.0;
_textEncoding = NSUTF8StringEncoding;
_buffer = [NSMutableData data];
_sseTextBuffer = [NSMutableString string];
_pendingQueue = [NSMutableArray array];
_flushInterval = 0.1;
_splitLargeDeltasOnWhitespace = YES;
_loggingEnabled = YES;
_pendingSplitTokenPrefix = nil;
}
return self;
}
- (void)start {
if (!self.url) return;
[self cancel];
NSURLSessionConfiguration *cfg = [NSURLSessionConfiguration defaultSessionConfiguration];
cfg.requestCachePolicy = NSURLRequestReloadIgnoringLocalCacheData;
cfg.timeoutIntervalForRequest = self.requestTimeout;
cfg.timeoutIntervalForResource = MAX(self.requestTimeout, 60.0);
self.session = [NSURLSession sessionWithConfiguration:cfg delegate:self delegateQueue:[NSOperationQueue mainQueue]];
NSMutableURLRequest *req = [NSMutableURLRequest requestWithURL:self.url];
NSString *method = self.httpMethod.length > 0 ? self.httpMethod : @"GET";
req.HTTPMethod = method;
if (self.httpBody.length > 0) {
req.HTTPBody = self.httpBody;
}
if (self.disableCompression) { [req setValue:@"identity" forHTTPHeaderField:@"Accept-Encoding"]; }
if (self.acceptEventStream) { [req setValue:@"text/event-stream" forHTTPHeaderField:@"Accept"]; }
[req setValue:@"no-cache" forHTTPHeaderField:@"Cache-Control"];
[req setValue:@"keep-alive" forHTTPHeaderField:@"Connection"];
[self.extraHeaders enumerateKeysAndObjectsUsingBlock:^(NSString *k, NSString *v, BOOL *stop){ [req setValue:v forHTTPHeaderField:k]; }];
//
[self.buffer setLength:0];
[self.sseTextBuffer setString:@""];
self.isSSE = NO;
self.textEncoding = NSUTF8StringEncoding;
self.decodedPrefixBytes = 0;
self.deliveredCharCount = 0;
self.hasEmitted = NO;
self.lastChunkEndedWithTab = NO;
[self.pendingQueue removeAllObjects];
[self.flushTimer invalidate]; self.flushTimer = nil;
self.finishError = nil;
self.pendingSplitTokenPrefix = nil;
self.tStart = CFAbsoluteTimeGetCurrent();
self.tFirstByte = 0;
self.tFinish = 0;
self.emittedChunkCount = 0;
if (self.loggingEnabled) {
NSLog(@"[KBStream] start url=%@ acceptSSE=%@ disableCompression=%@ flush=%.0fms splitWords=%@",
self.url.absoluteString,
self.acceptEventStream?@"YES":@"NO",
self.disableCompression?@"YES":@"NO",
self.flushInterval*1000.0,
self.splitLargeDeltasOnWhitespace?@"YES":@"NO");
}
self.task = [self.session dataTaskWithRequest:req];
[self.task resume];
}
- (void)cancel {
[self.task cancel];
self.task = nil;
[self.session invalidateAndCancel];
self.session = nil;
[self.buffer setLength:0];
[self.sseTextBuffer setString:@""];
self.decodedPrefixBytes = 0;
self.deliveredCharCount = 0;
self.hasEmitted = NO;
self.lastChunkEndedWithTab = NO;
[self.pendingQueue removeAllObjects];
[self.flushTimer invalidate]; self.flushTimer = nil;
self.finishError = nil;
self.pendingSplitTokenPrefix = nil;
}
#pragma mark - NSURLSessionDataDelegate
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler {
self.isSSE = NO;
self.textEncoding = NSUTF8StringEncoding;
if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
NSHTTPURLResponse *r = (NSHTTPURLResponse *)response;
NSString *ct = r.allHeaderFields[@"Content-Type"] ?: r.allHeaderFields[@"content-type"];
if ([ct isKindOfClass:[NSString class]]) {
NSString *lower = [ct lowercaseString];
if ([lower containsString:@"text/event-stream"]) self.isSSE = YES;
NSRange pos = [lower rangeOfString:@"charset="];
if (pos.location != NSNotFound) {
NSString *charset = [[lower substringFromIndex:pos.location + pos.length] componentsSeparatedByString:@";"][0];
if ([charset containsString:@"utf-8"] || [charset containsString:@"utf8"]) {
self.textEncoding = NSUTF8StringEncoding;
} else if ([charset containsString:@"iso-8859-1"] || [charset containsString:@"latin1"]) {
self.textEncoding = NSISOLatin1StringEncoding;
}
}
}
}
[self.sseTextBuffer setString:@""];
self.decodedPrefixBytes = 0;
if (completionHandler) completionHandler(NSURLSessionResponseAllow);
}
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
if (data.length == 0) return;
[self.buffer appendData:data];
NSUInteger validLen = (self.textEncoding == NSUTF8StringEncoding)
? kb_validUTF8PrefixLen(self.buffer)
: self.buffer.length;
if (validLen > 0 && self.tFirstByte == 0) {
self.tFirstByte = CFAbsoluteTimeGetCurrent();
if (self.loggingEnabled) {
NSLog(@"[KBStream] first-bytes after %.0fms (encoding=%@, SSE=%@)",
(self.tFirstByte - self.tStart)*1000.0,
(self.textEncoding==NSUTF8StringEncoding?@"UTF-8":@"Other"),
self.isSSE?@"YES":@"NO");
}
}
if (validLen == 0) return; //
if (self.isSSE) {
if ((NSUInteger)self.decodedPrefixBytes < validLen) {
NSRange rng = NSMakeRange((NSUInteger)self.decodedPrefixBytes, validLen - (NSUInteger)self.decodedPrefixBytes);
NSString *piece = [[NSString alloc] initWithBytes:(const char *)self.buffer.bytes + rng.location
length:rng.length
encoding:self.textEncoding];
if (piece.length > 0) {
[self.sseTextBuffer appendString:piece];
self.decodedPrefixBytes = (NSInteger)validLen;
}
}
// SSE \n\n
if (self.sseTextBuffer.length > 0) {
NSString *normalized = [self.sseTextBuffer stringByReplacingOccurrencesOfString:@"\r\n" withString:@"\n"];
[self.sseTextBuffer setString:normalized];
while (1) {
NSRange sep = [self.sseTextBuffer rangeOfString:@"\n\n"]; //
if (sep.location == NSNotFound) break;
NSString *event = [self.sseTextBuffer substringToIndex:sep.location];
[self.sseTextBuffer deleteCharactersInRange:NSMakeRange(0, sep.location + sep.length)];
// data:
NSArray<NSString *> *lines = [event componentsSeparatedByString:@"\n"];
NSMutableString *payload = [NSMutableString string];
for (NSString *ln in lines) {
if ([ln hasPrefix:@"data:"]) {
NSString *v = [ln substringFromIndex:5];
if (v.length > 0 && [v hasPrefix:@" "]) v = [v substringFromIndex:1];
[payload appendString:v ?: @""];
}
}
if (payload.length > 0) {
if (self.loggingEnabled) {
NSLog(@"[KBStream] SSE raw payload: %@", payload);
}
NSString *llmText = nil;
if ([self processLLMChunkPayload:payload output:&llmText]) {
if (llmText.length > 0) { [self enqueueChunk:llmText]; }
} else {
[self enqueueChunk:payload];
}
}
}
}
return;
}
// SSE
NSString *prefix = [[NSString alloc] initWithBytes:self.buffer.bytes length:validLen encoding:self.textEncoding];
if (!prefix) return;
if (self.deliveredCharCount < (NSInteger)prefix.length) {
NSString *delta = [prefix substringFromIndex:self.deliveredCharCount];
self.deliveredCharCount = prefix.length;
if (self.splitLargeDeltasOnWhitespace && delta.length > 16) {
// 使
NSArray<NSString *> *parts = [delta componentsSeparatedByString:@" "];
for (NSUInteger i = 0; i < parts.count; i++) {
NSString *w = parts[i];
if (w.length == 0) { [self enqueueChunk:@" "]; continue; }
if (i + 1 < parts.count) {
[self enqueueChunk:[w stringByAppendingString:@" "]];
} else {
[self enqueueChunk:w];
}
}
} else {
[self enqueueChunk:delta];
}
}
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
if (!error && self.isSSE && self.sseTextBuffer.length > 0) {
// \n\n
NSString *normalized = [self.sseTextBuffer stringByReplacingOccurrencesOfString:@"\r\n" withString:@"\n"];
NSArray<NSString *> *lines = [normalized componentsSeparatedByString:@"\n"];
NSMutableString *payload = [NSMutableString string];
for (NSString *ln in lines) {
if ([ln hasPrefix:@"data:"]) {
NSString *v = [ln substringFromIndex:5];
if (v.length > 0 && [v hasPrefix:@" "]) v = [v substringFromIndex:1];
[payload appendString:v ?: @""];
}
}
if (payload.length > 0) {
if (self.loggingEnabled) {
NSLog(@"[KBStream] SSE raw payload: %@", payload);
}
NSString *delta = nil;
if ((NSInteger)payload.length >= self.deliveredCharCount) {
delta = [payload substringFromIndex:self.deliveredCharCount];
} else {
delta = payload;
}
self.deliveredCharCount = payload.length;
if (delta.length > 0) {
NSString *llmText = nil;
if ([self processLLMChunkPayload:delta output:&llmText]) {
if (llmText.length > 0) { [self emitChunk:llmText]; }
} else {
[self emitChunk:delta];
}
}
}
}
if (self.pendingSplitTokenPrefix.length > 0) {
NSString *carry = self.pendingSplitTokenPrefix;
self.pendingSplitTokenPrefix = nil;
if (carry.length > 0) { [self enqueueChunk:carry]; }
}
self.tFinish = CFAbsoluteTimeGetCurrent();
if (self.loggingEnabled) {
double t0 = (self.tFirstByte>0? (self.tFirstByte - self.tStart)*1000.0 : -1);
double t1 = (self.tFirstByte>0? (self.tFinish - self.tFirstByte)*1000.0 : -1);
double tt = (self.tFinish - self.tStart)*1000.0;
NSLog(@"[KBStream] finish chunks=%ld firstByte=%.0fms after start, tail=%.0fms, total=%.0fms error=%@",
(long)self.emittedChunkCount, t0, t1, tt, error);
}
// finish
if (self.pendingQueue.count > 0) {
self.finishError = error;
[self startFlushTimerIfNeeded];
} else {
if (self.onFinish) dispatch_async(dispatch_get_main_queue(), ^{ self.onFinish(error); });
[self cancel];
}
}
#pragma mark - Helpers
- (void)emitChunk:(NSString *)rawText {
if (rawText.length == 0) return;
// 便
if (self.loggingEnabled) {
// NSLog(@"[KBStream] RAW chunk#%ld len=%lu text=\"%@\"",
// (long)(self.emittedChunkCount + 1),
// (unsigned long)rawText.length,
// KBPrintableSnippet(rawText, 160));
}
NSString *text = rawText;
// 0) \r/\n "\n\t""\r\n\t""\r\t" "\t"
text = [text stringByReplacingOccurrencesOfString:@"\r\n\t" withString:@"\t"];
text = [text stringByReplacingOccurrencesOfString:@"\n\t" withString:@"\t"];
text = [text stringByReplacingOccurrencesOfString:@"\r\t" withString:@"\t"];
while (text.length > 0) {
unichar c0 = [text characterAtIndex:0];
if (c0 == '\n' || c0 == '\r') { text = [text substringFromIndex:1]; continue; }
break;
}
// 1) /t -> \t
if (self.treatSlashTAsTab) {
text = [text stringByReplacingOccurrencesOfString:@"/t" withString:@"\t"];
}
// 2) "\t"
if (!self.hasEmitted && self.trimLeadingTabOnce) {
if (text.length > 0 && [text characterAtIndex:0] == '\t') {
NSUInteger start = 1;
if (start < text.length && [text characterAtIndex:start] == ' ') start++;
text = [text substringFromIndex:start];
}
}
// 3) \t -> \t
if (text.length > 0) {
// \t
if (self.lastChunkEndedWithTab) {
NSUInteger j = 0;
while (j < text.length && [text characterAtIndex:j] == ' ') { j++; }
if (j > 0) {
text = [text substringFromIndex:1]; //
}
}
// \t \t
text = [text stringByReplacingOccurrencesOfString:@"\t " withString:@"\t"];
}
if (text.length == 0) { self.lastChunkEndedWithTab = NO; return; }
self.emittedChunkCount += 1;
if (self.loggingEnabled) {
NSLog(@"[KBStream] chunk#%ld len=%lu text=\"%@\"",
(long)self.emittedChunkCount, (unsigned long)text.length, KBPrintableSnippet(text, 160));
}
if (self.onChunk) dispatch_async(dispatch_get_main_queue(), ^{ self.onChunk(text); });
self.hasEmitted = YES;
// \t
unichar lastc = [text characterAtIndex:text.length - 1];
self.lastChunkEndedWithTab = (lastc == '\t');
}
- (BOOL)processLLMChunkPayload:(NSString *)payload output:(NSString * _Nullable __autoreleasing *)output {
if (output) { *output = nil; }
if (payload.length == 0) { return NO; }
NSData *jsonData = [payload dataUsingEncoding:NSUTF8StringEncoding];
if (!jsonData) { return NO; }
NSError *jsonError = nil;
id obj = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&jsonError];
if (jsonError || ![obj isKindOfClass:[NSDictionary class]]) { return NO; }
NSString *type = ((NSDictionary *)obj)[@"type"];
if (![type isKindOfClass:[NSString class]]) { return NO; }
if ([type isEqualToString:@"llm_chunk"]) {
id dataValue = ((NSDictionary *)obj)[@"data"];
if (![dataValue isKindOfClass:[NSString class]]) {
if (output) { *output = @""; }
return YES;
}
NSString *normalized = [self normalizedLLMDataString:(NSString *)dataValue];
if (output) { *output = normalized; }
return YES;
}
if ([type isEqualToString:@"search_result"]) {
NSString *searchText = [self normalizedSearchResultString:((NSDictionary *)obj)[@"data"]];
if (output) { *output = searchText ?: @""; }
return YES;
}
if ([type isEqualToString:@"done"]) {
if (output) { *output = @""; }
return YES;
}
return NO;
}
- (NSString *)normalizedLLMDataString:(NSString *)dataString {
NSString *combined = dataString ?: @"";
if (self.pendingSplitTokenPrefix.length > 0) {
combined = [self.pendingSplitTokenPrefix stringByAppendingString:combined];
self.pendingSplitTokenPrefix = nil;
}
if (combined.length == 0) { return @""; }
NSString *result = [combined stringByReplacingOccurrencesOfString:kKBStreamSplitToken withString:@"\t"];
NSString *suffix = [self pendingSplitPrefixSuffixForString:result];
if (suffix.length > 0) {
self.pendingSplitTokenPrefix = suffix;
result = [result substringToIndex:result.length - suffix.length];
}
return result;
}
- (NSString *)normalizedSearchResultString:(id)dataValue {
if (![dataValue isKindOfClass:[NSArray class]]) { return @""; }
NSArray *list = (NSArray *)dataValue;
NSMutableArray<NSString *> *segments = [NSMutableArray array];
for (NSUInteger i = 0; i < list.count; i++) {
id item = list[i];
NSString *payload = nil;
if ([item isKindOfClass:[NSDictionary class]]) {
id val = ((NSDictionary *)item)[@"payload"];
if ([val isKindOfClass:[NSString class]]) {
payload = (NSString *)val;
}
} else if ([item isKindOfClass:[NSString class]]) {
payload = (NSString *)item;
}
payload = [payload stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
if (payload.length == 0) { continue; }
NSString *line = [NSString stringWithFormat:@"%lu. %@", (unsigned long)(segments.count + 1), payload];
[segments addObject:line];
}
if (segments.count == 0) { return @""; }
NSString *title = KBLocalized(@"Search result");
NSMutableString *text = [NSMutableString string];
[text appendString:@"\t"];
[text appendFormat:@"%@:", title.length > 0 ? title : @"Search result"];
for (NSString *line in segments) {
[text appendString:@"\t"];
[text appendString:line];
}
return text;
}
- (NSString *)pendingSplitPrefixSuffixForString:(NSString *)text {
if (text.length == 0) { return @""; }
NSUInteger tokenLen = kKBStreamSplitToken.length;
if (tokenLen <= 1) { return @""; }
NSUInteger maxLen = MIN(tokenLen - 1, text.length);
for (NSUInteger len = maxLen; len > 0; len--) {
NSString *suffix = [text substringFromIndex:text.length - len];
NSString *prefix = [kKBStreamSplitToken substringToIndex:len];
if ([suffix isEqualToString:prefix]) {
return suffix;
}
}
return @"";
}
#pragma mark - Queue/Flush
- (void)enqueueChunk:(NSString *)s {
if (s.length == 0) return;
[self.pendingQueue addObject:s];
[self startFlushTimerIfNeeded];
}
- (void)startFlushTimerIfNeeded {
if (self.flushTimer) return;
__weak typeof(self) weakSelf = self;
self.flushTimer = [NSTimer scheduledTimerWithTimeInterval:MAX(0.01, self.flushInterval)
repeats:YES
block:^(NSTimer * _Nonnull t) {
__strong typeof(weakSelf) self = weakSelf; if (!self) { [t invalidate]; return; }
if (self.pendingQueue.count == 0) {
[t invalidate]; self.flushTimer = nil;
if (self.finishError || self.finishError == nil) {
NSError *err = self.finishError; self.finishError = nil;
if (self.onFinish) dispatch_async(dispatch_get_main_queue(), ^{ self.onFinish(err); });
[self cancel];
}
return;
}
NSString *first = self.pendingQueue.firstObject;
[self.pendingQueue removeObjectAtIndex:0];
[self emitChunk:first];
}];
}
#pragma mark - Logging helpers
static NSString *KBPrintableSnippet(NSString *s, NSUInteger maxLen) {
if (!s) return @"";
NSString *x = [s stringByReplacingOccurrencesOfString:@"\n" withString:@"\\n"];
if (x.length > maxLen) {
x = [[x substringToIndex:maxLen] stringByAppendingString:@"…"];
}
return x;
}
@end

View File

@@ -0,0 +1,71 @@
//
// NetworkStreamHandler.h
// CustomKeyboard
//
// Created by Mac on 2025/11/12.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
typedef NS_ENUM(NSUInteger, NetworkStreamState) {
NetworkStreamStateIdle,
NetworkStreamStateConnecting,
NetworkStreamStateReceiving,
NetworkStreamStateCompleted,
NetworkStreamStateError
};
@class NetworkStreamHandler;
@protocol NetworkStreamDelegate <NSObject>
@optional
// 接收到数据块
- (void)networkStream:(NetworkStreamHandler *)stream didReceiveData:(NSData *)data;
// 接收到文本数据(如果是文本内容)
- (void)networkStream:(NetworkStreamHandler *)stream didReceiveText:(NSString *)text;
// 进度更新
- (void)networkStream:(NetworkStreamHandler *)stream downloadProgress:(float)progress;
// 状态改变
- (void)networkStream:(NetworkStreamHandler *)stream stateChanged:(NetworkStreamState)state;
// 请求完成
- (void)networkStream:(NetworkStreamHandler *)stream didCompleteWithError:(NSError * _Nullable)error;
@end
typedef void (^NetworkStreamProgressBlock)(float progress);
typedef void (^NetworkStreamDataBlock)(NSData *data);
typedef void (^NetworkStreamTextBlock)(NSString *text);
typedef void (^NetworkStreamCompletionBlock)(NSError * _Nullable error);
@interface NetworkStreamHandler : NSObject <NSURLSessionDataDelegate>
@property (nonatomic, weak) id<NetworkStreamDelegate> delegate;
@property (nonatomic, assign, readonly) NetworkStreamState state;
@property (nonatomic, strong, readonly) NSURLResponse *response;
@property (nonatomic, assign, readonly) long long totalBytesReceived;
// 初始化方法
- (instancetype)initWithURL:(NSURL *)url;
- (instancetype)initWithRequest:(NSURLRequest *)request;
// 开始请求(使用代理回调)
- (void)startRequest;
// 开始请求(使用 Block 回调)
- (void)startRequestWithProgress:(NetworkStreamProgressBlock _Nullable)progress
onData:(NetworkStreamDataBlock _Nullable)dataBlock
onText:(NetworkStreamTextBlock _Nullable)textBlock
completion:(NetworkStreamCompletionBlock _Nullable)completion;
// 取消请求
- (void)cancelRequest;
// 构建默认请求(包含常见的请求头)
+ (NSURLRequest *)createDefaultRequestWithURL:(NSURL *)url method:(NSString *)method;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,253 @@
//
// NetworkStreamHandler.m
// CustomKeyboard
//
// Created by Mac on 2025/11/12.
//
#import "NetworkStreamHandler.h"
@interface NetworkStreamHandler ()
@property (nonatomic, strong) NSURLSession *session;
@property (nonatomic, strong) NSURLSessionDataTask *dataTask;
@property (nonatomic, strong) NSURLRequest *request;
@property (nonatomic, strong) NSMutableData *receivedData;
@property (nonatomic, assign) long long expectedContentLength;
@property (nonatomic, assign) NetworkStreamState state;
@property (nonatomic, strong) NSURLResponse *response;
// Block
@property (nonatomic, copy) NetworkStreamProgressBlock progressBlock;
@property (nonatomic, copy) NetworkStreamDataBlock dataBlock;
@property (nonatomic, copy) NetworkStreamTextBlock textBlock;
@property (nonatomic, copy) NetworkStreamCompletionBlock completionBlock;
@end
@implementation NetworkStreamHandler
- (instancetype)initWithURL:(NSURL *)url {
NSURLRequest *request = [NetworkStreamHandler createDefaultRequestWithURL:url method:@"GET"];
return [self initWithRequest:request];
}
- (instancetype)initWithRequest:(NSURLRequest *)request {
self = [super init];
if (self) {
_request = request;
_receivedData = [NSMutableData data];
_state = NetworkStreamStateIdle;
_totalBytesReceived = 0;
// URLSession
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
config.timeoutIntervalForRequest = 30.0;
config.timeoutIntervalForResource = 300.0;
config.requestCachePolicy = NSURLRequestReloadIgnoringLocalCacheData;
// URLSession
_session = [NSURLSession sessionWithConfiguration:config
delegate:self
delegateQueue:[NSOperationQueue mainQueue]];
}
return self;
}
- (void)dealloc {
[self cancelRequest];
}
#pragma mark - Public Methods
- (void)startRequest {
if (self.state != NetworkStreamStateIdle) {
NSLog(@"Request already in progress");
return;
}
[self updateState:NetworkStreamStateConnecting];
self.dataTask = [self.session dataTaskWithRequest:self.request];
[self.dataTask resume];
}
- (void)startRequestWithProgress:(NetworkStreamProgressBlock)progress
onData:(NetworkStreamDataBlock)dataBlock
onText:(NetworkStreamTextBlock)textBlock
completion:(NetworkStreamCompletionBlock)completion {
self.progressBlock = progress;
self.dataBlock = dataBlock;
self.textBlock = textBlock;
self.completionBlock = completion;
[self startRequest];
}
- (void)cancelRequest {
if (self.dataTask) {
[self.dataTask cancel];
self.dataTask = nil;
}
[self updateState:NetworkStreamStateIdle];
}
+ (NSURLRequest *)createDefaultRequestWithURL:(NSURL *)url method:(NSString *)method {
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
request.HTTPMethod = method;
request.timeoutInterval = 30.0;
//
[request setValue:@"text/html, application/xhtml+xml, application/xml; q=0.9, image/avif, image/webp, image/apng, */*; q=0.8, application/signed-exchange; v=b3; q=0.7" forHTTPHeaderField:@"Accept"];
[request setValue:@"gzip, deflate" forHTTPHeaderField:@"Accept-Encoding"];
[request setValue:@"zh-CN, zh; q=0.9, ko; q=0.8, ja; q=0.7" forHTTPHeaderField:@"Accept-Language"];
[request setValue:@"keep-alive" forHTTPHeaderField:@"Connection"];
[request setValue:@"1" forHTTPHeaderField:@"Upgrade-Insecure-Requests"];
//
NSString *userAgent = @"Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Mobile/15E148 Safari/604.1";
[request setValue:userAgent forHTTPHeaderField:@"User-Agent"];
return [request copy];
}
#pragma mark - Private Methods
- (void)updateState:(NetworkStreamState)newState {
if (_state != newState) {
_state = newState;
//
if ([self.delegate respondsToSelector:@selector(networkStream:stateChanged:)]) {
[self.delegate networkStream:self stateChanged:newState];
}
}
}
- (void)notifyProgress:(float)progress {
if (self.progressBlock) {
self.progressBlock(progress);
}
if ([self.delegate respondsToSelector:@selector(networkStream:downloadProgress:)]) {
[self.delegate networkStream:self downloadProgress:progress];
}
}
- (void)notifyReceivedData:(NSData *)data {
if (self.dataBlock) {
self.dataBlock(data);
}
if ([self.delegate respondsToSelector:@selector(networkStream:didReceiveData:)]) {
[self.delegate networkStream:self didReceiveData:data];
}
//
if (self.textBlock || [self.delegate respondsToSelector:@selector(networkStream:didReceiveText:)]) {
NSString *text = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
if (text) {
if (self.textBlock) {
self.textBlock(text);
}
if ([self.delegate respondsToSelector:@selector(networkStream:didReceiveText:)]) {
[self.delegate networkStream:self didReceiveText:text];
}
}
}
}
- (void)notifyCompletionWithError:(NSError * _Nullable)error {
if (self.completionBlock) {
self.completionBlock(error);
}
if ([self.delegate respondsToSelector:@selector(networkStream:didCompleteWithError:)]) {
[self.delegate networkStream:self didCompleteWithError:error];
}
}
#pragma mark - NSURLSessionDataDelegate
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler {
self.response = response;
self.expectedContentLength = response.expectedContentLength;
_totalBytesReceived = 0;
[self.receivedData setLength:0];
[self updateState:NetworkStreamStateReceiving];
// CORS
if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
NSLog(@"Response headers: %@", httpResponse.allHeaderFields);
// CORS
NSString *allowOrigin = httpResponse.allHeaderFields[@"Access-Control-Allow-Origin"];
if (allowOrigin) {
NSLog(@"CORS Allow Origin: %@", allowOrigin);
}
}
completionHandler(NSURLSessionResponseAllow);
}
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didReceiveData:(NSData *)data {
_totalBytesReceived += data.length;
[self.receivedData appendData:data];
//
[self notifyReceivedData:data];
//
if (self.expectedContentLength != NSURLResponseUnknownLength) {
float progress = (float)self.totalBytesReceived / (float)self.expectedContentLength;
[self notifyProgress:progress];
} else {
// chunked
[self notifyProgress:-1]; // 使 -1
}
}
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didCompleteWithError:(NSError *)error {
if (error) {
[self updateState:NetworkStreamStateError];
NSLog(@"Request failed with error: %@", error);
} else {
[self updateState:NetworkStreamStateCompleted];
NSLog(@"Request completed successfully. Total bytes: %lld", self.totalBytesReceived);
}
[self notifyCompletionWithError:error];
//
[self.session finishTasksAndInvalidate];
self.dataTask = nil;
}
#pragma mark - URL Session Delegate ( SSL/)
- (void)URLSession:(NSURLSession *)session
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential * _Nullable))completionHandler {
// SSL
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
} else {
completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
}
}
@end

View File

@@ -0,0 +1,70 @@
//
// WJXEventSource.h
// WJXEventSource
//
// Created by JiuxingWang on 2025/2/9.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
#ifdef __cplusplus
#define WJX_EXTERN extern "C" __attribute__((visibility ("default")))
#else
#define WJX_EXTERN extern __attribute__((visibility ("default")))
#endif
/// 消息事件
typedef NSString *WJXEventName NS_TYPED_EXTENSIBLE_ENUM;
/// 消息事件
WJX_EXTERN WJXEventName const WJXEventNameMessage;
/// readyState 变化事件
WJX_EXTERN WJXEventName const WJXEventNameReadyState;
/// open 事件
WJX_EXTERN WJXEventName const WJXEventNameOpen;
/// error 事件
WJX_EXTERN WJXEventName const WJXEventNameError;
typedef NS_ENUM(NSUInteger, WJXEventState) {
WJXEventStateConnecting = 0,
WJXEventStateOpen,
WJXEventStateClosed,
};
@interface WJXEvent : NSObject
@property (nonatomic, strong, nullable) id eventId;
@property (nonatomic, copy, nullable) NSString *event;
@property (nonatomic, copy, nullable) NSString *data;
@property (nonatomic, assign) WJXEventState readyState;
@property (nonatomic, strong, nullable) NSError *error;
- (instancetype)initWithReadyState:(WJXEventState)readyState;
@end
typedef void(^WJXEventSourceEventHandler)(WJXEvent *event);
@interface WJXEventSource : NSObject
@property (nonatomic, assign) BOOL ignoreRetryAction;
- (instancetype)initWithRquest:(NSURLRequest *)request;
- (void)addListener:(WJXEventSourceEventHandler)listener
forEvent:(WJXEventName)eventName
queue:(nullable NSOperationQueue *)queue;
- (void)open;
- (void)close;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,309 @@
//
// WJXEventSource.m
// WJXEventSource
//
// Created by JiuxingWang on 2025/2/9.
//
#import "WJXEventSource.h"
///
WJXEventName const WJXEventNameMessage = @"message";
/// readyState
WJXEventName const WJXEventNameReadyState = @"readyState";
/// open
WJXEventName const WJXEventNameOpen = @"open";
/// error
WJXEventName const WJXEventNameError = @"error";
#pragma mark -
#pragma mark WJXEvent
@implementation WJXEvent
- (instancetype)initWithReadyState:(WJXEventState)readyState;
{
if (self = [super init]) {
self.readyState = readyState;
}
return self;
}
- (NSString *)description
{
NSString *state = nil;
switch (_readyState) {
case WJXEventStateConnecting: {
state = @"CONNECTING";
} break;
case WJXEventStateOpen: {
state = @"OPEN";
} break;
case WJXEventStateClosed: {
state = @"CLOSED";
} break;
}
return [NSString stringWithFormat:@"<%@: readyState: %@, id: %@; event: %@; data: %@>", [self class], state, _eventId, _event, _data];
}
@end
#pragma mark -
#pragma mark WJXEventHandler
@interface WJXEventHandler : NSObject
@property (nonatomic, copy, nonnull) WJXEventSourceEventHandler handler;
@property (nonatomic, strong, nullable) NSOperationQueue *queue;
@end
@implementation WJXEventHandler
- (instancetype)initWithHandler:(WJXEventSourceEventHandler)handler queue:(NSOperationQueue *)queue
{
if (self = [super init]) {
self.handler = handler;
self.queue = queue;
}
return self;
}
@end
#pragma mark -
#pragma mark WJXEventSource
@interface WJXEventSource () <NSURLSessionDataDelegate>
@property (nonatomic, strong) NSMutableURLRequest *request;
@property (nonatomic, strong) NSMutableDictionary<WJXEventName, NSMutableArray<WJXEventHandler *> *> *listeners;
@property (nonatomic, strong) NSURLSession *session;
@property (nonatomic, strong) NSURLSessionDataTask *dataTask;
@property (nonatomic, copy) NSString *lastEventId;
@property (nonatomic, assign) NSTimeInterval retryInterval;
@property (nonatomic, assign) BOOL closedByUser;
@property (nonatomic, strong) NSMutableData *buffer;
@end
@implementation WJXEventSource
- (instancetype)initWithRquest:(NSURLRequest *)request;
{
if (self = [super init]) {
self.request = [request mutableCopy];
self.listeners = [NSMutableDictionary dictionary];
self.session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration ephemeralSessionConfiguration] delegate:self delegateQueue:NSOperationQueue.mainQueue];
self.buffer = [NSMutableData data];
}
return self;
}
- (void)dealloc
{
[_session finishTasksAndInvalidate];
}
- (void)addListener:(WJXEventSourceEventHandler)listener
forEvent:(WJXEventName)eventName
queue:(nullable NSOperationQueue *)queue;
{
if (nil == listener) {
return;
}
NSMutableArray *listeners = self.listeners[eventName];
if (nil == listeners) {
self.listeners[eventName] = listeners = [NSMutableArray array];
}
[listeners addObject:[[WJXEventHandler alloc] initWithHandler:listener queue:queue]];
}
- (void)open;
{
if (_lastEventId.length) {
[_request setValue:_lastEventId forHTTPHeaderField:@"Last-Event-ID"];
}
self.dataTask = [_session dataTaskWithRequest:_request];
[_dataTask resume];
WJXEvent *event = [[WJXEvent alloc] initWithReadyState:WJXEventStateConnecting];
[self _dispatchEvent:event forName:WJXEventNameReadyState];
}
- (void)close;
{
self.closedByUser = YES;
[_dataTask cancel];
[_session finishTasksAndInvalidate];
_buffer = [NSMutableData data];
}
#pragma mark -
#pragma mark NSURLSessionDataDelegate
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler;
{
NSHTTPURLResponse *HTTPResponse = (NSHTTPURLResponse *)response;
if (200 == HTTPResponse.statusCode) {
WJXEvent *event = [[WJXEvent alloc] initWithReadyState:WJXEventStateOpen];
[self _dispatchEvent:event forName:WJXEventNameReadyState];
[self _dispatchEvent:event forName:WJXEventNameOpen];
}
if (nil != completionHandler) {
completionHandler(NSURLSessionResponseAllow);
}
}
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didReceiveData:(NSData *)data;
{
[_buffer appendData:data];
[self _processBuffer];
}
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didCompleteWithError:(nullable NSError *)error;
{
if (_closedByUser) {
_buffer = [NSMutableData data];
return;
}
[self _dispatchPlainBufferIfNeeded];
WJXEvent *event = [[WJXEvent alloc] initWithReadyState:WJXEventStateClosed];
if (nil == (event.error = error)) {
event.error = [NSError errorWithDomain:@"WJXEventSource" code:event.readyState userInfo:@{
NSLocalizedDescriptionKey: @"Connection with the event source was closed without error",
}];
}
[self _dispatchEvent:event forName:WJXEventNameReadyState];
if (nil != error) {
[self _dispatchEvent:event forName:WJXEventNameError];
if (!_ignoreRetryAction) {
[self performSelector:@selector(open) withObject:nil afterDelay:_retryInterval];
}
}
}
#pragma mark -
#pragma mark Private
- (void)_processBuffer
{
NSData *separatorLFLFData = [NSData dataWithBytes:"\n\n" length:2];
NSRange range = [_buffer rangeOfData:separatorLFLFData options:kNilOptions range:(NSRange) {
.length = _buffer.length
}];
while (NSNotFound != range.location) {
// Extract event data
NSData *eventData = [_buffer subdataWithRange:(NSRange) {
.length = range.location
}];
[_buffer replaceBytesInRange:(NSRange) {
.length = range.location + 2
} withBytes:NULL length:0];
[self _parseEventData:eventData];
// Look for next event
range = [_buffer rangeOfData:separatorLFLFData options:kNilOptions range:(NSRange) {
.length = _buffer.length
}];
}
}
- (void)_parseEventData:(NSData *)data
{
WJXEvent *event = [[WJXEvent alloc] initWithReadyState:WJXEventStateOpen];
NSString *eventString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
if (eventString.length == 0) { return; }
NSArray *lines = [eventString componentsSeparatedByCharactersInSet:NSCharacterSet.newlineCharacterSet];
BOOL hasDataLine = NO;
for (NSString *line in lines) {
if ([line hasPrefix:@"id:"]) {
event.eventId = [[line substringFromIndex:3] stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceCharacterSet];
} else if ([line hasPrefix:@"event:"]) {
event.event = [[line substringFromIndex:6] stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceCharacterSet];
} else if ([line hasPrefix:@"data:"]) {
hasDataLine = YES;
NSString *data = [[line substringFromIndex:5] stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceCharacterSet];
event.data = event.data ? [event.data stringByAppendingFormat:@"\n%@", data] : data;
} else if ([line hasPrefix:@"retry:"]) {
NSString *retryString = [[line substringFromIndex:6] stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceCharacterSet];
self.retryInterval = [retryString doubleValue] / 1000;
}
}
if (!hasDataLine) {
NSString *trimmed = [eventString stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
if (trimmed.length > 0) {
event.data = trimmed;
}
}
if (event.eventId) {
self.lastEventId = event.eventId;
}
[self _dispatchEvent:event forName:WJXEventNameMessage];
}
- (void)_dispatchEvent:(WJXEvent *)event forName:(WJXEventName)name
{
NSMutableArray<WJXEventHandler *> *listeners = self.listeners[name];
[listeners enumerateObjectsUsingBlock:^(WJXEventHandler * _Nonnull handler, NSUInteger idx, BOOL * _Nonnull stop) {
NSOperationQueue *queue = handler.queue ?: NSOperationQueue.mainQueue;
[queue addOperationWithBlock:^{
handler.handler(event);
}];
}];
}
- (void)_dispatchPlainBufferIfNeeded
{
if (_buffer.length == 0) { return; }
NSData *data = [_buffer copy];
[_buffer setLength:0];
if (data.length == 0) { return; }
[self _parseEventData:data];
}
#pragma mark -
#pragma mark Setters
- (void)setDataTask:(NSURLSessionDataTask *)dataTask
{
self.closedByUser = YES; {
[_dataTask cancel];
_dataTask = dataTask;
} self.closedByUser = NO;
}
@end

View File

@@ -12,10 +12,30 @@
// You will also need to set the Prefix Header build setting of one or more of your targets to reference this file. // You will also need to set the Prefix Header build setting of one or more of your targets to reference this file.
#define SCREEN_WIDTH [UIScreen mainScreen].bounds.size.width #define SCREEN_WIDTH [UIScreen mainScreen].bounds.size.width
#define SCREEN_HEIGHT [UIScreen mainScreen].bounds.size.width #define SCREEN_HEIGHT [UIScreen mainScreen].bounds.size.height
#define imageNamed(s) [UIImage imageNamed:s] #define imageNamed(s) [UIImage imageNamed:s]
// 公共配置
#import "KBConfig.h"
#import "KBAPI.h" // 接口路径宏(统一管理)
#import "Masonry.h" #import "Masonry.h"
#import "KBHUD.h" // 复用 App 内的 HUD 封装
#import "KBLocalizationManager.h" // 复用多语言封装(可在扩展内使用)
#import "KBMaiPointReporter.h"
//#import "KBLog.h"
// 通用链接Universal Links统一配置
// 配置好 AASA 与 Associated Domains 后,只需修改这里即可切换域名/path。
#define KB_UL_BASE @"https://app.tknb.net/ul" // 与 Associated Domains 一致
#define KB_UL_LOGIN KB_UL_BASE @"/login"
#define KB_UL_SETTINGS KB_UL_BASE @"/settings"
// 在扩展内,启用 URL Bridge仅在显式的用户点击动作中使用
// 这样即便宿主 App如备忘录拒绝 extensionContext 的 openURL仍可通过响应链兜底拉起容器 App。
#ifndef KB_URL_BRIDGE_ENABLE
#define KB_URL_BRIDGE_ENABLE 1
#endif
#endif /* PrefixHeader_pch */ #endif /* PrefixHeader_pch */

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,249 @@
/* 字母 q小写 */
"letter_q_lower" = "key_q";
/* 字母 Q大写 */
"letter_q_upper" = "key_q_up";
/* 字母 w小写 */
"letter_w_lower" = "key_w";
/* 字母 W大写 */
"letter_w_upper" = "key_w_up";
/* 字母 e小写 */
"letter_e_lower" = "key_e";
/* 字母 E大写 */
"letter_e_upper" = "key_e_up";
/* 字母 r小写 */
"letter_r_lower" = "key_r";
/* 字母 R大写 */
"letter_r_upper" = "key_r_up";
/* 字母 t小写 */
"letter_t_lower" = "key_t";
/* 字母 T大写 */
"letter_t_upper" = "key_t_up";
/* 字母 y小写 */
"letter_y_lower" = "key_y";
/* 字母 Y大写 */
"letter_y_upper" = "key_y_up";
/* 字母 u小写 */
"letter_u_lower" = "key_u";
/* 字母 U大写 */
"letter_u_upper" = "key_u_up";
/* 字母 i小写 */
"letter_i_lower" = "key_i";
/* 字母 I大写 */
"letter_i_upper" = "key_i_up";
/* 字母 o小写 */
"letter_o_lower" = "key_o";
/* 字母 O大写 */
"letter_o_upper" = "key_o_up";
/* 字母 p小写 */
"letter_p_lower" = "key_p";
/* 字母 P大写 */
"letter_p_upper" = "key_p_up";
/* 字母 a小写 */
"letter_a_lower" = "key_a";
/* 字母 A大写 */
"letter_a_upper" = "key_a_up";
/* 字母 s小写 */
"letter_s_lower" = "key_s";
/* 字母 S大写 */
"letter_s_upper" = "key_s_up";
/* 字母 d小写 */
"letter_d_lower" = "key_d";
/* 字母 D大写 */
"letter_d_upper" = "key_d_up";
/* 字母 f小写 */
"letter_f_lower" = "key_f";
/* 字母 F大写 */
"letter_f_upper" = "key_f_up";
/* 字母 g小写 */
"letter_g_lower" = "key_g";
/* 字母 G大写 */
"letter_g_upper" = "key_f_up";
/* 字母 h小写 */
"letter_h_lower" = "key_h";
/* 字母 H大写 */
"letter_h_upper" = "key_h_up";
/* 字母 j小写 */
"letter_j_lower" = "key_j";
/* 字母 J大写 */
"letter_j_upper" = "key_j_up";
/* 字母 k小写 */
"letter_k_lower" = "key_k";
/* 字母 K大写 */
"letter_k_upper" = "key_k_up";
/* 字母 l小写 */
"letter_l_lower" = "key_l";
/* 字母 L大写 */
"letter_l_upper" = "key_l_up";
/* 字母 z小写 */
"letter_z_lower" = "key_z";
/* 字母 Z大写 */
"letter_z_upper" = "key_z_up";
/* 字母 x小写 */
"letter_x_lower" = "key_x";
/* 字母 X大写 */
"letter_x_upper" = "key_x_up";
/* 字母 c小写 */
"letter_c_lower" = "key_c";
/* 字母 C大写 */
"letter_c_upper" = "key_c_up";
/* 字母 v小写 */
"letter_v_lower" = "key_v";
/* 字母 V大写 */
"letter_v_upper" = "key_v_up";
/* 字母 b小写 */
"letter_b_lower" = "key_b";
/* 字母 B大写 */
"letter_b_upper" = "key_b_up";
/* 字母 n小写 */
"letter_n_lower" = "key_n";
/* 字母 N大写 */
"letter_n_upper" = "key_n_up";
/* 字母 m小写 */
"letter_m_lower" = "key_m";
/* 字母 M大写 */
"letter_m_upper" = "key_m_up";
/* 数字 1 */
"digit_1" = "key_1";
/* 数字 2 */
"digit_2" = "key_2";
/* 数字 3 */
"digit_3" = "key_3";
/* 数字 4 */
"digit_4" = "key_4";
/* 数字 5 */
"digit_5" = "key_5";
/* 数字 6 */
"digit_6" = "key_6";
/* 数字 7 */
"digit_7" = "key_7";
/* 数字 8 */
"digit_8" = "key_8";
/* 数字 9 */
"digit_9" = "key_9";
/* 数字 0 */
"digit_0" = "key_0";
/* '-' */
"sym_minus" = "key_minus";
/* '/' */
"sym_slash" = "key_slash";
/* ':' */
"sym_colon" = "key_colon";
/* ';' */
"sym_semicolon" = "key_semicolon";
/* '(' */
"sym_paren_l" = "key_paren_l";
/* ')' */
"sym_paren_r" = "key_paren_r";
/* '$' */
"sym_dollar" = "key_dollar";
/* '&' */
"sym_amp" = "key_amp";
/* '@' */
"sym_at" = "key_at";
/* 双引号 " */
"sym_quote_double" = "key_quote_d";
/* ',' */
"sym_comma" = "key_comma";
/* '.' */
"sym_dot" = "key_dot";
/* '?' */
"sym_question" = "key_question";
/* '!' */
"sym_exclam" = "key_exclam";
/* 单引号 ' */
"sym_quote_single" = "key_quote";
/* '[' */
"sym_bracket_l" = "key_bracket_l";
/* ']' */
"sym_bracket_r" = "key_bracket_r";
/* '{' */
"sym_brace_l" = "key_brace_l";
/* '}' */
"sym_brace_r" = "key_brace_r";
/* '#' */
"sym_hash" = "key_hash";
/* '%' */
"sym_percent" = "key_percent";
/* '^' */
"sym_caret" = "key_caret";
/* '*' */
"sym_asterisk" = "key_asterisk";
/* '+' */
"sym_plus" = "key_plus";
/* '=' */
"sym_equal" = "key_equal";
/* '_' */
"sym_underscore" = "key_underscore";
/* '\' */
"sym_backslash" = "key_backslash";
/* '|' */
"sym_pipe" = "key_pipe";
/* '~' */
"sym_tilde" = "key_tilde";
/* '<' */
"sym_lt" = "key_lt";
/* '>' */
"sym_gt" = "key_gt";
/* '¥' */
"sym_money" = "key_money";
/* '€' */
"sym_euro" = "key_euro";
/* '£' */
"sym_pound" = "key_pound";
/* '•' */
"sym_bullet" = "key_bullet";
/* 空格键 */
"space" = "key_space";
/* 删除键(⌫) */
"backspace" = "key_del";
/* Shift */
"shift" = "key_up";
/* Shift大写 */
"shift_upper" = "key_up_upper";
/* 字母面板左下角 "123" */
"mode_123" = "key_123";
/* 数字面板左下角 "abc" */
"mode_abc" = "key_abc";
/* 数字面板内 "123 -> #+=" */
"symbols_toggle_more" = "key_symbols_more";
/* 数字面板内 "#+= -> 123" */
"symbols_toggle_123" = "key_symbols_123";
/* 自定义 AI 功能键 */
"ai" = "key_ai";
/* Emoji功能键 */
//"emoji" = "key_emoji";
"emoji_panel" = "key_emoji";
/* 发送/换行键 */
"return" = "key_send";

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,22 @@
//
// KBBackspaceLongPressHandler.h
// CustomKeyboard
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface KBBackspaceLongPressHandler : NSObject
- (instancetype)initWithContainerView:(UIView *)containerView;
/// 配置删除按钮(包含长按删除;可选是否显示“上滑清空”提示)
- (void)bindDeleteButton:(nullable UIView *)button showClearLabel:(BOOL)showClearLabel;
/// 触发“上滑清空”逻辑(可用于功能面板的清空按钮)
- (void)performClearAction;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,662 @@
//
// KBBackspaceLongPressHandler.m
// CustomKeyboard
//
#import "KBBackspaceLongPressHandler.h"
#import "KBResponderUtils.h"
#import "KBSkinManager.h"
#import "KBBackspaceUndoManager.h"
#import "KBInputBufferManager.h"
static const NSTimeInterval kKBBackspaceLongPressMinDuration = 0.35;
static const NSTimeInterval kKBBackspaceRepeatInterval = 0.06;
static const NSTimeInterval kKBBackspaceChunkStartDelay = 0.6;
static const NSTimeInterval kKBBackspaceChunkRepeatInterval = 0.1;
static const NSTimeInterval kKBBackspaceChunkFastDelay = 1.2;
static const NSInteger kKBBackspaceChunkSize = 8;
static const NSInteger kKBBackspaceChunkSizeFast = 16;
static const CGFloat kKBBackspaceClearLabelCornerRadius = 8.0;
static const CGFloat kKBBackspaceClearLabelHeight = 34;
static const CGFloat kKBBackspaceClearLabelPaddingX = 10.0;
static const CGFloat kKBBackspaceClearLabelTopGap = 6.0;
static const CGFloat kKBBackspaceClearLabelHorizontalInset = 6.0;
static const NSTimeInterval kKBBackspaceClearBatchInterval = 0.02;
static const NSInteger kKBBackspaceClearMaxDeletes = 10000;
static const NSInteger kKBBackspaceClearEmptyContextMaxRounds = 40;
static const NSInteger kKBBackspaceClearMaxStep = 80;
static const NSInteger kKBBackspaceClearDeletesPerTick = 10;
typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
KBBackspaceChunkClassUnknown = 0,
KBBackspaceChunkClassWhitespace,
KBBackspaceChunkClassASCIIWord,
KBBackspaceChunkClassPunctuation,
KBBackspaceChunkClassOther
};
typedef NS_ENUM(NSInteger, KBClearPhase) {
KBClearPhaseSkipWhitespace = 0,
KBClearPhaseSkipTrailingBoundary,
KBClearPhaseDeleteUntilBoundary
};
@interface KBBackspaceLongPressHandler ()
@property (nonatomic, weak) UIView *containerView;
@property (nonatomic, weak) UIView *backspaceButton;
@property (nonatomic, strong) UILongPressGestureRecognizer *longPress;
@property (nonatomic, assign) BOOL showClearLabelEnabled;
@property (nonatomic, assign) BOOL backspaceHoldActive;
@property (nonatomic, assign) NSTimeInterval backspaceHoldStartTime;
@property (nonatomic, assign) BOOL backspaceChunkModeActive;
@property (nonatomic, assign) BOOL backspaceClearHighlighted;
@property (nonatomic, assign) NSUInteger backspaceHoldToken;
@property (nonatomic, assign) BOOL backspaceHasLastTouchPoint;
@property (nonatomic, assign) CGPoint backspaceLastTouchPointInSelf;
@property (nonatomic, assign) NSUInteger backspaceClearToken;
@property (nonatomic, strong) UILabel *backspaceClearLabel;
@property (nonatomic, copy) NSString *pendingClearBefore;
@property (nonatomic, copy) NSString *pendingClearAfter;
@property (nonatomic, assign) KBClearPhase backspaceClearPhase;
@end
@implementation KBBackspaceLongPressHandler
- (instancetype)initWithContainerView:(UIView *)containerView {
if (self = [super init]) {
_containerView = containerView;
_backspaceClearPhase = KBClearPhaseSkipWhitespace;
}
return self;
}
- (void)bindDeleteButton:(UIView *)button showClearLabel:(BOOL)showClearLabel {
if (self.backspaceButton == button) { return; }
if (self.longPress && self.backspaceButton) {
[self.backspaceButton removeGestureRecognizer:self.longPress];
}
self.backspaceButton = button;
self.showClearLabelEnabled = showClearLabel;
self.backspaceHoldActive = NO;
self.backspaceChunkModeActive = NO;
self.backspaceClearHighlighted = NO;
self.backspaceHasLastTouchPoint = NO;
self.backspaceHoldToken += 1;
[self kb_hideBackspaceClearLabel];
self.pendingClearBefore = nil;
self.pendingClearAfter = nil;
if (!button) { return; }
self.longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self
action:@selector(onBackspaceLongPress:)];
self.longPress.minimumPressDuration = kKBBackspaceLongPressMinDuration;
self.longPress.allowableMovement = CGFLOAT_MAX;
self.longPress.cancelsTouchesInView = YES;
[button addGestureRecognizer:self.longPress];
}
- (void)performClearAction {
[self kb_clearAllInput];
}
#pragma mark - Long Press
- (void)onBackspaceLongPress:(UILongPressGestureRecognizer *)gr {
UIView *hostView = [self kb_hostView];
if (!hostView) { return; }
if (gr) {
self.backspaceLastTouchPointInSelf = [gr locationInView:hostView];
self.backspaceHasLastTouchPoint = YES;
}
switch (gr.state) {
case UIGestureRecognizerStateBegan: {
UIResponder *start = (UIResponder *)([self kb_hostView] ?: self.backspaceButton);
UIInputViewController *ivc = KBFindInputViewController(start);
if (ivc) {
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
[[KBInputBufferManager shared] refreshFromProxyIfPossible:proxy];
[[KBInputBufferManager shared] prepareSnapshotForDeleteWithContextBefore:proxy.documentContextBeforeInput
after:proxy.documentContextAfterInput];
}
if (self.showClearLabelEnabled) {
[self kb_capturePendingClearSnapshotIfNeeded];
[[KBInputBufferManager shared] beginPendingClearSnapshot];
}
self.backspaceHoldToken += 1;
NSUInteger token = self.backspaceHoldToken;
self.backspaceHoldActive = YES;
self.backspaceHoldStartTime = [NSDate date].timeIntervalSinceReferenceDate;
self.backspaceChunkModeActive = NO;
[self kb_setBackspaceClearHighlighted:NO];
[self kb_hideBackspaceClearLabel];
if (self.showClearLabelEnabled) {
[self kb_showBackspaceClearLabelIfNeeded];
}
[self kb_backspaceStepForToken:token];
} break;
case UIGestureRecognizerStateChanged: {
[self kb_handleBackspaceLongPressChanged:gr];
} break;
case UIGestureRecognizerStateEnded:
case UIGestureRecognizerStateCancelled:
case UIGestureRecognizerStateFailed: {
[self kb_handleBackspaceLongPressEnded:gr];
} break;
default: break;
}
}
#pragma mark - Delete Steps
- (void)kb_backspaceStepForToken:(NSUInteger)token {
if (!self.backspaceHoldActive) { return; }
if (token != self.backspaceHoldToken) { return; }
UIResponder *start = (UIResponder *)([self kb_hostView] ?: self.backspaceButton);
UIInputViewController *ivc = KBFindInputViewController(start);
if (!ivc) { self.backspaceHoldActive = NO; return; }
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
NSString *before = proxy.documentContextBeforeInput ?: @"";
if (before.length == 0) { before = [KBInputBufferManager shared].liveText ?: @""; }
NSTimeInterval elapsed = [NSDate date].timeIntervalSinceReferenceDate - self.backspaceHoldStartTime;
NSInteger deleteCount = 1;
if (before.length > 0) {
deleteCount = [self kb_backspaceDeleteCountForContext:before elapsed:elapsed];
}
if (!self.backspaceChunkModeActive && elapsed >= kKBBackspaceChunkStartDelay) {
self.backspaceChunkModeActive = YES;
if (self.showClearLabelEnabled) {
[self kb_showBackspaceClearLabelIfNeeded];
}
}
[[KBBackspaceUndoManager shared] captureAndDeleteBackwardFromProxy:proxy count:(NSUInteger)deleteCount];
[[KBInputBufferManager shared] applyHoldDeleteCount:(NSUInteger)deleteCount];
NSTimeInterval interval = [self kb_backspaceRepeatIntervalForElapsed:elapsed];
__weak typeof(self) weakSelf = self;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
(int64_t)(interval * NSEC_PER_SEC)),
dispatch_get_main_queue(), ^{
__strong typeof(weakSelf) selfStrong = weakSelf;
[selfStrong kb_backspaceStepForToken:token];
});
}
- (NSTimeInterval)kb_backspaceRepeatIntervalForElapsed:(NSTimeInterval)elapsed {
if (elapsed >= kKBBackspaceChunkStartDelay) {
return kKBBackspaceChunkRepeatInterval;
}
return kKBBackspaceRepeatInterval;
}
- (NSInteger)kb_backspaceDeleteCountForContext:(NSString *)context elapsed:(NSTimeInterval)elapsed {
if (elapsed < kKBBackspaceChunkStartDelay) {
return 1;
}
NSInteger maxCount = (elapsed >= kKBBackspaceChunkFastDelay)
? kKBBackspaceChunkSizeFast : kKBBackspaceChunkSize;
return [self kb_backspaceChunkDeleteCountForContext:context maxCount:maxCount];
}
- (NSInteger)kb_backspaceChunkDeleteCountForContext:(NSString *)context maxCount:(NSInteger)maxCount {
if (context.length == 0) { return 1; }
static NSCharacterSet *whitespaceSet = nil;
static NSCharacterSet *asciiWordSet = nil;
static NSCharacterSet *punctuationSet = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
whitespaceSet = [NSCharacterSet whitespaceAndNewlineCharacterSet];
asciiWordSet = [NSCharacterSet characterSetWithCharactersInString:
@"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"];
NSMutableCharacterSet *punct = [[NSCharacterSet punctuationCharacterSet] mutableCopy];
// / chunk 1
[punct addCharactersInString:@",。!?;:、()【】《》“”‘’·…—"];
punctuationSet = [punct copy];
});
__block NSInteger deleteCount = 0;
typedef NS_ENUM(NSInteger, KBBackspaceChunkPhase) {
KBBackspaceChunkPhaseWhitespace = 0,
KBBackspaceChunkPhasePunctuation,
KBBackspaceChunkPhaseCore
};
__block KBBackspaceChunkPhase phase = KBBackspaceChunkPhaseWhitespace;
__block KBBackspaceChunkClass coreClass = KBBackspaceChunkClassUnknown;
[context enumerateSubstringsInRange:NSMakeRange(0, context.length)
options:NSStringEnumerationByComposedCharacterSequences | NSStringEnumerationReverse
usingBlock:^(NSString *substring, __unused NSRange substringRange, __unused NSRange enclosingRange, BOOL *stop) {
if (substring.length == 0) { return; }
if (deleteCount >= maxCount) {
*stop = YES;
return;
}
KBBackspaceChunkClass currentClass = KBBackspaceChunkClassOther;
if ([substring rangeOfCharacterFromSet:whitespaceSet].location != NSNotFound) {
currentClass = KBBackspaceChunkClassWhitespace;
} else if ([substring rangeOfCharacterFromSet:punctuationSet].location != NSNotFound) {
currentClass = KBBackspaceChunkClassPunctuation;
} else if ([substring rangeOfCharacterFromSet:asciiWordSet].location != NSNotFound) {
currentClass = KBBackspaceChunkClassASCIIWord;
}
BOOL consumed = NO;
while (!consumed) {
if (phase == KBBackspaceChunkPhaseWhitespace) {
if (currentClass == KBBackspaceChunkClassWhitespace) {
deleteCount += 1;
consumed = YES;
} else {
phase = KBBackspaceChunkPhasePunctuation;
}
continue;
}
if (phase == KBBackspaceChunkPhasePunctuation) {
if (currentClass == KBBackspaceChunkClassPunctuation) {
deleteCount += 1;
consumed = YES;
} else {
phase = KBBackspaceChunkPhaseCore;
}
continue;
}
// phase == CoreASCII /
if (coreClass == KBBackspaceChunkClassUnknown) {
coreClass = currentClass;
}
if (currentClass != coreClass) {
*stop = YES;
consumed = YES;
continue;
}
deleteCount += 1;
consumed = YES;
}
if (deleteCount >= maxCount) {
*stop = YES;
return;
}
}];
return MAX(deleteCount, 1);
}
- (NSInteger)kb_clearDeleteCountForContext:(NSString *)context
hitBoundary:(BOOL *)hitBoundary {
if (context.length == 0) {
if (hitBoundary) { *hitBoundary = NO; }
return 1;
}
static NSCharacterSet *sentenceBoundarySet = nil;
static NSCharacterSet *whitespaceSet = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sentenceBoundarySet = [NSCharacterSet characterSetWithCharactersInString:@".!?。!?"];
whitespaceSet = [NSCharacterSet whitespaceAndNewlineCharacterSet];
});
NSInteger length = context.length;
NSInteger end = length;
while (end > 0) {
unichar ch = [context characterAtIndex:end - 1];
if ([whitespaceSet characterIsMember:ch]) {
end -= 1;
} else {
break;
}
}
NSInteger searchEnd = end;
while (searchEnd > 0) {
unichar ch = [context characterAtIndex:searchEnd - 1];
if ([sentenceBoundarySet characterIsMember:ch]) {
searchEnd -= 1;
} else {
break;
}
}
NSInteger boundaryIndex = NSNotFound;
for (NSInteger i = searchEnd - 1; i >= 0; i--) {
unichar ch = [context characterAtIndex:i];
if ([sentenceBoundarySet characterIsMember:ch]) {
boundaryIndex = i;
break;
}
}
BOOL boundaryFound = (boundaryIndex != NSNotFound);
NSInteger deleteCount = length;
if (boundaryIndex != NSNotFound) {
deleteCount = length - (boundaryIndex + 1);
}
deleteCount = MAX(deleteCount, 1);
if (hitBoundary) {
*hitBoundary = boundaryFound;
}
return MIN(deleteCount, kKBBackspaceClearMaxStep);
}
#pragma mark - Long Press State
- (void)kb_handleBackspaceLongPressChanged:(UILongPressGestureRecognizer *)gr {
if (!self.backspaceHoldActive) { return; }
if (!self.showClearLabelEnabled) { return; }
[self kb_showBackspaceClearLabelIfNeeded];
UIView *hostView = [self kb_hostView];
if (!hostView) { return; }
CGPoint point = [gr locationInView:hostView];
self.backspaceLastTouchPointInSelf = point;
self.backspaceHasLastTouchPoint = YES;
BOOL inside = [self kb_isPointInsideBackspaceClearLabel:point];
[self kb_setBackspaceClearHighlighted:inside];
}
- (void)kb_handleBackspaceLongPressEnded:(UILongPressGestureRecognizer *)gr {
BOOL shouldClear = NO;
if (self.showClearLabelEnabled) {
shouldClear = self.backspaceClearHighlighted;
if (!shouldClear) {
UIView *hostView = [self kb_hostView];
CGPoint point = CGPointZero;
if (gr && hostView) {
point = [gr locationInView:hostView];
} else if (self.backspaceHasLastTouchPoint) {
point = self.backspaceLastTouchPointInSelf;
}
shouldClear = [self kb_isPointInsideBackspaceClearLabel:point];
}
}
#if DEBUG
NSLog(@"[kb_handleBackspaceLongPressEnded] shouldClear=%@ highlighted=%@ labelHidden=%@",
shouldClear ? @"YES" : @"NO",
self.backspaceClearHighlighted ? @"YES" : @"NO",
self.backspaceClearLabel.hidden ? @"YES" : @"NO");
#endif
self.backspaceHoldActive = NO;
self.backspaceChunkModeActive = NO;
self.backspaceHoldToken += 1;
self.backspaceHasLastTouchPoint = NO;
[self kb_hideBackspaceClearLabel];
if (shouldClear) {
[self kb_clearAllInput];
} else {
self.pendingClearBefore = nil;
self.pendingClearAfter = nil;
[[KBInputBufferManager shared] clearPendingClearSnapshot];
[[KBInputBufferManager shared] commitLiveToManual];
}
}
#pragma mark - Clear Label
- (void)kb_showBackspaceClearLabelIfNeeded {
UIView *hostView = [self kb_hostView];
if (!hostView || !self.backspaceButton) { return; }
UILabel *label = self.backspaceClearLabel;
[self kb_refreshBackspaceClearLabelColors];
if (!label.superview) {
[hostView addSubview:label];
}
[self kb_updateBackspaceClearLabelFrame];
[hostView bringSubviewToFront:label];
if (label.hidden) {
label.alpha = 0.0;
label.hidden = NO;
[self kb_playLightHaptic];
[UIView animateWithDuration:0.12 animations:^{
label.alpha = 1.0;
}];
}
}
- (void)kb_hideBackspaceClearLabel {
if (!_backspaceClearLabel || _backspaceClearLabel.hidden) { return; }
_backspaceClearLabel.hidden = YES;
_backspaceClearLabel.alpha = 1.0;
[self kb_setBackspaceClearHighlighted:NO];
}
- (void)kb_updateBackspaceClearLabelFrame {
UIView *hostView = [self kb_hostView];
if (!hostView || !self.backspaceButton || !self.backspaceClearLabel) { return; }
CGRect btnFrame = [self.backspaceButton convertRect:self.backspaceButton.bounds toView:hostView];
UILabel *label = self.backspaceClearLabel;
CGSize textSize = [label sizeThatFits:CGSizeMake(CGFLOAT_MAX, kKBBackspaceClearLabelHeight)];
CGFloat width = MAX(textSize.width + kKBBackspaceClearLabelPaddingX * 2.0, 60.0);
CGFloat height = kKBBackspaceClearLabelHeight;
CGFloat x = CGRectGetMidX(btnFrame) - width * 0.5;
CGFloat y = CGRectGetMinY(btnFrame) - height - kKBBackspaceClearLabelTopGap;
if (x < kKBBackspaceClearLabelHorizontalInset) { x = kKBBackspaceClearLabelHorizontalInset; }
CGFloat maxX = CGRectGetWidth(hostView.bounds) - kKBBackspaceClearLabelHorizontalInset - width;
if (x > maxX) { x = maxX; }
if (y < 0) { y = 0; }
label.frame = CGRectIntegral(CGRectMake(x, y, width, height));
}
- (BOOL)kb_isPointInsideBackspaceClearLabel:(CGPoint)point {
if (!self.backspaceClearLabel || self.backspaceClearLabel.hidden) { return NO; }
[self kb_updateBackspaceClearLabelFrame];
CGRect hitFrame = CGRectInset(self.backspaceClearLabel.frame, -12.0, -10.0);
return CGRectContainsPoint(hitFrame, point);
}
- (void)kb_setBackspaceClearHighlighted:(BOOL)highlighted {
if (self.backspaceClearHighlighted == highlighted) { return; }
self.backspaceClearHighlighted = highlighted;
[self kb_refreshBackspaceClearLabelColors];
}
- (void)kb_refreshBackspaceClearLabelColors {
UILabel *label = self.backspaceClearLabel;
label.textColor = [KBSkinManager shared].current.keyTextColor ?: UIColor.blackColor;
label.backgroundColor = self.backspaceClearHighlighted
? [self kb_backspaceClearLabelHighlightedColor]
: [self kb_backspaceClearLabelNormalColor];
}
- (UIColor *)kb_backspaceClearLabelNormalColor {
KBSkinTheme *t = [KBSkinManager shared].current;
return t.keyHighlightBackground ?: [UIColor colorWithWhite:0.9 alpha:1.0];
}
- (UIColor *)kb_backspaceClearLabelHighlightedColor {
KBSkinTheme *t = [KBSkinManager shared].current;
return t.accentColor ?: t.keyHighlightBackground ?: [UIColor colorWithWhite:0.8 alpha:1.0];
}
- (void)kb_playLightHaptic {
if (@available(iOS 10.0, *)) {
UIImpactFeedbackGenerator *gen = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleLight];
[gen prepare];
[gen impactOccurred];
}
}
- (UILabel *)backspaceClearLabel {
if (!_backspaceClearLabel) {
UILabel *label = [[UILabel alloc] initWithFrame:CGRectZero];
label.text = KBLocalized(@"Clear");
label.textAlignment = NSTextAlignmentCenter;
label.font = [UIFont systemFontOfSize:16 weight:UIFontWeightSemibold];
label.textColor = [KBSkinManager shared].current.keyTextColor ?: UIColor.blackColor;
label.backgroundColor = [self kb_backspaceClearLabelNormalColor];
label.layer.cornerRadius = kKBBackspaceClearLabelCornerRadius;
label.layer.masksToBounds = YES;
label.hidden = YES;
label.userInteractionEnabled = NO;
_backspaceClearLabel = label;
}
return _backspaceClearLabel;
}
#pragma mark - Clear
- (void)kb_clearAllInput {
UIResponder *start = (UIResponder *)([self kb_hostView] ?: self.backspaceButton);
UIInputViewController *ivc = KBFindInputViewController(start);
if (ivc) {
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
[[KBInputBufferManager shared] refreshFromProxyIfPossible:proxy];
}
self.pendingClearBefore = nil;
self.pendingClearAfter = nil;
[[KBInputBufferManager shared] clearPendingClearSnapshot];
self.backspaceClearToken += 1;
self.backspaceClearPhase = KBClearPhaseSkipWhitespace;
NSUInteger token = self.backspaceClearToken;
[self kb_clearAllInputStepForToken:token guard:0 emptyRounds:0];
}
- (void)kb_clearAllInputStepForToken:(NSUInteger)token
guard:(NSInteger)guard
emptyRounds:(NSInteger)emptyRounds {
if (token != self.backspaceClearToken) { return; }
UIResponder *start = (UIResponder *)([self kb_hostView] ?: self.backspaceButton);
UIInputViewController *ivc = KBFindInputViewController(start);
if (!ivc) { return; }
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
NSInteger nextEmptyRounds = emptyRounds;
static NSCharacterSet *stopBoundarySet = nil;
static NSCharacterSet *trailingBoundarySet = nil;
static NSCharacterSet *trailingWhitespaceSet = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// stopBoundary:
// - . ! ?
// - /
// - \n \r
stopBoundarySet = [NSCharacterSet characterSetWithCharactersInString:@".!?。!?…\n\r\u2028\u2029"];
// trailingBoundary:
// //
trailingBoundarySet = [NSCharacterSet characterSetWithCharactersInString:@".!?。!?"];
// trailingWhitespace: /Tab stopBoundarySet
trailingWhitespaceSet = [NSCharacterSet whitespaceCharacterSet];
});
KBClearPhase phase = self.backspaceClearPhase;
NSInteger deletedThisTick = 0;
BOOL shouldStop = NO;
NSString *lastBefore = nil;
for (NSInteger i = 0; i < kKBBackspaceClearDeletesPerTick; i++) {
NSString *before = proxy.documentContextBeforeInput ?: @"";
if (before.length == 0) {
nextEmptyRounds += 1;
// 宿/QQ context使
// before
shouldStop = YES;
break;
}
nextEmptyRounds = 0;
if (lastBefore && [before isEqualToString:lastBefore] && deletedThisTick > 0) {
// 宿 context tick /
break;
}
lastBefore = before;
//
__block NSString *lastChar = @"";
[before enumerateSubstringsInRange:NSMakeRange(0, before.length)
options:NSStringEnumerationByComposedCharacterSequences | NSStringEnumerationReverse
usingBlock:^(NSString *substring, __unused NSRange substringRange, __unused NSRange enclosingRange, BOOL *stop) {
lastChar = substring ?: @"";
*stop = YES;
}];
if (lastChar.length == 0) { break; }
BOOL isWhitespace = ([lastChar rangeOfCharacterFromSet:trailingWhitespaceSet].location != NSNotFound);
BOOL isStopBoundary = ([lastChar rangeOfCharacterFromSet:stopBoundarySet].location != NSNotFound);
BOOL isTrailingBoundary = ([lastChar rangeOfCharacterFromSet:trailingBoundarySet].location != NSNotFound);
if (phase == KBClearPhaseSkipWhitespace) {
if (isWhitespace) {
[[KBBackspaceUndoManager shared] captureAndDeleteBackwardFromProxy:proxy count:1];
[[KBInputBufferManager shared] applyClearDeleteCount:1];
deletedThisTick += 1;
continue;
}
phase = KBClearPhaseSkipTrailingBoundary;
}
if (phase == KBClearPhaseSkipTrailingBoundary) {
if (isTrailingBoundary) {
[[KBBackspaceUndoManager shared] captureAndDeleteBackwardFromProxy:proxy count:1];
[[KBInputBufferManager shared] applyClearDeleteCount:1];
deletedThisTick += 1;
continue;
}
phase = KBClearPhaseDeleteUntilBoundary;
}
// phase == DeleteUntilBoundary
if (isStopBoundary) {
shouldStop = YES; //
break;
}
[[KBBackspaceUndoManager shared] captureAndDeleteBackwardFromProxy:proxy count:1];
[[KBInputBufferManager shared] applyClearDeleteCount:1];
deletedThisTick += 1;
if (guard + deletedThisTick >= kKBBackspaceClearMaxDeletes) { break; }
if (deletedThisTick >= kKBBackspaceClearMaxStep) { break; }
}
self.backspaceClearPhase = phase;
NSInteger nextGuard = guard + deletedThisTick;
if (nextGuard >= kKBBackspaceClearMaxDeletes ||
nextEmptyRounds > kKBBackspaceClearEmptyContextMaxRounds ||
shouldStop) {
return;
}
__weak typeof(self) weakSelf = self;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
(int64_t)(kKBBackspaceClearBatchInterval * NSEC_PER_SEC)),
dispatch_get_main_queue(), ^{
__strong typeof(weakSelf) selfStrong = weakSelf;
[selfStrong kb_clearAllInputStepForToken:token
guard:nextGuard
emptyRounds:nextEmptyRounds];
});
}
#pragma mark - Helpers
- (UIView *)kb_hostView {
if (self.containerView) { return self.containerView; }
return self.backspaceButton.superview;
}
- (void)kb_captureDeletionSnapshotIfNeeded {
if ([KBBackspaceUndoManager shared].hasUndo) { return; }
UIResponder *start = (UIResponder *)([self kb_hostView] ?: self.backspaceButton);
UIInputViewController *ivc = KBFindInputViewController(start);
if (!ivc) { return; }
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
[[KBBackspaceUndoManager shared] recordDeletionSnapshotBefore:proxy.documentContextBeforeInput
after:proxy.documentContextAfterInput];
}
- (void)kb_capturePendingClearSnapshotIfNeeded {
if (self.pendingClearBefore.length > 0 || self.pendingClearAfter.length > 0) { return; }
UIResponder *start = (UIResponder *)([self kb_hostView] ?: self.backspaceButton);
UIInputViewController *ivc = KBFindInputViewController(start);
if (!ivc) { return; }
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
self.pendingClearBefore = proxy.documentContextBeforeInput ?: @"";
self.pendingClearAfter = proxy.documentContextAfterInput ?: @"";
#if DEBUG
NSLog(@"[kb_capturePendingClearSnapshotIfNeeded/before] len=%lu text=%@", (unsigned long)self.pendingClearBefore.length, self.pendingClearBefore);
NSLog(@"[kb_capturePendingClearSnapshotIfNeeded/after] len=%lu text=%@", (unsigned long)self.pendingClearAfter.length, self.pendingClearAfter);
#endif
}
@end

View File

@@ -0,0 +1,35 @@
//
// KBBackspaceUndoManager.h
// CustomKeyboard
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
extern NSNotificationName const KBBackspaceUndoStateDidChangeNotification;
@interface KBBackspaceUndoManager : NSObject
@property (nonatomic, readonly) BOOL hasUndo;
+ (instancetype)shared;
/// 记录一次删除前的快照(不改变撤销按钮显示)。
- (void)recordDeletionSnapshotBefore:(NSString *)before after:(NSString *)after;
/// 记录一次“立刻清空”删除的内容(基于 documentContextBeforeInput/AfterInput
- (void)recordClearWithContextBefore:(NSString *)before after:(NSString *)after;
/// 记录本次将被 deleteBackward 的内容,并执行 deleteBackward支持多次累计撤销时一次性插回
- (void)captureAndDeleteBackwardFromProxy:(id<UITextDocumentProxy>)proxy count:(NSUInteger)count;
/// 在指定 responder 处执行撤销(向光标处插回删除的内容)
- (void)performUndoFromResponder:(UIResponder *)responder;
/// 非删除行为触发时,清理撤销状态
- (void)registerNonClearAction;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,304 @@
//
// KBBackspaceUndoManager.m
// CustomKeyboard
//
#import "KBBackspaceUndoManager.h"
#import "KBResponderUtils.h"
#import "KBInputBufferManager.h"
NSNotificationName const KBBackspaceUndoStateDidChangeNotification = @"KBBackspaceUndoStateDidChangeNotification";
#if DEBUG
static NSString *KBLogString(NSString *tag, NSString *text) {
NSString *safeTag = tag ?: @"";
NSString *safeText = text ?: @"";
if (safeText.length <= 2000) {
return [NSString stringWithFormat:@"[%@] len=%lu text=%@", safeTag, (unsigned long)safeText.length, safeText];
}
NSString *head = [safeText substringToIndex:800];
NSString *tail = [safeText substringFromIndex:safeText.length - 800];
return [NSString stringWithFormat:@"[%@] len=%lu head=%@ ... tail=%@", safeTag, (unsigned long)safeText.length, head, tail];
}
#define KB_UNDO_LOG(tag, text) NSLog(@"%@", KBLogString((tag), (text)))
#else
#define KB_UNDO_LOG(tag, text) do {} while(0)
#endif
typedef NS_ENUM(NSInteger, KBUndoSnapshotSource) {
KBUndoSnapshotSourceNone = 0,
KBUndoSnapshotSourceDeletionSnapshot,
KBUndoSnapshotSourceClear
};
@interface KBBackspaceUndoManager ()
@property (nonatomic, copy) NSString *undoText;
@property (nonatomic, assign) NSInteger undoAfterLength;
@property (nonatomic, assign) BOOL hasUndo;
@property (nonatomic, assign) KBUndoSnapshotSource snapshotSource;
@property (nonatomic, strong) NSMutableArray<NSString *> *undoDeletedPieces;
@end
@implementation KBBackspaceUndoManager
+ (instancetype)shared {
static KBBackspaceUndoManager *mgr = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
mgr = [[KBBackspaceUndoManager alloc] init];
});
return mgr;
}
- (instancetype)init {
if (self = [super init]) {
_undoText = @"";
_undoAfterLength = 0;
_snapshotSource = KBUndoSnapshotSourceNone;
_undoDeletedPieces = [NSMutableArray array];
}
return self;
}
- (void)captureAndDeleteBackwardFromProxy:(id<UITextDocumentProxy>)proxy count:(NSUInteger)count {
if (!proxy || count == 0) { return; }
NSString *selected = proxy.selectedText ?: @"";
NSString *ctxBefore = proxy.documentContextBeforeInput ?: @"";
NSString *ctxAfter = proxy.documentContextAfterInput ?: @"";
NSUInteger ctxLen = ctxBefore.length + ctxAfter.length;
BOOL isSelectAllLike = (selected.length > 0 &&
(ctxLen == 0 || selected.length >= MAX((NSUInteger)40, ctxLen * 2)));
if (isSelectAllLike) {
// /QQ/
if (self.hasUndo) {
[self registerNonClearAction];
}
#if DEBUG
KB_UNDO_LOG(@"captureAndDelete/selectAllDisableUndo", selected);
#endif
[proxy deleteBackward];
[[KBInputBufferManager shared] resetWithText:@""];
return;
}
if (!self.hasUndo) {
[self.undoDeletedPieces removeAllObjects];
self.undoText = @"";
self.undoAfterLength = 0;
self.snapshotSource = KBUndoSnapshotSourceDeletionSnapshot;
[self kb_updateHasUndo:YES];
}
BOOL didAppend = NO;
NSString *lastObservedBefore = nil;
for (NSUInteger i = 0; i < count; i++) {
NSString *before = proxy.documentContextBeforeInput ?: @"";
if (before.length > 0) {
// 宿 runloop context
if (lastObservedBefore && [before isEqualToString:lastObservedBefore]) {
// still delete, but don't record
} else {
NSString *piece = [self kb_lastComposedCharacterFromString:before];
if (piece.length > 0) {
[self.undoDeletedPieces addObject:piece];
didAppend = YES;
}
lastObservedBefore = before;
}
}
[proxy deleteBackward];
}
#if DEBUG
if (didAppend) {
NSUInteger piecesCount = self.undoDeletedPieces.count;
if (piecesCount <= 20) {
KB_UNDO_LOG(@"captureAndDelete/undoInsertTextNow", [self kb_buildUndoInsertTextFromPieces]);
} else if (piecesCount % 50 == 0) {
NSString *lastPiece = self.undoDeletedPieces.lastObject ?: @"";
NSLog(@"[captureAndDelete/undoPieces] pieces=%lu lastPiece=%@",
(unsigned long)piecesCount,
lastPiece);
}
}
#endif
}
- (void)recordDeletionSnapshotBefore:(NSString *)before after:(NSString *)after {
if (self.hasUndo) { return; }
NSString *pending = [KBInputBufferManager shared].pendingClearSnapshot;
NSString *manual = [KBInputBufferManager shared].manualSnapshot;
NSString *fallbackText = (pending.length > 0) ? pending : ((manual.length > 0) ? manual : [KBInputBufferManager shared].liveText);
if (fallbackText.length > 0) {
self.undoText = fallbackText;
self.undoAfterLength = 0;
self.snapshotSource = KBUndoSnapshotSourceDeletionSnapshot;
KB_UNDO_LOG(@"recordDeletionSnapshot/fallback", self.undoText);
[self kb_updateHasUndo:YES];
return;
}
NSString *safeBefore = before ?: @"";
NSString *safeAfter = after ?: @"";
NSString *full = [safeBefore stringByAppendingString:safeAfter];
if (full.length == 0) { return; }
self.undoText = full;
self.undoAfterLength = (NSInteger)safeAfter.length;
self.snapshotSource = KBUndoSnapshotSourceDeletionSnapshot;
KB_UNDO_LOG(@"recordDeletionSnapshot/context", self.undoText);
[self kb_updateHasUndo:YES];
}
- (void)recordClearWithContextBefore:(NSString *)before after:(NSString *)after {
NSString *pending = [KBInputBufferManager shared].pendingClearSnapshot;
NSString *manual = [KBInputBufferManager shared].manualSnapshot;
NSString *fallbackText = (pending.length > 0) ? pending : ((manual.length > 0) ? manual : [KBInputBufferManager shared].liveText);
NSString *safeBefore = before ?: @"";
NSString *safeAfter = after ?: @"";
NSString *contextText = [[safeBefore stringByAppendingString:safeAfter] copy];
NSString *candidate = (fallbackText.length > 0) ? fallbackText : contextText;
NSInteger candidateAfterLen = (fallbackText.length > 0) ? 0 : (NSInteger)safeAfter.length;
if (candidate.length == 0) { return; }
KB_UNDO_LOG(@"recordClear/candidate", candidate);
if (self.undoText.length > 0) {
if (self.snapshotSource == KBUndoSnapshotSourceClear) {
KB_UNDO_LOG(@"recordClear/ignored(alreadyClear)", self.undoText);
[self kb_updateHasUndo:YES];
return;
}
if (self.snapshotSource == KBUndoSnapshotSourceDeletionSnapshot) {
if (candidate.length > self.undoText.length) {
self.undoText = candidate;
self.undoAfterLength = candidateAfterLen;
KB_UNDO_LOG(@"recordClear/upgradedFromDeletion", self.undoText);
} else {
KB_UNDO_LOG(@"recordClear/keepDeletionSnapshot", self.undoText);
}
self.snapshotSource = KBUndoSnapshotSourceClear;
[self kb_updateHasUndo:YES];
return;
}
}
self.undoText = candidate;
self.undoAfterLength = candidateAfterLen;
self.snapshotSource = KBUndoSnapshotSourceClear;
KB_UNDO_LOG(@"recordClear/set", self.undoText);
[self kb_updateHasUndo:YES];
}
- (void)performUndoFromResponder:(UIResponder *)responder {
if (!self.hasUndo) { return; }
UIInputViewController *ivc = KBFindInputViewController(responder);
if (!ivc) { return; }
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
NSString *curBefore = proxy.documentContextBeforeInput ?: @"";
NSString *curAfter = proxy.documentContextAfterInput ?: @"";
KB_UNDO_LOG(@"performUndo/currentBefore", curBefore);
KB_UNDO_LOG(@"performUndo/currentAfter", curAfter);
NSString *insertText = [self kb_buildUndoInsertTextFromPieces];
if (insertText.length > 0) {
KB_UNDO_LOG(@"performUndo/insertDeletedText", insertText);
[proxy insertText:insertText];
[[KBInputBufferManager shared] appendText:insertText];
} else if (self.undoText.length > 0) {
KB_UNDO_LOG(@"performUndo/fallbackUndoText", self.undoText);
[self kb_clearAllTextForProxy:proxy];
[proxy insertText:self.undoText];
if (self.undoAfterLength > 0 &&
[proxy respondsToSelector:@selector(adjustTextPositionByCharacterOffset:)]) {
[proxy adjustTextPositionByCharacterOffset:-self.undoAfterLength];
}
[[KBInputBufferManager shared] resetWithText:self.undoText];
} else {
return;
}
self.undoText = @"";
self.undoAfterLength = 0;
self.snapshotSource = KBUndoSnapshotSourceNone;
[self.undoDeletedPieces removeAllObjects];
[self kb_updateHasUndo:NO];
}
- (void)registerNonClearAction {
if (!self.hasUndo) { return; }
if (self.undoText.length > 0) {
KB_UNDO_LOG(@"registerNonClearAction/clearUndoText", self.undoText);
}
if (self.undoDeletedPieces.count > 0) {
KB_UNDO_LOG(@"registerNonClearAction/clearDeletedPieces", [self kb_buildUndoInsertTextFromPieces]);
}
self.undoText = @"";
self.undoAfterLength = 0;
self.snapshotSource = KBUndoSnapshotSourceNone;
[self.undoDeletedPieces removeAllObjects];
[self kb_updateHasUndo:NO];
}
#pragma mark - Helpers
- (void)kb_updateHasUndo:(BOOL)hasUndo {
if (self.hasUndo == hasUndo) { return; }
self.hasUndo = hasUndo;
[[NSNotificationCenter defaultCenter] postNotificationName:KBBackspaceUndoStateDidChangeNotification object:self];
}
- (NSString *)kb_lastComposedCharacterFromString:(NSString *)text {
if (text.length == 0) { return @""; }
__block NSString *last = @"";
[text enumerateSubstringsInRange:NSMakeRange(0, text.length)
options:NSStringEnumerationByComposedCharacterSequences | NSStringEnumerationReverse
usingBlock:^(NSString *substring, __unused NSRange substringRange, __unused NSRange enclosingRange, BOOL *stop) {
last = substring ?: @"";
*stop = YES;
}];
return last ?: @"";
}
- (NSString *)kb_buildUndoInsertTextFromPieces {
if (self.undoDeletedPieces.count == 0) { return @""; }
NSMutableString *result = [NSMutableString string];
for (NSInteger i = (NSInteger)self.undoDeletedPieces.count - 1; i >= 0; i--) {
NSString *piece = self.undoDeletedPieces[(NSUInteger)i] ?: @"";
if (piece.length == 0) { continue; }
[result appendString:piece];
}
return result;
}
static const NSInteger kKBUndoClearMaxRounds = 200;
- (void)kb_clearAllTextForProxy:(id<UITextDocumentProxy>)proxy {
if (!proxy) { return; }
if ([proxy respondsToSelector:@selector(adjustTextPositionByCharacterOffset:)]) {
NSInteger guard = 0;
NSString *contextAfter = proxy.documentContextAfterInput ?: @"";
while (contextAfter.length > 0 && guard < kKBUndoClearMaxRounds) {
NSInteger offset = (NSInteger)contextAfter.length;
[proxy adjustTextPositionByCharacterOffset:offset];
for (NSUInteger i = 0; i < contextAfter.length; i++) {
[proxy deleteBackward];
}
guard += 1;
contextAfter = proxy.documentContextAfterInput ?: @"";
}
}
NSInteger guard = 0;
NSString *contextBefore = proxy.documentContextBeforeInput ?: @"";
while (contextBefore.length > 0 && guard < kKBUndoClearMaxRounds) {
for (NSUInteger i = 0; i < contextBefore.length; i++) {
[proxy deleteBackward];
}
guard += 1;
contextBefore = proxy.documentContextBeforeInput ?: @"";
}
}
@end

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