Compare commits
567 Commits
efb04d134e
...
dev_st_重构2
| Author | SHA1 | Date | |
|---|---|---|---|
| 47291934a2 | |||
| e619f48f93 | |||
| f55a70681c | |||
| cb86f7c32c | |||
| 40ef964b8c | |||
| 4269fde923 | |||
| c3e037e070 | |||
| a711be4c4d | |||
| 69bd2b2af9 | |||
| 82222afd76 | |||
| 92ca5c6180 | |||
| 851c0d9531 | |||
| 1c9013bede | |||
| 0a16a4f240 | |||
| 27d4b2b817 | |||
| bc623676ca | |||
| 5edf1751ff | |||
| 0ac47925fd | |||
| 635ad932c7 | |||
| cbe0a53cac | |||
| 5c273c3963 | |||
| c9743cb363 | |||
| f0cb69948e | |||
| 0144f9cc6d | |||
| ae4070ae88 | |||
| a83fd918a8 | |||
| 4168da618e | |||
| d2ffada83f | |||
| 76d387e08b | |||
| ea0df4fb19 | |||
| 02323fb5f1 | |||
| 3c71797b7b | |||
| 4c57f16058 | |||
| cb2e8467a7 | |||
| 4dfd6f5cbb | |||
| e4223b3a4c | |||
| 3d19403539 | |||
| 3cb02d5b76 | |||
| 750b391100 | |||
| faccf6f10f | |||
| 35b1fc0f1e | |||
| b73f225d15 | |||
| dd59094a16 | |||
| bacaf537f3 | |||
| 619d356d31 | |||
| db9f07d199 | |||
| 3ed120106e | |||
| ff4edab820 | |||
| 3e30f619b9 | |||
| 533e23ebfe | |||
| 85fb407717 | |||
| c1b50b407d | |||
| 7c7e2477cb | |||
| faae0297cb | |||
| e50eaecbd9 | |||
| 879dbb860c | |||
| b4e4b7b606 | |||
| 68a610e0a8 | |||
| 305326aa9a | |||
| 61095a379f | |||
| 822a814f85 | |||
| 0bd0392191 | |||
| b9663037f5 | |||
| a0923c8572 | |||
| d482cfcb7d | |||
| 9e6d2906f8 | |||
| 6f7bb4f960 | |||
| fa9af5ff1b | |||
| 08628bcd1d | |||
| 19cb29616f | |||
| 6e50cdcd2a | |||
| f1b52151be | |||
| 993ec623af | |||
| 0416a64235 | |||
| 2b75ad90fb | |||
| 0ac9030f80 | |||
| ea9c40f64f | |||
| 48c90fa0be | |||
| fe59a0cb45 | |||
| 81bc50ce17 | |||
| 6ae504823b | |||
| d2f582b7f8 | |||
| cc82396195 | |||
| 2ff8a7a4af | |||
| 3c0b7e754c | |||
| 3705db4aab | |||
| 36774a8a2c | |||
| 36135313d8 | |||
| 23c0d14128 | |||
| d0c5cada35 | |||
| b556e6841d | |||
| 26096abbcc | |||
| 766c62f3c0 | |||
| 07a77149fc | |||
| 32ebc6fb65 | |||
| 25fbe9b64e | |||
| 4392296616 | |||
| ef52cd4872 | |||
| 70a8466d9f | |||
| 66d85f78a0 | |||
| 93a20cd92a | |||
| 9a54a2ae6c | |||
| 1b9ce1622d | |||
| b4db79eba8 | |||
| 22f77d56ea | |||
| d8d5bdc3ae | |||
| 7d583ceb1d | |||
| 51b744ecd7 | |||
| 3fd7d2af2e | |||
| db869552e4 | |||
| b34de116a3 | |||
| e67bc37571 | |||
| 2b749cd2b0 | |||
| ce889e1ed0 | |||
| e8b4b2c58a | |||
| 3a5a6395af | |||
| a22599feda | |||
| 6a177ceebc | |||
| f9d7579536 | |||
| 0fa31418f6 | |||
| 77fd46aa34 | |||
| 6ad9783bcb | |||
| edc25c159d | |||
| 06a572c08a | |||
| 36c0b0b210 | |||
| d1d47336c2 | |||
| 063ceae10f | |||
| 552387293c | |||
| 93489b09d9 | |||
| 663cb8493b | |||
| ac0d9584d8 | |||
| 7fa124d45f | |||
| 3dfb8f31e2 | |||
| 619c02f236 | |||
| 28852a8d4b | |||
| b021fd308f | |||
| 169a1929d7 | |||
| b5da9f35a5 | |||
| 8f4deaac4e | |||
| d479d1903b | |||
| 32c4138ae0 | |||
| da62d4f411 | |||
| 85dcd72a5d | |||
| 21fcbe3665 | |||
| 1b6724f043 | |||
| ef332ecaa1 | |||
| 3d6d673c0b | |||
| 674f09d5b6 | |||
| 11d8f78b1b | |||
| bbacef4ff7 | |||
| 8e692647d3 | |||
| 6f80f969a4 | |||
| bdf2a9af80 | |||
| e858d35722 | |||
| f2d5210313 | |||
| 1b0af3e2d6 | |||
| 0965cd3c7e | |||
| c3909d63da | |||
| 1096f24c57 | |||
| 7ed84fd445 | |||
| 4e2d7d2908 | |||
| 34089ddeea | |||
| 6ec98468de | |||
| 2d5919016f | |||
| c0fa51bb2e | |||
| 6713f36387 | |||
| f24750458a | |||
| 510a2f4d66 | |||
| ae37730da6 | |||
| 203f104ece | |||
| 8e934dd83a | |||
| 1676916a5c | |||
| 1af5a0e849 | |||
| 5b6e0a8fbf | |||
| 9968883bab | |||
| af5f637d31 | |||
| 0a725e845e | |||
| 6a539dc3c5 | |||
| 73d6ec933a | |||
| 000d603241 | |||
| fbf9fe9f2a | |||
| 8e4d7e1ee8 | |||
| 262eb57b36 | |||
| 2e1c261775 | |||
| 6ad2079351 | |||
| a477592f5d | |||
| 6f336e8368 | |||
| 17e038beb1 | |||
| 4e6fd90668 | |||
| 5cfc76e6c5 | |||
| 9e33c93763 | |||
| 1c9ae7bc06 | |||
| 472e9ad341 | |||
| 19c69f4f6f | |||
| 8788cbb105 | |||
| ea77e9a5f8 | |||
| eaaf0e1ed6 | |||
| 8a344b293d | |||
| 9cdd024ce2 | |||
| d612346db5 | |||
| 2cacaab974 | |||
| 200b1ab9f8 | |||
| 8d0939cd78 | |||
| df51620ca9 | |||
| 70520fb7d9 | |||
| 7587fe6714 | |||
| 1c8834caf6 | |||
| 68306aa07f | |||
| 9544ad75ff | |||
| d90e080981 | |||
| fc65052583 | |||
| e0d5ae0257 | |||
| 639ce7eafd | |||
| e0379d3717 | |||
| c108077178 | |||
| 182e5b9da1 | |||
| ea4ecc05b4 | |||
| ae05127292 | |||
| 38a3d2879e | |||
| 10ba4cd80f | |||
| 857822c49c | |||
| 85012eab78 | |||
| 8aa43d723a | |||
| 1ecb7d60e5 | |||
| 904a6c932a | |||
| 886de394d0 | |||
| 8bad475288 | |||
| 4a26419e67 | |||
| 1e04e7c39a | |||
| dde8716262 | |||
| c6b4444589 | |||
| 5bd20a911f | |||
| b43567748c | |||
| 59297eac77 | |||
| 9f7d805a52 | |||
| 6800864866 | |||
| e8a980ff5b | |||
| dfbd5efe69 | |||
| 4a474e9b44 | |||
| 30f2e4f24f | |||
| c898d16688 | |||
| f10ddd9a31 | |||
| 1651258eec | |||
| fd0ddfd45a | |||
| 444877fb73 | |||
| 1436464eca | |||
| 05b9a0b823 | |||
| 053001170a | |||
| 9cafb0f70e | |||
| a399af53b5 | |||
| a7574cd286 | |||
| 06dee39566 | |||
| d5b4ef2b59 | |||
| 2620bd6845 | |||
| 1f0bdb1bd4 | |||
| b21a2a8193 | |||
| 1f07120289 | |||
| 64e0218add | |||
| 7972188ac3 | |||
| 6963c8016f | |||
| 633e6a9123 | |||
| 437f796a08 | |||
| 2b08dd44ee | |||
| 1eeeef266b | |||
| 3813974eae | |||
| 6a5bda44e6 | |||
| 704553cd4e | |||
| 35597f89ca | |||
| 577b749198 | |||
| cccced6afa | |||
| 14637a21ad | |||
| 111fe42782 | |||
| 58da905ade | |||
| f338a54e41 | |||
| 526ac1a7df | |||
| 7f90240731 | |||
| e4442afe72 | |||
| be1d1ad70d | |||
| 4fd0a52a36 | |||
| 04c7d19c37 | |||
| 94269209e0 | |||
| 2b4123741a | |||
| 45695364e9 | |||
| d348b35984 | |||
| e39104c431 | |||
| 7b86b739eb | |||
| ade23e7a20 | |||
| 1b2b0c1143 | |||
| 0400d2020b | |||
| 2cc93e0b48 | |||
| fd8c08316b | |||
| 0a1c30f669 | |||
| ca3ea0630e | |||
| a26b6b58a9 | |||
| 6fd4a86a7e | |||
| fa999f502f | |||
| ad18a47d21 | |||
| d2258883df | |||
| f7d11c5f8b | |||
| 40d9b5aad4 | |||
| 6ac6514f89 | |||
| eb7ad1a9f1 | |||
| 17ce91d40a | |||
| 515487e748 | |||
| 7a25a6a5fa | |||
| 64887054e0 | |||
| 8f63741d8c | |||
| f593ef0b4a | |||
| 231f7f8c13 | |||
| c9863cd353 | |||
| ce4c4f0531 | |||
| af2bcc42fd | |||
| 1596aac717 | |||
| d13bb734b1 | |||
| 2665b5ad1f | |||
| cffd77eeb5 | |||
| b8f8d2e6b0 | |||
| 279255a14c | |||
| b216ddaa61 | |||
| f770f8055e | |||
| 819d74cc8d | |||
| 9651ae7ad7 | |||
| eca168957d | |||
| f026b9f9fd | |||
| b368ba0159 | |||
| 43e8b85656 | |||
| 716f91bdd0 | |||
| b00283cd96 | |||
| 25edf2d817 | |||
| 1d6371c37e | |||
| 82123fc232 | |||
| 6f02bc7cf5 | |||
| a5ff2ce51b | |||
| 4deccb76dc | |||
| 93556ddb9c | |||
| 681fced59d | |||
| 91e2b047eb | |||
| c1c4c85bd2 | |||
| d06e0499bc | |||
| 49f730b609 | |||
| 04a392e7c7 | |||
| 22e393e588 | |||
| 6556689c8f | |||
| a50d18b486 | |||
| 599a5de3bc | |||
| c1eb6a3458 | |||
| b87998549c | |||
| 27aa723e7d | |||
| 6be90ebb10 | |||
| 2f55e7bfa1 | |||
| c56655c728 | |||
| 5e4c16c577 | |||
| 8245e7b3d1 | |||
| cafde48f4a | |||
| c897111855 | |||
| a2bb61408b | |||
| 9268a21eb8 | |||
| d4c553f072 | |||
| 73802b6e80 | |||
| 1a0a444a99 | |||
| c37038f163 | |||
| 3144315de5 | |||
| 8f16250cbe | |||
| 95d5e2b972 | |||
| 2760a070a3 | |||
| 2435d760e8 | |||
| 80e4db86e4 | |||
| 4ab8a61a3c | |||
| 73c83153f9 | |||
| 1b67998f6a | |||
| b8cc38aa61 | |||
| c4398a689b | |||
| b660eb19f4 | |||
| 1eb73f5257 | |||
| 71423df1c0 | |||
| 709e0f4453 | |||
| 18df76a2b4 | |||
| 15e37841bb | |||
| 8e93f8f86f | |||
| fd35c5c993 | |||
| fd7b3a7f75 | |||
| af8fff5b13 | |||
| fc87c545a0 | |||
| 0f4ca89060 | |||
| c371c7224e | |||
| 31bb72c8f4 | |||
| faa05e2a10 | |||
| 6bdd111a3a | |||
| 8296ac12b6 | |||
| b2994adc1c | |||
| 75d2e4072a | |||
| b23927968f | |||
| c27fd099f6 | |||
| bc1264e28f | |||
| 799b0f3989 | |||
| b3ce856ad4 | |||
| f51fe1fac9 | |||
| 8dbaa9dcf6 | |||
| 0196128008 | |||
| 4108aed4e0 | |||
| cc55bb107a | |||
| 7518a29d2f | |||
| 37e131eb09 | |||
| 3dcc4932c3 | |||
| 254e65906a | |||
| ced0b88ca4 | |||
| b2021dcb3c | |||
| 0ef7b7d1d8 | |||
| 7254e2dbd9 | |||
| 26ef29ac4e | |||
| 005e3c7581 | |||
| ee433db4ad | |||
| ea4b8168b7 | |||
| f366a4aa6c | |||
| d849b201ca | |||
| dc813fcabc | |||
| 1d215ffdb3 | |||
| d9bfc30c88 | |||
| 9305acb69b | |||
| f9a8955384 | |||
| 1f9dbba39d | |||
| dace0a9309 | |||
| 4f2e80e482 | |||
| b27b9f9ee1 | |||
| 66a1ddef66 | |||
| eacac8425c | |||
| d164514fcf | |||
| ae79d1b1ba | |||
| 50163d02a7 | |||
| 5ec950cc61 | |||
| a61b5fa2fd | |||
| f406416698 | |||
| debbe2777b | |||
| bc261661ae | |||
| 0aead49816 | |||
| 66b7a9218e | |||
| 2f4205ad1a | |||
| fea22aecab | |||
| 62f3ddae4a | |||
| c317afc0fe | |||
| 1dbe04cdf9 | |||
| afc44cb471 | |||
| 39d8b3d547 | |||
| f387b95d0d | |||
| 1d064c1f31 | |||
| 3440cc4773 | |||
| 20b13bcffa | |||
| 105e2ddf9b | |||
| a1a38d821c | |||
| 83987db5ac | |||
| d10114572e | |||
| e34288ae56 | |||
| 17b8bf2bfd | |||
| 57bd4ba109 | |||
| e4ba237a00 | |||
| 9059a24637 | |||
| dc0c55c495 | |||
| a007a77db9 | |||
| 3eb3a86376 | |||
| 1dc9560a1f | |||
| 8069b08fab | |||
| 2c8142c0d2 | |||
| 9f4110b24a | |||
| 1cdc17b710 | |||
| 97316c7989 | |||
| fac5e7657c | |||
| 50dcb78417 | |||
| 998fa7aa67 | |||
| 5e1a1f540e | |||
| 2415e97c97 | |||
| aa71cc3c4f | |||
| e5ddcc4308 | |||
| 883b222254 | |||
| dc9ee10023 | |||
| 2c4a4329ff | |||
| 553238de0c | |||
| 80b6102673 | |||
| 705b0f374e | |||
| 5bdc7ddec0 | |||
| 5d2a3de2f4 | |||
| 675a9f6d64 | |||
| 41b14ceea4 | |||
| a729396401 | |||
| 3b0beb52da | |||
| faeb930fe3 | |||
| 9a39c29e88 | |||
| b23c9a678b | |||
| 96cd32ed99 | |||
| 50dd53b0c0 | |||
| 48a12f0919 | |||
| 91d754b389 | |||
| 450798c8bd | |||
| c3acc11f6a | |||
| d592c9f12e | |||
| 26e39ce416 | |||
| 32521208a0 | |||
| 6e969648c6 | |||
| 074596ebcb | |||
| f0542c11c8 | |||
| 0fa3d10284 | |||
| 0d13192723 | |||
| a72aae84ef | |||
| 6ba1339c0b | |||
| a75afbe4c1 | |||
| 1f45564539 | |||
| 41aec6b89e | |||
| a1db745b6c | |||
| 15fc9621cd | |||
| d7874829d9 | |||
| abf32e8457 | |||
| efdcf60ed1 | |||
| 7a1b17d060 | |||
| f43f94b94d | |||
| 3e2dc4bcb6 | |||
| 6fb9e56720 | |||
| e2cff76d13 | |||
| 6e57f1c853 | |||
| 4fe18c77dc | |||
| cb5819e330 | |||
| 6f20e6aeb1 | |||
| 5af2612ff7 | |||
| cac2f13b88 | |||
| edf88721da | |||
| 915b329805 | |||
| 1673a2f4be | |||
| e4cebeac85 | |||
| c7021e382e | |||
| ffea9d2022 | |||
| 90c1e7ff6c | |||
| 59d04bb33c | |||
| eb0d3aaa71 | |||
| 10dfe9b1d6 | |||
| 6993bfd682 | |||
| 247a87891e | |||
| 9af91cc4bc | |||
| 4f23118ec0 | |||
| 482756f6f0 | |||
| 85a3694e35 | |||
| f58bf61500 | |||
| 783d088f22 | |||
| 74476cd592 | |||
| 9b43274e93 | |||
| 8ce1d95c8c | |||
| e8c88a6148 | |||
| e218c1bf3d | |||
| c5326a3079 | |||
| 9101ffaab0 | |||
| e594711fa3 | |||
| 7fd084e529 | |||
| 11b25241bf | |||
| 8fcfce7376 | |||
| 23317c9fd4 | |||
| 045d5eaff8 | |||
| 72b6dbb157 | |||
| e78b56e2cb | |||
| 6c05026402 | |||
| 13facba33a | |||
| 935284388c | |||
| 4c7fd9049f | |||
| 0031b7a5f6 | |||
| 02dd204744 | |||
| f28f7de49d | |||
| c2859f888a | |||
| a2b51189aa | |||
| 2f2f20cfc2 | |||
| 377e88b6db | |||
| 1deca2ae5b |
13
.claude/settings.local.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"WebSearch",
|
||||||
|
"Bash(git checkout:*)",
|
||||||
|
"Bash(xcodebuild:*)",
|
||||||
|
"Bash(plutil:*)",
|
||||||
|
"Bash(find:*)",
|
||||||
|
"Bash(ls:*)",
|
||||||
|
"Bash(wc:*)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
18
CustomKeyboard/CustomKeyboard.entitlements
Normal 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>
|
||||||
@@ -2,6 +2,12 @@
|
|||||||
<!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>NSMicrophoneUsageDescription</key>
|
||||||
|
<string>需要使用麦克风进行语音输入</string>
|
||||||
<key>NSExtension</key>
|
<key>NSExtension</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSExtensionAttributes</key>
|
<key>NSExtensionAttributes</key>
|
||||||
|
|||||||
BIN
CustomKeyboard/KeyboardAssets.xcassets/App_icon.imageset/App_icon.png
vendored
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
CustomKeyboard/KeyboardAssets.xcassets/App_icon.imageset/App_icon@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 9.2 KiB |
BIN
CustomKeyboard/KeyboardAssets.xcassets/App_icon.imageset/App_icon@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 19 KiB |
23
CustomKeyboard/KeyboardAssets.xcassets/App_icon.imageset/Contents.json
vendored
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
6
CustomKeyboard/KeyboardAssets.xcassets/Contents.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
22
CustomKeyboard/KeyboardAssets.xcassets/ai_key_icon.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "切图 271@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "切图 271@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
CustomKeyboard/KeyboardAssets.xcassets/ai_key_icon.imageset/切图 271@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
CustomKeyboard/KeyboardAssets.xcassets/ai_key_icon.imageset/切图 271@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
22
CustomKeyboard/KeyboardAssets.xcassets/ai_limit_close.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "ai_limit_close@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "ai_limit_close@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
CustomKeyboard/KeyboardAssets.xcassets/ai_limit_close.imageset/ai_limit_close@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
CustomKeyboard/KeyboardAssets.xcassets/ai_limit_close.imageset/ai_limit_close@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
22
CustomKeyboard/KeyboardAssets.xcassets/ai_limit_goto.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "ai_limit_goto@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "ai_limit_goto@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
CustomKeyboard/KeyboardAssets.xcassets/ai_limit_goto.imageset/ai_limit_goto@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
CustomKeyboard/KeyboardAssets.xcassets/ai_limit_goto.imageset/ai_limit_goto@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 9.5 KiB |
22
CustomKeyboard/KeyboardAssets.xcassets/ai_limit_icon.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "ai_limit_icon@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "ai_limit_icon@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
CustomKeyboard/KeyboardAssets.xcassets/ai_limit_icon.imageset/ai_limit_icon@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 358 KiB |
BIN
CustomKeyboard/KeyboardAssets.xcassets/ai_limit_icon.imageset/ai_limit_icon@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 733 KiB |
22
CustomKeyboard/KeyboardAssets.xcassets/back_keybord_icon.imageset/Contents.json
vendored
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
CustomKeyboard/KeyboardAssets.xcassets/back_keybord_icon.imageset/back_keybord_icon@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
CustomKeyboard/KeyboardAssets.xcassets/back_keybord_icon.imageset/back_keybord_icon@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
22
CustomKeyboard/KeyboardAssets.xcassets/buy_sel_icon.imageset/Contents.json
vendored
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
CustomKeyboard/KeyboardAssets.xcassets/buy_sel_icon.imageset/buy_sel_icon@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
CustomKeyboard/KeyboardAssets.xcassets/buy_sel_icon.imageset/buy_sel_icon@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
22
CustomKeyboard/KeyboardAssets.xcassets/close_icon.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "close_icon@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "close_icon@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
CustomKeyboard/KeyboardAssets.xcassets/close_icon.imageset/close_icon@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
CustomKeyboard/KeyboardAssets.xcassets/close_icon.imageset/close_icon@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
22
CustomKeyboard/KeyboardAssets.xcassets/home_ai_icon.imageset/Contents.json
vendored
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
CustomKeyboard/KeyboardAssets.xcassets/home_ai_icon.imageset/home_ai_icon@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 9.4 KiB |
BIN
CustomKeyboard/KeyboardAssets.xcassets/home_ai_icon.imageset/home_ai_icon@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 18 KiB |
22
CustomKeyboard/KeyboardAssets.xcassets/home_chat_icon.imageset/Contents.json
vendored
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
CustomKeyboard/KeyboardAssets.xcassets/home_chat_icon.imageset/home_chat_icon@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 8.2 KiB |
BIN
CustomKeyboard/KeyboardAssets.xcassets/home_chat_icon.imageset/home_chat_icon@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 16 KiB |
22
CustomKeyboard/KeyboardAssets.xcassets/home_emotion_icon.imageset/Contents.json
vendored
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
CustomKeyboard/KeyboardAssets.xcassets/home_emotion_icon.imageset/home_emotion_icon@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 9.3 KiB |
BIN
CustomKeyboard/KeyboardAssets.xcassets/home_emotion_icon.imageset/home_emotion_icon@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 18 KiB |
22
CustomKeyboard/KeyboardAssets.xcassets/home_keyboard_icon.imageset/Contents.json
vendored
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
CustomKeyboard/KeyboardAssets.xcassets/home_keyboard_icon.imageset/home_keyboard_icon@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
CustomKeyboard/KeyboardAssets.xcassets/home_keyboard_icon.imageset/home_keyboard_icon@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 25 KiB |
86
CustomKeyboard/KeyboardAssets.xcassets/kb_del_icon.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "light"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "kb_del_icon@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "light"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"filename" : "kb_del_icon@2x 1.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"filename" : "切图 256@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "kb_del_icon@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "light"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"filename" : "kb_del_icon@3x 1.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"filename" : "切图 256@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
CustomKeyboard/KeyboardAssets.xcassets/kb_del_icon.imageset/kb_del_icon@2x 1.png
vendored
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
CustomKeyboard/KeyboardAssets.xcassets/kb_del_icon.imageset/kb_del_icon@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
CustomKeyboard/KeyboardAssets.xcassets/kb_del_icon.imageset/kb_del_icon@3x 1.png
vendored
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
CustomKeyboard/KeyboardAssets.xcassets/kb_del_icon.imageset/kb_del_icon@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
CustomKeyboard/KeyboardAssets.xcassets/kb_del_icon.imageset/切图 256@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 1008 B |
BIN
CustomKeyboard/KeyboardAssets.xcassets/kb_del_icon.imageset/切图 256@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
22
CustomKeyboard/KeyboardAssets.xcassets/kb_zt_icon.imageset/Contents.json
vendored
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
CustomKeyboard/KeyboardAssets.xcassets/kb_zt_icon.imageset/kb_zt_icon@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 1014 B |
BIN
CustomKeyboard/KeyboardAssets.xcassets/kb_zt_icon.imageset/kb_zt_icon@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
22
CustomKeyboard/KeyboardAssets.xcassets/key_revoke.imageset/Contents.json
vendored
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
CustomKeyboard/KeyboardAssets.xcassets/key_revoke.imageset/key_revoke@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
CustomKeyboard/KeyboardAssets.xcassets/key_revoke.imageset/key_revoke@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
22
CustomKeyboard/KeyboardAssets.xcassets/keybord_bg_icon.imageset/Contents.json
vendored
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
CustomKeyboard/KeyboardAssets.xcassets/keybord_bg_icon.imageset/keybord_bg_icon@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 486 KiB |
BIN
CustomKeyboard/KeyboardAssets.xcassets/keybord_bg_icon.imageset/keybord_bg_icon@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
22
CustomKeyboard/KeyboardAssets.xcassets/upgrad_vip_icon.imageset/Contents.json
vendored
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
CustomKeyboard/KeyboardAssets.xcassets/upgrad_vip_icon.imageset/upgrad_vip_icon@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
CustomKeyboard/KeyboardAssets.xcassets/upgrad_vip_icon.imageset/upgrad_vip_icon@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 39 KiB |
@@ -5,75 +5,237 @@
|
|||||||
// Created by Mac on 2025/10/27.
|
// Created by Mac on 2025/10/27.
|
||||||
//
|
//
|
||||||
|
|
||||||
#import "KeyboardViewController.h"
|
#import "KeyboardViewController+Private.h"
|
||||||
|
|
||||||
static CGFloat KEYBOARDHEIGHT = 256;
|
#import "KBBackspaceUndoManager.h"
|
||||||
|
#import "KBChatLimitPopView.h"
|
||||||
|
#import "KBChatPanelView.h"
|
||||||
|
#import "KBFullAccessManager.h"
|
||||||
|
#import "KBFunctionView.h"
|
||||||
|
#import "KBInputBufferManager.h"
|
||||||
|
#import "KBKeyBoardMainView.h"
|
||||||
|
#import "KBKeyboardSubscriptionView.h"
|
||||||
|
#import "KBLocalizationManager.h"
|
||||||
|
#import "KBSkinManager.h"
|
||||||
|
#import "KBSuggestionEngine.h"
|
||||||
|
#import <SDWebImage/SDWebImage.h>
|
||||||
|
|
||||||
@interface KeyboardViewController ()
|
#if DEBUG
|
||||||
@property (nonatomic, strong) UIButton *nextKeyboardButton;
|
#import <mach/mach.h>
|
||||||
@end
|
#endif
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
static NSInteger sKBKeyboardVCAliveCount = 0;
|
||||||
|
|
||||||
|
static uint64_t KBPhysFootprintBytes(void) {
|
||||||
|
task_vm_info_data_t vmInfo;
|
||||||
|
mach_msg_type_number_t count = TASK_VM_INFO_COUNT;
|
||||||
|
kern_return_t kr = task_info(mach_task_self(), TASK_VM_INFO,
|
||||||
|
(task_info_t)&vmInfo, &count);
|
||||||
|
if (kr != KERN_SUCCESS) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return (uint64_t)vmInfo.phys_footprint;
|
||||||
|
}
|
||||||
|
|
||||||
|
static NSString *KBFormatMB(uint64_t bytes) {
|
||||||
|
double mb = (double)bytes / 1024.0 / 1024.0;
|
||||||
|
return [NSString stringWithFormat:@"%.1fMB", mb];
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
@implementation KeyboardViewController
|
@implementation KeyboardViewController
|
||||||
|
|
||||||
- (void)updateViewConstraints {
|
{
|
||||||
[super updateViewConstraints];
|
BOOL _kb_didTriggerLoginDeepLinkOnce;
|
||||||
|
#if DEBUG
|
||||||
// Add custom view sizing constraints here
|
BOOL _kb_debugDidCountAlive;
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)viewDidLoad {
|
- (void)viewDidLoad {
|
||||||
[super viewDidLoad];
|
[super viewDidLoad];
|
||||||
//
|
#if DEBUG
|
||||||
// // Perform custom UI setup here
|
if (!_kb_debugDidCountAlive) {
|
||||||
// self.nextKeyboardButton = [UIButton buttonWithType:UIButtonTypeSystem];
|
_kb_debugDidCountAlive = YES;
|
||||||
//
|
sKBKeyboardVCAliveCount += 1;
|
||||||
// [self.nextKeyboardButton setTitle:NSLocalizedString(@"Next Keyboard", @"Title for 'Next Keyboard' button") forState:UIControlStateNormal];
|
}
|
||||||
// [self.nextKeyboardButton sizeToFit];
|
NSLog(@"[Keyboard] KeyboardViewController viewDidLoad alive=%ld self=%p mem=%@",
|
||||||
// self.nextKeyboardButton.translatesAutoresizingMaskIntoConstraints = NO;
|
(long)sKBKeyboardVCAliveCount, self, KBFormatMB(KBPhysFootprintBytes()));
|
||||||
//
|
#endif
|
||||||
// [self.nextKeyboardButton addTarget:self action:@selector(handleInputModeListFromView:withEvent:) forControlEvents:UIControlEventAllTouchEvents];
|
// 撤销删除是“上一段删除操作”的临时状态;键盘被系统回收/重建或跨页面回来时应当清空,避免误显示。
|
||||||
//
|
[[KBBackspaceUndoManager shared] registerNonClearAction];
|
||||||
// [self.view addSubview:self.nextKeyboardButton];
|
[self setupUI];
|
||||||
//
|
self.suggestionEngine = [KBSuggestionEngine shared];
|
||||||
// [self.nextKeyboardButton.leftAnchor constraintEqualToAnchor:self.view.leftAnchor].active = YES;
|
self.currentWord = @"";
|
||||||
// [self.nextKeyboardButton.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor].active = YES;
|
// 指定 HUD 的承载视图(扩展里无法取到 App 的 KeyWindow)
|
||||||
|
[KBHUD setContainerView:self.view];
|
||||||
[self setupUI];
|
// 绑定完全访问管理器,便于统一感知和联动网络开关
|
||||||
|
[[KBFullAccessManager shared] bindInputController:self];
|
||||||
|
self.kb_fullAccessObserverToken = [[NSNotificationCenter defaultCenter]
|
||||||
|
addObserverForName:KBFullAccessChangedNotification
|
||||||
|
object:nil
|
||||||
|
queue:[NSOperationQueue mainQueue]
|
||||||
|
usingBlock:^(__unused NSNotification *_Nonnull note){
|
||||||
|
// 如需,可在此刷新与完全访问相关的 UI
|
||||||
|
}];
|
||||||
|
|
||||||
|
// 皮肤变化时,立即应用
|
||||||
|
__weak typeof(self) weakSelf = self;
|
||||||
|
self.kb_skinObserverToken = [[NSNotificationCenter defaultCenter]
|
||||||
|
addObserverForName:KBSkinDidChangeNotification
|
||||||
|
object:nil
|
||||||
|
queue:[NSOperationQueue mainQueue]
|
||||||
|
usingBlock:^(__unused NSNotification *_Nonnull note) {
|
||||||
|
__strong typeof(weakSelf) self = weakSelf;
|
||||||
|
if (!self) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
[self kb_applyTheme];
|
||||||
|
}];
|
||||||
|
[self kb_applyTheme];
|
||||||
|
[self kb_registerDarwinSkinInstallObserver];
|
||||||
|
[self kb_consumePendingShopSkin];
|
||||||
|
[self kb_applyDefaultSkinIfNeeded];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (void)didReceiveMemoryWarning {
|
||||||
- (void)setupUI {
|
[super didReceiveMemoryWarning];
|
||||||
CGFloat toolBarHeight = 40;
|
// 扩展进程内存上限较小:在系统发出内存警告时主动清理可重建的缓存,降低被系统杀死概率。
|
||||||
CGFloat bottom = 5;
|
self.kb_cachedGradientImage = nil;
|
||||||
CGFloat buttonSpace = 8;
|
[self.kb_defaultGradientLayer removeFromSuperlayer];
|
||||||
CGFloat eachButtonHeight = (KEYBOARDHEIGHT - toolBarHeight - 10 - 8 * 3 - bottom) / 4;
|
self.kb_defaultGradientLayer = nil;
|
||||||
|
[[KBSkinManager shared] clearRuntimeImageCaches];
|
||||||
UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, SCREEN_WIDTH, 30)];
|
[[SDImageCache sharedImageCache] clearMemory];
|
||||||
view.backgroundColor = [UIColor redColor];
|
|
||||||
[self.view addSubview:view];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (void)viewWillAppear:(BOOL)animated {
|
||||||
- (void)viewWillLayoutSubviews
|
[super viewWillAppear:animated];
|
||||||
{
|
// FIX: iOS 26 键盘闪烁问题 —— 恢复键盘正确高度
|
||||||
self.nextKeyboardButton.hidden = !self.needsInputModeSwitchKey;
|
// setupUI 中高度初始为 0(防止系统预渲染快照闪烁),此处恢复为实际键盘高度。
|
||||||
[super viewWillLayoutSubviews];
|
// 此时系统已准备好键盘滑入动画,恢复高度后键盘将正常从底部滑入。
|
||||||
|
CGFloat portraitWidth = [self kb_portraitWidth];
|
||||||
|
CGFloat keyboardHeight = [self kb_keyboardHeightForWidth:portraitWidth];
|
||||||
|
if (self.kb_heightConstraint) {
|
||||||
|
self.kb_heightConstraint.constant = keyboardHeight;
|
||||||
|
}
|
||||||
|
// 进入/重新进入输入界面时,清理上一次会话残留的撤销状态与缓存,避免显示“撤销删除”但实际上已不可撤销。
|
||||||
|
[[KBBackspaceUndoManager shared] registerNonClearAction];
|
||||||
|
[[KBInputBufferManager shared] resetWithText:@""];
|
||||||
|
[[KBLocalizationManager shared] reloadFromSharedStorageIfNeeded];
|
||||||
|
// 键盘再次出现时,恢复 HUD 容器与主题(viewDidDisappear 里可能已清理图片/缓存)。
|
||||||
|
[KBHUD setContainerView:self.view];
|
||||||
|
[self kb_ensureKeyBoardMainViewIfNeeded];
|
||||||
|
[self kb_applyTheme];
|
||||||
|
#if DEBUG
|
||||||
|
NSLog(@"[Keyboard] viewWillAppear self=%p mem=%@",
|
||||||
|
self, KBFormatMB(KBPhysFootprintBytes()));
|
||||||
|
#endif
|
||||||
|
// 注意:微信/QQ 等宿主的 documentContext 可能是“截断窗口”,这里只更新
|
||||||
|
// liveText,不要把它当作全文 manualSnapshot。
|
||||||
|
[[KBInputBufferManager shared]
|
||||||
|
updateFromExternalContextBefore:self.textDocumentProxy
|
||||||
|
.documentContextBeforeInput
|
||||||
|
after:self.textDocumentProxy
|
||||||
|
.documentContextAfterInput];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)textWillChange:(id<UITextInput>)textInput {
|
- (void)viewWillDisappear:(BOOL)animated {
|
||||||
// The app is about to change the document's contents. Perform any preparation here.
|
[super viewWillDisappear:animated];
|
||||||
|
[[KBBackspaceUndoManager shared] registerNonClearAction];
|
||||||
|
[self kb_releaseMemoryWhenKeyboardHidden];
|
||||||
|
#if DEBUG
|
||||||
|
NSLog(@"[Keyboard] viewWillDisappear self=%p mem=%@",
|
||||||
|
self, KBFormatMB(KBPhysFootprintBytes()));
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)viewDidDisappear:(BOOL)animated {
|
||||||
|
[super viewDidDisappear:animated];
|
||||||
|
// 再兜底一次,防止某些宿主只触发 willDisappear 而未触发 didDisappear。
|
||||||
|
[self kb_releaseMemoryWhenKeyboardHidden];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection {
|
||||||
|
[super traitCollectionDidChange:previousTraitCollection];
|
||||||
|
if (@available(iOS 13.0, *)) {
|
||||||
|
if (previousTraitCollection.userInterfaceStyle !=
|
||||||
|
self.traitCollection.userInterfaceStyle) {
|
||||||
|
self.kb_cachedGradientImage = nil;
|
||||||
|
[self kb_applyDefaultSkinIfNeeded];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)textDidChange:(id<UITextInput>)textInput {
|
- (void)textDidChange:(id<UITextInput>)textInput {
|
||||||
// The app has just changed the document's contents, the document context has been updated.
|
[super textDidChange:textInput];
|
||||||
|
[[KBInputBufferManager shared]
|
||||||
UIColor *textColor = nil;
|
updateFromExternalContextBefore:self.textDocumentProxy
|
||||||
if (self.textDocumentProxy.keyboardAppearance == UIKeyboardAppearanceDark) {
|
.documentContextBeforeInput
|
||||||
textColor = [UIColor whiteColor];
|
after:self.textDocumentProxy
|
||||||
} else {
|
.documentContextAfterInput];
|
||||||
textColor = [UIColor blackColor];
|
}
|
||||||
}
|
|
||||||
[self.nextKeyboardButton setTitleColor:textColor forState:UIControlStateNormal];
|
- (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];
|
||||||
|
|
||||||
|
// 首次布局完成后显示,避免闪烁
|
||||||
|
if (self.contentView.hidden) {
|
||||||
|
self.contentView.hidden = NO;
|
||||||
|
}
|
||||||
|
if (self.kb_defaultGradientLayer) {
|
||||||
|
self.kb_defaultGradientLayer.frame = self.bgImageView.bounds;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (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)dealloc {
|
||||||
|
if (self.kb_fullAccessObserverToken) {
|
||||||
|
[[NSNotificationCenter defaultCenter]
|
||||||
|
removeObserver:self.kb_fullAccessObserverToken];
|
||||||
|
self.kb_fullAccessObserverToken = nil;
|
||||||
|
}
|
||||||
|
if (self.kb_skinObserverToken) {
|
||||||
|
[[NSNotificationCenter defaultCenter]
|
||||||
|
removeObserver:self.kb_skinObserverToken];
|
||||||
|
self.kb_skinObserverToken = nil;
|
||||||
|
}
|
||||||
|
[self kb_unregisterDarwinSkinInstallObserver];
|
||||||
|
#if DEBUG
|
||||||
|
if (_kb_debugDidCountAlive) {
|
||||||
|
sKBKeyboardVCAliveCount -= 1;
|
||||||
|
}
|
||||||
|
NSLog(@"[Keyboard] KeyboardViewController dealloc alive=%ld self=%p mem=%@",
|
||||||
|
(long)sKBKeyboardVCAliveCount, self, KBFormatMB(KBPhysFootprintBytes()));
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -0,0 +1,712 @@
|
|||||||
|
//
|
||||||
|
// KeyboardViewController+Chat.m
|
||||||
|
// CustomKeyboard
|
||||||
|
//
|
||||||
|
// Created by Codex on 2026/02/22.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import "KeyboardViewController+Private.h"
|
||||||
|
|
||||||
|
#import "KBChatLimitPopView.h"
|
||||||
|
#import "KBChatMessage.h"
|
||||||
|
#import "KBChatPanelView.h"
|
||||||
|
#import "KBFullAccessManager.h"
|
||||||
|
#import "KBHostAppLauncher.h"
|
||||||
|
#import "KBInputBufferManager.h"
|
||||||
|
#import "KBNetworkManager.h"
|
||||||
|
#import "KBVM.h"
|
||||||
|
#import "Masonry.h"
|
||||||
|
|
||||||
|
#import <AVFoundation/AVFoundation.h>
|
||||||
|
|
||||||
|
static const NSUInteger kKBChatMessageLimit = 6;
|
||||||
|
|
||||||
|
@implementation KeyboardViewController (Chat)
|
||||||
|
|
||||||
|
#pragma mark - KBChatPanelViewDelegate
|
||||||
|
|
||||||
|
- (void)chatPanelView:(KBChatPanelView *)view didSendText:(NSString *)text {
|
||||||
|
NSString *trim =
|
||||||
|
[text stringByTrimmingCharactersInSet:
|
||||||
|
[NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
||||||
|
if (trim.length == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
[self kb_sendChatText:trim];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)chatPanelView:(KBChatPanelView *)view
|
||||||
|
didTapMessage:(KBChatMessage *)message {
|
||||||
|
if (message.audioFilePath.length == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
[self kb_playChatAudioAtPath:message.audioFilePath];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)chatPanelView:(KBChatPanelView *)view
|
||||||
|
didTapVoiceButtonForMessage:(KBChatMessage *)message {
|
||||||
|
if (!message)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// 如果有 audioData,直接播放
|
||||||
|
if (message.audioData && message.audioData.length > 0) {
|
||||||
|
[self kb_playChatAudioData:message.audioData];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果有 audioFilePath,播放文件
|
||||||
|
if (message.audioFilePath.length > 0) {
|
||||||
|
[self kb_playChatAudioAtPath:message.audioFilePath];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSLog(@"[Keyboard] 没有音频数据可播放");
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)chatPanelViewDidTapClose:(KBChatPanelView *)view {
|
||||||
|
// 清空 chatPanelView 内部的消息
|
||||||
|
[view kb_reloadWithMessages:@[]];
|
||||||
|
if (self.chatAudioPlayer.isPlaying) {
|
||||||
|
[self.chatAudioPlayer stop];
|
||||||
|
}
|
||||||
|
self.chatAudioPlayer = nil;
|
||||||
|
[self kb_setPanelMode:KBKeyboardPanelModeMain animated:YES];
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - Chat Helpers
|
||||||
|
|
||||||
|
- (void)kb_handleChatSendAction {
|
||||||
|
if (!self.chatPanelVisible) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
[[KBInputBufferManager shared]
|
||||||
|
refreshFromProxyIfPossible:self.textDocumentProxy];
|
||||||
|
NSString *fullText = [KBInputBufferManager shared].liveText ?: @"";
|
||||||
|
|
||||||
|
// 去掉打开聊天面板前宿主输入框里已有的基线文本,只取新增部分
|
||||||
|
NSString *baseline = self.chatPanelBaselineText ?: @"";
|
||||||
|
NSString *rawText = fullText;
|
||||||
|
if (baseline.length > 0 && [fullText hasPrefix:baseline]) {
|
||||||
|
rawText = [fullText substringFromIndex:baseline.length];
|
||||||
|
}
|
||||||
|
|
||||||
|
NSString *trim =
|
||||||
|
[rawText stringByTrimmingCharactersInSet:
|
||||||
|
[NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
||||||
|
NSString *textToClear = rawText;
|
||||||
|
if (trim.length == 0) {
|
||||||
|
// 兼容「先输入再打开聊天面板」场景:
|
||||||
|
// 此时新增文本为空,但当前输入框已有可发送内容,应该允许直接发送。
|
||||||
|
NSString *fullTrim =
|
||||||
|
[fullText stringByTrimmingCharactersInSet:
|
||||||
|
[NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
||||||
|
if (fullTrim.length > 0) {
|
||||||
|
trim = fullTrim;
|
||||||
|
textToClear = fullText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (trim.length == 0) {
|
||||||
|
[KBHUD showInfo:KBLocalized(@"请输入内容")];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
[self kb_sendChatText:trim];
|
||||||
|
// 默认只清新增文本;若命中兜底则清当前全文,避免“已发送但输入框残留”。
|
||||||
|
[self kb_clearHostInputForText:textToClear];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)kb_sendChatText:(NSString *)text {
|
||||||
|
if (text.length == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
NSLog(@"[KB] 发送消息: %@", text);
|
||||||
|
|
||||||
|
KBChatMessage *outgoing = [KBChatMessage userMessageWithText:text];
|
||||||
|
outgoing.avatarURL = [self kb_sharedUserAvatarURL];
|
||||||
|
[self.chatPanelView kb_addUserMessage:text];
|
||||||
|
[self kb_prefetchAvatarForMessage:outgoing];
|
||||||
|
|
||||||
|
if (![[KBFullAccessManager shared] ensureFullAccessOrGuideInView:self.view]) {
|
||||||
|
[KBHUD showInfo:KBLocalized(@"请开启完全访问后使用")];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加 loading 消息
|
||||||
|
[self.chatPanelView kb_addLoadingAssistantMessage];
|
||||||
|
|
||||||
|
// 调用新的聊天接口
|
||||||
|
[self kb_requestChatMessageWithContent:text];
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - Chat Limit Pop
|
||||||
|
|
||||||
|
- (void)kb_showChatLimitPopWithMessage:(NSString *)message {
|
||||||
|
[self kb_dismissChatLimitPop];
|
||||||
|
|
||||||
|
UIControl *mask = [[UIControl alloc] init];
|
||||||
|
mask.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.4];
|
||||||
|
mask.alpha = 0.0;
|
||||||
|
[mask addTarget:self
|
||||||
|
action:@selector(kb_dismissChatLimitPop)
|
||||||
|
forControlEvents:UIControlEventTouchUpInside];
|
||||||
|
[self.contentView addSubview:mask];
|
||||||
|
[mask mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
|
make.edges.equalTo(self.contentView);
|
||||||
|
}];
|
||||||
|
|
||||||
|
CGFloat width = 252.0;
|
||||||
|
CGFloat height = 252.0 + 18.0 + 53.0 + 18.0 + 28.0;
|
||||||
|
KBChatLimitPopView *content =
|
||||||
|
[[KBChatLimitPopView alloc] initWithFrame:CGRectMake(0, 0, width, height)];
|
||||||
|
content.message = message ?: @"";
|
||||||
|
content.delegate = self;
|
||||||
|
[mask addSubview:content];
|
||||||
|
[content mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
|
make.center.equalTo(mask);
|
||||||
|
make.width.mas_equalTo(width);
|
||||||
|
make.height.mas_equalTo(height);
|
||||||
|
}];
|
||||||
|
|
||||||
|
self.chatLimitMaskView = mask;
|
||||||
|
[self.contentView bringSubviewToFront:mask];
|
||||||
|
[UIView animateWithDuration:0.18
|
||||||
|
animations:^{
|
||||||
|
mask.alpha = 1.0;
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)kb_dismissChatLimitPop {
|
||||||
|
if (!self.chatLimitMaskView) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
UIControl *mask = self.chatLimitMaskView;
|
||||||
|
self.chatLimitMaskView = nil;
|
||||||
|
[UIView animateWithDuration:0.15
|
||||||
|
animations:^{
|
||||||
|
mask.alpha = 0.0;
|
||||||
|
}
|
||||||
|
completion:^(__unused BOOL finished) {
|
||||||
|
[mask removeFromSuperview];
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)kb_clearHostInputForText:(NSString *)text {
|
||||||
|
if (text.length == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
NSUInteger count = [self kb_composedCharacterCountForString:text];
|
||||||
|
for (NSUInteger i = 0; i < count; i++) {
|
||||||
|
[self.textDocumentProxy deleteBackward];
|
||||||
|
}
|
||||||
|
[[KBInputBufferManager shared] clearAllLiveText];
|
||||||
|
[self kb_clearCurrentWord];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSUInteger)kb_composedCharacterCountForString:(NSString *)text {
|
||||||
|
if (text.length == 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
__block NSUInteger count = 0;
|
||||||
|
[text enumerateSubstringsInRange:NSMakeRange(0, text.length)
|
||||||
|
options:NSStringEnumerationByComposedCharacterSequences
|
||||||
|
usingBlock:^(__unused NSString *substring,
|
||||||
|
__unused NSRange substringRange,
|
||||||
|
__unused NSRange enclosingRange,
|
||||||
|
__unused BOOL *stop) {
|
||||||
|
count += 1;
|
||||||
|
}];
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSString *)kb_sharedUserAvatarURL {
|
||||||
|
NSUserDefaults *ud = [[NSUserDefaults alloc] initWithSuiteName:AppGroup];
|
||||||
|
NSString *url = [ud stringForKey:AppGroup_UserAvatarURL];
|
||||||
|
return url ?: @"";
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)kb_prefetchAvatarForMessage:(KBChatMessage *)message {
|
||||||
|
if (!message || message.avatarImage) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
NSString *urlString = message.avatarURL ?: @"";
|
||||||
|
if (urlString.length == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (![[KBFullAccessManager shared] hasFullAccess]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
__weak typeof(self) weakSelf = self;
|
||||||
|
[[KBVM shared] downloadAvatarFromURL:urlString
|
||||||
|
completion:^(UIImage *image, NSError *error) {
|
||||||
|
__strong typeof(weakSelf) self = weakSelf;
|
||||||
|
if (!self || !image)
|
||||||
|
return;
|
||||||
|
|
||||||
|
message.avatarImage = image;
|
||||||
|
[self kb_reloadChatRowForMessage:message];
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)kb_reloadChatRowForMessage:(KBChatMessage *)message {
|
||||||
|
// 头像预加载完成后不需要刷新表格
|
||||||
|
// 因为键盘扩展的聊天面板不显示头像,所以这里直接返回
|
||||||
|
// 如果将来需要显示头像,可以只刷新特定行而不是整个表格
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)kb_requestChatAudioForText:(NSString *)text {
|
||||||
|
NSString *mockPath = [self kb_mockChatAudioPath];
|
||||||
|
if (mockPath.length > 0) {
|
||||||
|
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.35 * NSEC_PER_SEC)),
|
||||||
|
dispatch_get_main_queue(), ^{
|
||||||
|
NSString *displayText = KBLocalized(@"语音回复");
|
||||||
|
KBChatMessage *incoming =
|
||||||
|
[KBChatMessage messageWithText:displayText
|
||||||
|
outgoing:NO
|
||||||
|
audioFilePath:mockPath];
|
||||||
|
incoming.displayName = KBLocalized(@"AI助手");
|
||||||
|
[self kb_appendChatMessage:incoming];
|
||||||
|
[self kb_playChatAudioAtPath:mockPath];
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
NSDictionary *payload = @{@"message" : text ?: @""};
|
||||||
|
__weak typeof(self) weakSelf = self;
|
||||||
|
[[KBNetworkManager shared] POST:API_AI_TALK
|
||||||
|
jsonBody:payload
|
||||||
|
headers:nil
|
||||||
|
completion:^(NSDictionary *json, NSURLResponse *response,
|
||||||
|
NSError *error) {
|
||||||
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
|
__strong typeof(weakSelf) self = weakSelf;
|
||||||
|
if (!self) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (error) {
|
||||||
|
NSString *tip = error.localizedDescription
|
||||||
|
?: KBLocalized(@"请求失败");
|
||||||
|
[KBHUD showInfo:tip];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
NSString *displayText =
|
||||||
|
[self kb_chatTextFromJSON:json];
|
||||||
|
NSString *audioURL =
|
||||||
|
[self kb_chatAudioURLFromJSON:json];
|
||||||
|
NSString *audioBase64 =
|
||||||
|
[self kb_chatAudioBase64FromJSON:json];
|
||||||
|
if (audioURL.length > 0) {
|
||||||
|
[self kb_downloadChatAudioFromURL:audioURL
|
||||||
|
displayText:displayText];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (audioBase64.length > 0) {
|
||||||
|
NSData *data = [[NSData alloc]
|
||||||
|
initWithBase64EncodedString:audioBase64
|
||||||
|
options:0];
|
||||||
|
if (data.length == 0) {
|
||||||
|
[KBHUD showInfo:KBLocalized(@"音频数据解析失败")];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
[self kb_handleChatAudioData:data
|
||||||
|
fileExtension:@"m4a"
|
||||||
|
displayText:displayText];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
[KBHUD showInfo:KBLocalized(@"未获取到音频文件")];
|
||||||
|
});
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - New Chat API (with typewriter effect and audio preload)
|
||||||
|
|
||||||
|
/// 调用新的聊天接口(返回文本和 audioId)
|
||||||
|
- (void)kb_requestChatMessageWithContent:(NSString *)content {
|
||||||
|
if (content.length == 0) {
|
||||||
|
[self.chatPanelView kb_removeLoadingAssistantMessage];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSInteger companionId = [[KBVM shared] selectedCompanionIdFromAppGroup];
|
||||||
|
NSLog(@"[KB] 请求聊天: companionId=%ld", (long)companionId);
|
||||||
|
|
||||||
|
__weak typeof(self) weakSelf = self;
|
||||||
|
[[KBVM shared] sendChatMessageWithContent:content
|
||||||
|
companionId:companionId
|
||||||
|
completion:^(KBChatResponse *response) {
|
||||||
|
__strong typeof(weakSelf) self = weakSelf;
|
||||||
|
if (!self)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (response.code != 0) {
|
||||||
|
if (response.code == 50030) {
|
||||||
|
NSLog(@"[KB] ⚠️ 次数用尽: %@",
|
||||||
|
response.message);
|
||||||
|
[self.chatPanelView
|
||||||
|
kb_removeLoadingAssistantMessage];
|
||||||
|
[self kb_showChatLimitPopWithMessage:
|
||||||
|
response.message];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
NSLog(@"[KB] ❌ 请求失败: %@",
|
||||||
|
response.message);
|
||||||
|
[self.chatPanelView
|
||||||
|
kb_removeLoadingAssistantMessage];
|
||||||
|
[KBHUD showInfo:response.message
|
||||||
|
?: KBLocalized(@"请求失败")];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSLog(@"[KB] ✅ 收到回复: %@",
|
||||||
|
response.data.aiResponse);
|
||||||
|
|
||||||
|
if (response.data.aiResponse.length == 0) {
|
||||||
|
[self.chatPanelView
|
||||||
|
kb_removeLoadingAssistantMessage];
|
||||||
|
[KBHUD showInfo:KBLocalized(@"未获取到回复内容")];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加 AI 消息(带打字机效果)
|
||||||
|
NSLog(@"[KB] 准备添加 AI 消息");
|
||||||
|
[self.chatPanelView
|
||||||
|
kb_addAssistantMessage:response.data.aiResponse
|
||||||
|
audioId:response.data.audioId];
|
||||||
|
NSLog(@"[KB] AI 消息添加完成");
|
||||||
|
|
||||||
|
// 通知主 App 刷新对应 persona 的聊天记录
|
||||||
|
[self kb_notifyMainAppChatUpdatedWithCompanionId:companionId];
|
||||||
|
|
||||||
|
// 如果有 audioId,开始预加载音频
|
||||||
|
if (response.data.audioId.length > 0) {
|
||||||
|
[self kb_preloadAudioWithAudioId:
|
||||||
|
response.data.audioId];
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 从 AppGroup 获取选中的 persona companionId
|
||||||
|
- (NSInteger)kb_selectedCompanionId {
|
||||||
|
return [[KBVM shared] selectedCompanionIdFromAppGroup];
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - Audio Preload
|
||||||
|
|
||||||
|
/// 预加载音频(轮询获取 audioURL)
|
||||||
|
- (void)kb_preloadAudioWithAudioId:(NSString *)audioId {
|
||||||
|
if (audioId.length == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
NSLog(@"[Keyboard] 开始预加载音频,audioId: %@", audioId);
|
||||||
|
|
||||||
|
__weak typeof(self) weakSelf = self;
|
||||||
|
[[KBVM shared] pollAudioURLWithAudioId:audioId
|
||||||
|
maxRetries:10
|
||||||
|
interval:1.0
|
||||||
|
completion:^(KBAudioResponse *response) {
|
||||||
|
__strong typeof(weakSelf) self = weakSelf;
|
||||||
|
if (!self)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!response.success ||
|
||||||
|
response.audioURL.length == 0) {
|
||||||
|
NSLog(@"[Keyboard] ❌ 预加载音频 URL 获取失败: %@",
|
||||||
|
response.errorMessage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSLog(@"[Keyboard] ✅ 预加载音频 URL 获取成功");
|
||||||
|
|
||||||
|
// 下载音频
|
||||||
|
[[KBVM shared]
|
||||||
|
downloadAudioFromURL:response.audioURL
|
||||||
|
completion:^(
|
||||||
|
KBAudioResponse *audioResponse) {
|
||||||
|
if (!audioResponse.success) {
|
||||||
|
NSLog(@"[Keyboard] ❌ 预加载音频下载失败: %@",
|
||||||
|
audioResponse.errorMessage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新最后一条 AI 消息的音频数据
|
||||||
|
[self.chatPanelView
|
||||||
|
kb_updateLastAssistantMessageWithAudioData:
|
||||||
|
audioResponse.audioData
|
||||||
|
duration:
|
||||||
|
audioResponse.duration];
|
||||||
|
NSLog(@"[Keyboard] ✅ 预加载音频完成,音频时长: %.2f秒",
|
||||||
|
audioResponse.duration);
|
||||||
|
}];
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)kb_downloadChatAudioFromURL:(NSString *)audioURL
|
||||||
|
displayText:(NSString *)displayText {
|
||||||
|
__weak typeof(self) weakSelf = self;
|
||||||
|
[[KBVM shared] downloadAudioFromURL:audioURL
|
||||||
|
completion:^(KBAudioResponse *response) {
|
||||||
|
__strong typeof(weakSelf) self = weakSelf;
|
||||||
|
if (!self)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!response.success) {
|
||||||
|
[KBHUD showInfo:response.errorMessage
|
||||||
|
?: KBLocalized(@"下载失败")];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.audioData ||
|
||||||
|
response.audioData.length == 0) {
|
||||||
|
[KBHUD showInfo:KBLocalized(@"未获取到音频数据")];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSString *ext = @"m4a";
|
||||||
|
NSURL *url = [NSURL URLWithString:audioURL];
|
||||||
|
if (url.pathExtension.length > 0) {
|
||||||
|
ext = url.pathExtension;
|
||||||
|
}
|
||||||
|
[self kb_handleChatAudioData:response.audioData
|
||||||
|
fileExtension:ext
|
||||||
|
displayText:displayText];
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)kb_handleChatAudioData:(NSData *)data
|
||||||
|
fileExtension:(NSString *)extension
|
||||||
|
displayText:(NSString *)displayText {
|
||||||
|
if (data.length == 0) {
|
||||||
|
[KBHUD showInfo:KBLocalized(@"音频数据为空")];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
NSString *ext = extension.length > 0 ? extension : @"m4a";
|
||||||
|
NSString *fileName = [NSString
|
||||||
|
stringWithFormat:@"kb_chat_%@.%@",
|
||||||
|
@((long long)([NSDate date].timeIntervalSince1970 *
|
||||||
|
1000)),
|
||||||
|
ext];
|
||||||
|
NSString *filePath =
|
||||||
|
[NSTemporaryDirectory() stringByAppendingPathComponent:fileName];
|
||||||
|
if (![data writeToFile:filePath atomically:YES]) {
|
||||||
|
[KBHUD showInfo:KBLocalized(@"音频保存失败")];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
NSString *text =
|
||||||
|
displayText.length > 0 ? displayText : KBLocalized(@"语音消息");
|
||||||
|
KBChatMessage *incoming =
|
||||||
|
[KBChatMessage messageWithText:text outgoing:NO audioFilePath:filePath];
|
||||||
|
incoming.displayName = KBLocalized(@"AI助手");
|
||||||
|
[self kb_appendChatMessage:incoming];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)kb_appendChatMessage:(KBChatMessage *)message {
|
||||||
|
if (!message) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
[self.chatMessages addObject:message];
|
||||||
|
if (self.chatMessages.count > kKBChatMessageLimit) {
|
||||||
|
NSUInteger overflow = self.chatMessages.count - kKBChatMessageLimit;
|
||||||
|
NSArray<KBChatMessage *> *removed =
|
||||||
|
[self.chatMessages subarrayWithRange:NSMakeRange(0, overflow)];
|
||||||
|
[self.chatMessages removeObjectsInRange:NSMakeRange(0, overflow)];
|
||||||
|
for (KBChatMessage *msg in removed) {
|
||||||
|
if (msg.audioFilePath.length > 0) {
|
||||||
|
NSString *tmpRoot = NSTemporaryDirectory();
|
||||||
|
if (tmpRoot.length > 0 && [msg.audioFilePath hasPrefix:tmpRoot]) {
|
||||||
|
[[NSFileManager defaultManager] removeItemAtPath:msg.audioFilePath
|
||||||
|
error:nil];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
[self.chatPanelView kb_reloadWithMessages:self.chatMessages];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSString *)kb_mockChatAudioPath {
|
||||||
|
NSString *path = [[NSBundle mainBundle] pathForResource:@"ai_test"
|
||||||
|
ofType:@"m4a"];
|
||||||
|
return path ?: @"";
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSString *)kb_chatTextFromJSON:(NSDictionary *)json {
|
||||||
|
NSDictionary *data = [self kb_chatDataDictionaryFromJSON:json];
|
||||||
|
NSString *text =
|
||||||
|
[self kb_stringValueInDict:data keys:@[ @"text", @"message", @"content" ]];
|
||||||
|
if (text.length == 0) {
|
||||||
|
text = [self kb_stringValueInDict:json
|
||||||
|
keys:@[ @"text", @"message", @"content" ]];
|
||||||
|
}
|
||||||
|
return text ?: @"";
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSString *)kb_chatAudioURLFromJSON:(NSDictionary *)json {
|
||||||
|
NSDictionary *data = [self kb_chatDataDictionaryFromJSON:json];
|
||||||
|
NSArray<NSString *> *keys =
|
||||||
|
@[ @"audioUrl", @"audioURL", @"audio_url", @"url", @"fileUrl",
|
||||||
|
@"file_url", @"audioFileUrl", @"audio_file_url" ];
|
||||||
|
NSString *url = [self kb_stringValueInDict:data keys:keys];
|
||||||
|
if (url.length == 0) {
|
||||||
|
url = [self kb_stringValueInDict:json keys:keys];
|
||||||
|
}
|
||||||
|
return url ?: @"";
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSString *)kb_chatAudioBase64FromJSON:(NSDictionary *)json {
|
||||||
|
NSDictionary *data = [self kb_chatDataDictionaryFromJSON:json];
|
||||||
|
NSArray<NSString *> *keys =
|
||||||
|
@[ @"audioBase64", @"audio_base64", @"audioData", @"audio_data",
|
||||||
|
@"base64" ];
|
||||||
|
NSString *b64 = [self kb_stringValueInDict:data keys:keys];
|
||||||
|
if (b64.length == 0) {
|
||||||
|
b64 = [self kb_stringValueInDict:json keys:keys];
|
||||||
|
}
|
||||||
|
return b64 ?: @"";
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSDictionary *)kb_chatDataDictionaryFromJSON:(NSDictionary *)json {
|
||||||
|
if (![json isKindOfClass:[NSDictionary class]]) {
|
||||||
|
return @{};
|
||||||
|
}
|
||||||
|
id dataObj = json[@"data"] ?: json[@"result"] ?: json[@"response"];
|
||||||
|
if ([dataObj isKindOfClass:[NSDictionary class]]) {
|
||||||
|
return (NSDictionary *)dataObj;
|
||||||
|
}
|
||||||
|
return @{};
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSString *)kb_stringValueInDict:(NSDictionary *)dict
|
||||||
|
keys:(NSArray<NSString *> *)keys {
|
||||||
|
if (![dict isKindOfClass:[NSDictionary class]]) {
|
||||||
|
return @"";
|
||||||
|
}
|
||||||
|
for (NSString *key in keys) {
|
||||||
|
id value = dict[key];
|
||||||
|
if ([value isKindOfClass:[NSString class]] &&
|
||||||
|
((NSString *)value).length > 0) {
|
||||||
|
return (NSString *)value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return @"";
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)kb_playChatAudioAtPath:(NSString *)path {
|
||||||
|
if (path.length == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
NSURL *url = [NSURL fileURLWithPath:path];
|
||||||
|
if (![NSFileManager.defaultManager fileExistsAtPath:path]) {
|
||||||
|
[KBHUD showInfo:KBLocalized(@"音频文件不存在")];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.chatAudioPlayer && self.chatAudioPlayer.isPlaying) {
|
||||||
|
NSURL *currentURL = self.chatAudioPlayer.url;
|
||||||
|
if ([currentURL isEqual:url]) {
|
||||||
|
[self.chatAudioPlayer stop];
|
||||||
|
self.chatAudioPlayer = nil;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
[self.chatAudioPlayer stop];
|
||||||
|
self.chatAudioPlayer = nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSError *sessionError = nil;
|
||||||
|
AVAudioSession *session = [AVAudioSession sharedInstance];
|
||||||
|
if ([session respondsToSelector:@selector(setCategory:options:error:)]) {
|
||||||
|
[session setCategory:AVAudioSessionCategoryPlayback
|
||||||
|
withOptions:AVAudioSessionCategoryOptionDuckOthers
|
||||||
|
error:&sessionError];
|
||||||
|
} else {
|
||||||
|
[session setCategory:AVAudioSessionCategoryPlayback error:&sessionError];
|
||||||
|
}
|
||||||
|
[session setActive:YES error:nil];
|
||||||
|
|
||||||
|
NSError *playerError = nil;
|
||||||
|
AVAudioPlayer *player =
|
||||||
|
[[AVAudioPlayer alloc] initWithContentsOfURL:url error:&playerError];
|
||||||
|
if (playerError || !player) {
|
||||||
|
[KBHUD showInfo:KBLocalized(@"音频播放失败")];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.chatAudioPlayer = player;
|
||||||
|
[player prepareToPlay];
|
||||||
|
[player play];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 播放音频数据
|
||||||
|
- (void)kb_playChatAudioData:(NSData *)audioData {
|
||||||
|
if (!audioData || audioData.length == 0) {
|
||||||
|
NSLog(@"[Keyboard] 音频数据为空");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果正在播放,先停止
|
||||||
|
if (self.chatAudioPlayer && self.chatAudioPlayer.isPlaying) {
|
||||||
|
[self.chatAudioPlayer stop];
|
||||||
|
self.chatAudioPlayer = nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 配置音频会话
|
||||||
|
NSError *sessionError = nil;
|
||||||
|
AVAudioSession *session = [AVAudioSession sharedInstance];
|
||||||
|
if ([session respondsToSelector:@selector(setCategory:options:error:)]) {
|
||||||
|
[session setCategory:AVAudioSessionCategoryPlayback
|
||||||
|
withOptions:AVAudioSessionCategoryOptionDuckOthers
|
||||||
|
error:&sessionError];
|
||||||
|
} else {
|
||||||
|
[session setCategory:AVAudioSessionCategoryPlayback error:&sessionError];
|
||||||
|
}
|
||||||
|
[session setActive:YES error:nil];
|
||||||
|
|
||||||
|
// 创建播放器
|
||||||
|
NSError *playerError = nil;
|
||||||
|
AVAudioPlayer *player =
|
||||||
|
[[AVAudioPlayer alloc] initWithData:audioData error:&playerError];
|
||||||
|
if (playerError || !player) {
|
||||||
|
NSLog(@"[Keyboard] 音频播放器初始化失败: %@",
|
||||||
|
playerError.localizedDescription);
|
||||||
|
[KBHUD showInfo:KBLocalized(@"音频播放失败")];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.chatAudioPlayer = player;
|
||||||
|
player.volume = 1.0;
|
||||||
|
[player prepareToPlay];
|
||||||
|
[player play];
|
||||||
|
|
||||||
|
NSLog(@"[Keyboard] 开始播放音频,时长: %.2f秒", player.duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - Notify Main App
|
||||||
|
|
||||||
|
/// 通知主 App 刷新对应 persona 的聊天记录
|
||||||
|
- (void)kb_notifyMainAppChatUpdatedWithCompanionId:(NSInteger)companionId {
|
||||||
|
NSUserDefaults *ud = [[NSUserDefaults alloc] initWithSuiteName:AppGroup];
|
||||||
|
[ud setInteger:companionId forKey:AppGroup_ChatUpdatedCompanionId];
|
||||||
|
[ud synchronize];
|
||||||
|
|
||||||
|
CFNotificationCenterPostNotification(
|
||||||
|
CFNotificationCenterGetDarwinNotifyCenter(),
|
||||||
|
(__bridge CFStringRef)kKBDarwinChatUpdated,
|
||||||
|
NULL, NULL, true);
|
||||||
|
|
||||||
|
NSLog(@"[KB] 已通知主 App 刷新 companionId=%ld 的聊天记录", (long)companionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - KBChatLimitPopViewDelegate
|
||||||
|
|
||||||
|
- (void)chatLimitPopViewDidTapCancel:(KBChatLimitPopView *)view {
|
||||||
|
[self kb_dismissChatLimitPop];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)chatLimitPopViewDidTapRecharge:(KBChatLimitPopView *)view {
|
||||||
|
[self kb_dismissChatLimitPop];
|
||||||
|
NSString *urlString =
|
||||||
|
[NSString stringWithFormat:@"%@://recharge?src=keyboard&vipType=svip",
|
||||||
|
KB_APP_SCHEME];
|
||||||
|
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")];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
//
|
||||||
|
// KeyboardViewController+Layout.m
|
||||||
|
// CustomKeyboard
|
||||||
|
//
|
||||||
|
// Created by Codex on 2026/02/22.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import "KeyboardViewController+Private.h"
|
||||||
|
|
||||||
|
// 以 375 宽设计稿为基准的键盘总高度
|
||||||
|
static const CGFloat kKBKeyboardBaseHeight = 250.0f;
|
||||||
|
static const CGFloat kKBChatPanelHeight = 180;
|
||||||
|
|
||||||
|
@implementation KeyboardViewController (Layout)
|
||||||
|
|
||||||
|
- (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;
|
||||||
|
}
|
||||||
|
CGFloat scale = width / KB_DESIGN_WIDTH;
|
||||||
|
CGFloat baseHeight = kKBKeyboardBaseHeight * scale;
|
||||||
|
CGFloat chatHeight = kKBChatPanelHeight * scale;
|
||||||
|
if (self.chatPanelVisible) {
|
||||||
|
return baseHeight + chatHeight;
|
||||||
|
}
|
||||||
|
return baseHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (CGFloat)kb_keyboardBaseHeightForWidth:(CGFloat)width {
|
||||||
|
if (width <= 0) {
|
||||||
|
width = KB_DESIGN_WIDTH;
|
||||||
|
}
|
||||||
|
CGFloat scale = width / KB_DESIGN_WIDTH;
|
||||||
|
return kKBKeyboardBaseHeight * scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (CGFloat)kb_chatPanelHeightForWidth:(CGFloat)width {
|
||||||
|
if (width <= 0) {
|
||||||
|
width = KB_DESIGN_WIDTH;
|
||||||
|
}
|
||||||
|
CGFloat scale = width / KB_DESIGN_WIDTH;
|
||||||
|
return kKBChatPanelHeight * scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)kb_updateKeyboardLayoutIfNeeded {
|
||||||
|
CGFloat portraitWidth = [self kb_portraitWidth];
|
||||||
|
CGFloat keyboardHeight = [self kb_keyboardHeightForWidth:portraitWidth];
|
||||||
|
CGFloat keyboardBaseHeight =
|
||||||
|
[self kb_keyboardBaseHeightForWidth:portraitWidth];
|
||||||
|
CGFloat chatPanelHeight = [self kb_chatPanelHeightForWidth: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];
|
||||||
|
}
|
||||||
|
if (self.keyBoardMainHeightConstraint) {
|
||||||
|
[self.keyBoardMainHeightConstraint setOffset:keyboardBaseHeight];
|
||||||
|
}
|
||||||
|
if (self.chatPanelHeightConstraint) {
|
||||||
|
[self.chatPanelHeightConstraint setOffset:chatPanelHeight];
|
||||||
|
}
|
||||||
|
[self.view layoutIfNeeded];
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
@@ -0,0 +1,656 @@
|
|||||||
|
//
|
||||||
|
// KeyboardViewController+Panels.m
|
||||||
|
// CustomKeyboard
|
||||||
|
//
|
||||||
|
// Created by Codex on 2026/02/22.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import "KeyboardViewController+Private.h"
|
||||||
|
|
||||||
|
#import "KBAuthManager.h"
|
||||||
|
#import "KBBackspaceUndoManager.h"
|
||||||
|
#import "KBChatMessage.h"
|
||||||
|
#import "KBChatPanelView.h"
|
||||||
|
#import "KBFunctionView.h"
|
||||||
|
#import "KBFullAccessManager.h"
|
||||||
|
#import "KBHostAppLauncher.h"
|
||||||
|
#import "KBInputBufferManager.h"
|
||||||
|
#import "KBKey.h"
|
||||||
|
#import "KBKeyBoardMainView.h"
|
||||||
|
#import "KBKeyboardSubscriptionView.h"
|
||||||
|
#import "KBSettingView.h"
|
||||||
|
#import "Masonry.h"
|
||||||
|
#import <SDWebImage/SDWebImage.h>
|
||||||
|
#import <AVFoundation/AVAudioPlayer.h>
|
||||||
|
|
||||||
|
@implementation KeyboardViewController (Panels)
|
||||||
|
|
||||||
|
#pragma mark - Panel Mode
|
||||||
|
|
||||||
|
- (void)kb_setPanelMode:(KBKeyboardPanelMode)mode animated:(BOOL)animated {
|
||||||
|
if (mode == self.kb_panelMode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
KBKeyboardPanelMode fromMode = self.kb_panelMode;
|
||||||
|
|
||||||
|
// AI 入口先判完全访问:未开启时仅展示引导,不再继续登录态判断。
|
||||||
|
if (mode == KBKeyboardPanelModeFunction &&
|
||||||
|
![[KBFullAccessManager shared] ensureFullAccessOrGuideInView:self.view]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 未登录时,不要提前写入面板状态,避免 mode 被错误卡在 Function 导致后续点击无响应。
|
||||||
|
BOOL islogin = YES;
|
||||||
|
if (mode == KBKeyboardPanelModeFunction) {
|
||||||
|
[[KBAuthManager shared] reloadFromKeychain];
|
||||||
|
islogin = KBAuthManager.shared.isLoggedIn;
|
||||||
|
}
|
||||||
|
#if DEBUG
|
||||||
|
if (mode == KBKeyboardPanelModeFunction) {
|
||||||
|
NSString *token = [KBAuthManager shared].current.accessToken ?: @"";
|
||||||
|
NSLog(@"[AuthTrace][Ext] tapAI mode=%ld isLoggedIn=%d tokenLen=%lu",
|
||||||
|
(long)mode, islogin, (unsigned long)token.length);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
if (mode == KBKeyboardPanelModeFunction && !islogin) {
|
||||||
|
[KBHUD showInfo:KBLocalized(@"请先登录后使用AI功能")];
|
||||||
|
NSString *schemeStr =
|
||||||
|
[NSString stringWithFormat:@"%@://login?src=keyboard", KB_APP_SCHEME];
|
||||||
|
NSURL *scheme = [NSURL URLWithString:schemeStr];
|
||||||
|
BOOL ok = [KBHostAppLauncher openHostAppURL:scheme fromResponder:self.view];
|
||||||
|
if (!ok) {
|
||||||
|
[KBHUD showInfo:KBLocalized(@"请回到桌面手动打开App登录")];
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.kb_panelMode = mode;
|
||||||
|
|
||||||
|
// 主键盘视图是基础承载:确保存在(键盘隐藏后会被释放)
|
||||||
|
[self kb_ensureKeyBoardMainViewIfNeeded];
|
||||||
|
|
||||||
|
// 1) 先收起所有面板(再展开目标面板),避免互相调用导致漏关/层级错乱
|
||||||
|
[self kb_setSubscriptionPanelVisible:NO animated:animated];
|
||||||
|
[self kb_setSettingViewVisible:NO animated:animated];
|
||||||
|
[self kb_setChatPanelVisible:NO animated:animated];
|
||||||
|
[self kb_setFunctionPanelVisible:NO];
|
||||||
|
|
||||||
|
// 2) 再展开目标面板
|
||||||
|
switch (mode) {
|
||||||
|
case KBKeyboardPanelModeFunction:
|
||||||
|
[self kb_setFunctionPanelVisible:YES];
|
||||||
|
break;
|
||||||
|
case KBKeyboardPanelModeChat:
|
||||||
|
[self kb_setChatPanelVisible:YES animated:animated];
|
||||||
|
break;
|
||||||
|
case KBKeyboardPanelModeSettings:
|
||||||
|
[self kb_setSettingViewVisible:YES animated:animated];
|
||||||
|
break;
|
||||||
|
case KBKeyboardPanelModeSubscription:
|
||||||
|
[self kb_setSubscriptionPanelVisible:YES animated:animated];
|
||||||
|
break;
|
||||||
|
case KBKeyboardPanelModeMain:
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) 事件埋点:保持原逻辑(仅功能面板/主面板会互相曝光)
|
||||||
|
if (mode == KBKeyboardPanelModeFunction) {
|
||||||
|
[[KBMaiPointReporter sharedReporter]
|
||||||
|
reportPageExposureWithEventName:@"enter_keyboard_function_panel"
|
||||||
|
pageId:@"keyboard_function_panel"
|
||||||
|
extra:nil
|
||||||
|
completion:nil];
|
||||||
|
} else if (mode == KBKeyboardPanelModeMain &&
|
||||||
|
fromMode == KBKeyboardPanelModeFunction) {
|
||||||
|
[[KBMaiPointReporter sharedReporter]
|
||||||
|
reportPageExposureWithEventName:@"enter_keyboard_main_panel"
|
||||||
|
pageId:@"keyboard_main_panel"
|
||||||
|
extra:nil
|
||||||
|
completion:nil];
|
||||||
|
} else if (mode == KBKeyboardPanelModeSettings) {
|
||||||
|
[[KBMaiPointReporter sharedReporter]
|
||||||
|
reportPageExposureWithEventName:@"enter_keyboard_settings"
|
||||||
|
pageId:@"keyboard_settings"
|
||||||
|
extra:nil
|
||||||
|
completion:nil];
|
||||||
|
} else if (mode == KBKeyboardPanelModeSubscription) {
|
||||||
|
[[KBMaiPointReporter sharedReporter]
|
||||||
|
reportPageExposureWithEventName:@"enter_keyboard_subscription_panel"
|
||||||
|
pageId:@"keyboard_subscription_panel"
|
||||||
|
extra:nil
|
||||||
|
completion:nil];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4) 层级:保证当前面板在最上层
|
||||||
|
if (mode == KBKeyboardPanelModeSubscription) {
|
||||||
|
[self.contentView bringSubviewToFront:self.subscriptionView];
|
||||||
|
} else if (mode == KBKeyboardPanelModeSettings) {
|
||||||
|
[self.contentView bringSubviewToFront:self.settingView];
|
||||||
|
} else if (mode == KBKeyboardPanelModeChat) {
|
||||||
|
[self.contentView bringSubviewToFront:self.chatPanelView];
|
||||||
|
} else if (mode == KBKeyboardPanelModeFunction) {
|
||||||
|
[self.contentView bringSubviewToFront:self.functionView];
|
||||||
|
} else {
|
||||||
|
[self.contentView bringSubviewToFront:self.keyBoardMainView];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 对外兼容:切换显示功能面板/键盘主视图
|
||||||
|
- (void)showFunctionPanel:(BOOL)show {
|
||||||
|
if (show) {
|
||||||
|
[self kb_setPanelMode:KBKeyboardPanelModeFunction animated:NO];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (self.kb_panelMode == KBKeyboardPanelModeFunction) {
|
||||||
|
[self kb_setPanelMode:KBKeyboardPanelModeMain animated:NO];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 对外兼容:显示/隐藏设置页(高度与 keyBoardMainView 一致),右侧滑入/滑出
|
||||||
|
- (void)showSettingView:(BOOL)show {
|
||||||
|
if (show) {
|
||||||
|
[self kb_setPanelMode:KBKeyboardPanelModeSettings animated:YES];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (self.kb_panelMode == KBKeyboardPanelModeSettings) {
|
||||||
|
[self kb_setPanelMode:KBKeyboardPanelModeMain animated:YES];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 对外兼容:显示/隐藏聊天面板(覆盖整个键盘区域)
|
||||||
|
- (void)showChatPanel:(BOOL)show {
|
||||||
|
if (show) {
|
||||||
|
[self kb_setPanelMode:KBKeyboardPanelModeChat animated:YES];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (self.kb_panelMode == KBKeyboardPanelModeChat) {
|
||||||
|
[self kb_setPanelMode:KBKeyboardPanelModeMain animated:YES];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)kb_setFunctionPanelVisible:(BOOL)visible {
|
||||||
|
if (visible) {
|
||||||
|
[self kb_ensureFunctionViewIfNeeded];
|
||||||
|
}
|
||||||
|
if (_functionView) {
|
||||||
|
_functionView.hidden = !visible;
|
||||||
|
} else if (visible) {
|
||||||
|
// ensure 后按理已存在;这里兜底一次,避免异常情况下状态不一致
|
||||||
|
self.functionView.hidden = NO;
|
||||||
|
}
|
||||||
|
self.keyBoardMainView.hidden = visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)kb_setChatPanelVisible:(BOOL)visible animated:(BOOL)animated {
|
||||||
|
if (visible == self.chatPanelVisible) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.chatPanelVisible = visible;
|
||||||
|
|
||||||
|
if (visible) {
|
||||||
|
// 记录打开聊天面板时宿主输入框已有的文本,发送时只取新增部分
|
||||||
|
[[KBInputBufferManager shared]
|
||||||
|
refreshFromProxyIfPossible:self.textDocumentProxy];
|
||||||
|
self.chatPanelBaselineText = [KBInputBufferManager shared].liveText ?: @"";
|
||||||
|
[self kb_ensureChatPanelViewIfNeeded];
|
||||||
|
self.chatPanelView.hidden = NO;
|
||||||
|
self.chatPanelView.alpha = 0.0;
|
||||||
|
if (animated) {
|
||||||
|
[UIView animateWithDuration:0.2
|
||||||
|
delay:0
|
||||||
|
options:UIViewAnimationOptionCurveEaseOut
|
||||||
|
animations:^{
|
||||||
|
self.chatPanelView.alpha = 1.0;
|
||||||
|
}
|
||||||
|
completion:nil];
|
||||||
|
} else {
|
||||||
|
self.chatPanelView.alpha = 1.0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 从未创建过聊天面板时,直接返回,避免 show/hide 触发额外内存分配
|
||||||
|
if (!_chatPanelView) {
|
||||||
|
[self kb_updateKeyboardLayoutIfNeeded];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (animated) {
|
||||||
|
[UIView animateWithDuration:0.18
|
||||||
|
delay:0
|
||||||
|
options:UIViewAnimationOptionCurveEaseIn
|
||||||
|
animations:^{
|
||||||
|
self.chatPanelView.alpha = 0.0;
|
||||||
|
}
|
||||||
|
completion:^(BOOL finished) {
|
||||||
|
self.chatPanelView.hidden = YES;
|
||||||
|
}];
|
||||||
|
} else {
|
||||||
|
self.chatPanelView.alpha = 0.0;
|
||||||
|
self.chatPanelView.hidden = YES;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
[self kb_updateKeyboardLayoutIfNeeded];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)kb_setSettingViewVisible:(BOOL)visible animated:(BOOL)animated {
|
||||||
|
if (visible) {
|
||||||
|
KBSettingView *settingView = self.settingView;
|
||||||
|
if (!settingView.superview) {
|
||||||
|
settingView.hidden = YES;
|
||||||
|
[self.contentView addSubview:settingView];
|
||||||
|
[settingView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
|
make.edges.equalTo(self.contentView);
|
||||||
|
}];
|
||||||
|
[settingView.backButton addTarget:self
|
||||||
|
action:@selector(onTapSettingsBack)
|
||||||
|
forControlEvents:UIControlEventTouchUpInside];
|
||||||
|
}
|
||||||
|
[self.contentView bringSubviewToFront: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];
|
||||||
|
}
|
||||||
|
settingView.transform = CGAffineTransformMakeTranslation(w, 0);
|
||||||
|
settingView.hidden = NO;
|
||||||
|
if (animated) {
|
||||||
|
[UIView animateWithDuration:0.25
|
||||||
|
delay:0
|
||||||
|
options:UIViewAnimationOptionCurveEaseOut
|
||||||
|
animations:^{
|
||||||
|
settingView.transform = CGAffineTransformIdentity;
|
||||||
|
}
|
||||||
|
completion:nil];
|
||||||
|
} else {
|
||||||
|
settingView.transform = CGAffineTransformIdentity;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
KBSettingView *settingView = _settingView;
|
||||||
|
if (!settingView) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!settingView.superview || settingView.hidden) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
CGFloat w = CGRectGetWidth(self.keyBoardMainView.bounds);
|
||||||
|
if (w <= 0) {
|
||||||
|
w = CGRectGetWidth(self.contentView.bounds);
|
||||||
|
}
|
||||||
|
if (w <= 0) {
|
||||||
|
w = [self kb_portraitWidth];
|
||||||
|
}
|
||||||
|
if (animated) {
|
||||||
|
[UIView animateWithDuration:0.22
|
||||||
|
delay:0
|
||||||
|
options:UIViewAnimationOptionCurveEaseIn
|
||||||
|
animations:^{
|
||||||
|
settingView.transform = CGAffineTransformMakeTranslation(w, 0);
|
||||||
|
}
|
||||||
|
completion:^(BOOL finished) {
|
||||||
|
settingView.hidden = YES;
|
||||||
|
}];
|
||||||
|
} else {
|
||||||
|
settingView.transform = CGAffineTransformMakeTranslation(w, 0);
|
||||||
|
settingView.hidden = YES;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)kb_setSubscriptionPanelVisible:(BOOL)visible animated:(BOOL)animated {
|
||||||
|
if (visible) {
|
||||||
|
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];
|
||||||
|
if (animated) {
|
||||||
|
[UIView animateWithDuration:0.25
|
||||||
|
delay:0
|
||||||
|
options:UIViewAnimationOptionCurveEaseOut
|
||||||
|
animations:^{
|
||||||
|
panel.alpha = 1.0;
|
||||||
|
panel.transform = CGAffineTransformIdentity;
|
||||||
|
}
|
||||||
|
completion:nil];
|
||||||
|
} else {
|
||||||
|
panel.alpha = 1.0;
|
||||||
|
panel.transform = CGAffineTransformIdentity;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
KBKeyboardSubscriptionView *panel = _subscriptionView;
|
||||||
|
if (!panel) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!panel.superview || panel.hidden) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
CGFloat height = CGRectGetHeight(panel.bounds);
|
||||||
|
if (height <= 0) {
|
||||||
|
height = CGRectGetHeight(self.contentView.bounds);
|
||||||
|
}
|
||||||
|
if (animated) {
|
||||||
|
[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;
|
||||||
|
}];
|
||||||
|
} else {
|
||||||
|
panel.hidden = YES;
|
||||||
|
panel.alpha = 1.0;
|
||||||
|
panel.transform = CGAffineTransformIdentity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 延迟创建:仅在用户真正打开功能面板时才创建/布局,降低默认内存占用。
|
||||||
|
- (void)kb_ensureFunctionViewIfNeeded {
|
||||||
|
if (_functionView && _functionView.superview) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
KBFunctionView *v = self.functionView;
|
||||||
|
if (!v.superview) {
|
||||||
|
v.hidden = YES;
|
||||||
|
[self.contentView addSubview:v];
|
||||||
|
[v mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
|
make.edges.equalTo(self.contentView);
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 延迟创建:仅在用户打开聊天面板时才创建/布局。
|
||||||
|
- (void)kb_ensureChatPanelViewIfNeeded {
|
||||||
|
if (_chatPanelView && _chatPanelView.superview) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
CGFloat portraitWidth = [self kb_portraitWidth];
|
||||||
|
CGFloat chatPanelHeight = [self kb_chatPanelHeightForWidth:portraitWidth];
|
||||||
|
KBChatPanelView *v = self.chatPanelView;
|
||||||
|
if (!v.superview) {
|
||||||
|
[self.contentView addSubview:v];
|
||||||
|
[v mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
|
make.left.right.equalTo(self.contentView);
|
||||||
|
make.bottom.equalTo(self.keyBoardMainView.mas_top);
|
||||||
|
self.chatPanelHeightConstraint =
|
||||||
|
make.height.mas_equalTo(chatPanelHeight);
|
||||||
|
}];
|
||||||
|
v.hidden = YES;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 延迟创建:键盘主面板(按键区)在隐藏时会被释放;再次显示时需要重建。
|
||||||
|
- (void)kb_ensureKeyBoardMainViewIfNeeded {
|
||||||
|
if (_keyBoardMainView && _keyBoardMainView.superview) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
CGFloat portraitWidth = [self kb_portraitWidth];
|
||||||
|
CGFloat keyboardBaseHeight =
|
||||||
|
[self kb_keyboardBaseHeightForWidth:portraitWidth];
|
||||||
|
KBKeyBoardMainView *v = self.keyBoardMainView;
|
||||||
|
if (!v.superview) {
|
||||||
|
[self.contentView addSubview:v];
|
||||||
|
[v mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
|
make.left.right.equalTo(self.contentView);
|
||||||
|
make.bottom.equalTo(self.contentView);
|
||||||
|
self.keyBoardMainHeightConstraint =
|
||||||
|
make.height.mas_equalTo(keyboardBaseHeight);
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
[self.contentView bringSubviewToFront:v];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 键盘隐藏时释放可重建资源(背景图/缓存/非必需面板),降低扩展内存峰值。
|
||||||
|
- (void)kb_releaseMemoryWhenKeyboardHidden {
|
||||||
|
[KBHUD setContainerView:nil];
|
||||||
|
self.bgImageView.image = nil;
|
||||||
|
self.kb_cachedGradientImage = nil;
|
||||||
|
[self.kb_defaultGradientLayer removeFromSuperlayer];
|
||||||
|
self.kb_defaultGradientLayer = nil;
|
||||||
|
[[SDImageCache sharedImageCache] clearMemory];
|
||||||
|
|
||||||
|
// 聊天相关可能持有音频数据/临时文件,键盘隐藏时直接清空,避免累计占用。
|
||||||
|
if (self.chatAudioPlayer) {
|
||||||
|
[self.chatAudioPlayer stop];
|
||||||
|
self.chatAudioPlayer = nil;
|
||||||
|
}
|
||||||
|
if (_chatMessages.count > 0) {
|
||||||
|
NSString *tmpRoot = NSTemporaryDirectory();
|
||||||
|
for (KBChatMessage *msg in _chatMessages.copy) {
|
||||||
|
if (tmpRoot.length > 0 && msg.audioFilePath.length > 0 &&
|
||||||
|
[msg.audioFilePath hasPrefix:tmpRoot]) {
|
||||||
|
[[NSFileManager defaultManager] removeItemAtPath:msg.audioFilePath
|
||||||
|
error:nil];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
[_chatMessages removeAllObjects];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_keyBoardMainView) {
|
||||||
|
[_keyBoardMainView removeFromSuperview];
|
||||||
|
_keyBoardMainView = nil;
|
||||||
|
}
|
||||||
|
self.keyBoardMainHeightConstraint = nil;
|
||||||
|
|
||||||
|
if (_functionView) {
|
||||||
|
[_functionView removeFromSuperview];
|
||||||
|
_functionView = nil;
|
||||||
|
}
|
||||||
|
if (_chatPanelView) {
|
||||||
|
[_chatPanelView removeFromSuperview];
|
||||||
|
_chatPanelView = nil;
|
||||||
|
}
|
||||||
|
self.chatPanelVisible = NO;
|
||||||
|
self.kb_panelMode = KBKeyboardPanelModeMain;
|
||||||
|
|
||||||
|
if (_subscriptionView) {
|
||||||
|
[_subscriptionView removeFromSuperview];
|
||||||
|
_subscriptionView = nil;
|
||||||
|
}
|
||||||
|
if (_settingView) {
|
||||||
|
[_settingView removeFromSuperview];
|
||||||
|
_settingView = nil;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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:
|
||||||
|
if (self.chatPanelVisible) {
|
||||||
|
[self kb_handleChatSendAction];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
[[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 kb_setPanelMode:KBKeyboardPanelModeFunction animated:NO];
|
||||||
|
[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 kb_setPanelMode:KBKeyboardPanelModeFunction animated:YES];
|
||||||
|
[self kb_clearCurrentWord];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (index == 1) {
|
||||||
|
[self kb_setPanelMode:KBKeyboardPanelModeChat animated:YES];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
[self kb_setPanelMode:KBKeyboardPanelModeMain animated:YES];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)keyBoardMainViewDidTapSettings:(KBKeyBoardMainView *)keyBoardMainView {
|
||||||
|
[[KBMaiPointReporter sharedReporter]
|
||||||
|
reportClickWithEventName:@"click_keyboard_settings_btn"
|
||||||
|
pageId:@"keyboard_main_panel"
|
||||||
|
elementId:@"settings_btn"
|
||||||
|
extra:nil
|
||||||
|
completion:nil];
|
||||||
|
[self kb_setPanelMode:KBKeyboardPanelModeSettings animated: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")];
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - KBFunctionViewDelegate
|
||||||
|
|
||||||
|
- (void)functionView:(KBFunctionView *)functionView
|
||||||
|
didTapToolActionAtIndex:(NSInteger)index {
|
||||||
|
// 需求:当 index == 0 时,切回键盘主视图
|
||||||
|
if (index == 0) {
|
||||||
|
[self kb_setPanelMode:KBKeyboardPanelModeMain animated: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];
|
||||||
|
// 从当前视图作为起点,通过响应链找到 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 - Actions
|
||||||
|
|
||||||
|
- (void)onTapSettingsBack {
|
||||||
|
[[KBMaiPointReporter sharedReporter]
|
||||||
|
reportClickWithEventName:@"click_keyboard_settings_back_btn"
|
||||||
|
pageId:@"keyboard_settings"
|
||||||
|
elementId:@"back_btn"
|
||||||
|
extra:nil
|
||||||
|
completion:nil];
|
||||||
|
[self kb_setPanelMode:KBKeyboardPanelModeMain animated:YES];
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
//
|
||||||
|
// KeyboardViewController+Private.h
|
||||||
|
// CustomKeyboard
|
||||||
|
//
|
||||||
|
// Created by Codex on 2026/02/22.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import "KeyboardViewController.h"
|
||||||
|
#import "Masonry.h"
|
||||||
|
|
||||||
|
@class AVAudioPlayer;
|
||||||
|
@class CAGradientLayer;
|
||||||
|
@class KBChatMessage;
|
||||||
|
@class KBChatPanelView;
|
||||||
|
@class KBFunctionView;
|
||||||
|
@class KBKeyBoardMainView;
|
||||||
|
@class KBKeyboardSubscriptionView;
|
||||||
|
@class KBSettingView;
|
||||||
|
@class KBSuggestionEngine;
|
||||||
|
|
||||||
|
@protocol KBChatLimitPopViewDelegate;
|
||||||
|
@protocol KBChatPanelViewDelegate;
|
||||||
|
@protocol KBFunctionViewDelegate;
|
||||||
|
@protocol KBKeyBoardMainViewDelegate;
|
||||||
|
@protocol KBKeyboardSubscriptionViewDelegate;
|
||||||
|
|
||||||
|
typedef NS_ENUM(NSInteger, KBKeyboardPanelMode) {
|
||||||
|
KBKeyboardPanelModeMain = 0,
|
||||||
|
KBKeyboardPanelModeFunction,
|
||||||
|
KBKeyboardPanelModeChat,
|
||||||
|
KBKeyboardPanelModeSettings,
|
||||||
|
KBKeyboardPanelModeSubscription,
|
||||||
|
};
|
||||||
|
|
||||||
|
@interface KeyboardViewController () <KBKeyBoardMainViewDelegate,
|
||||||
|
KBFunctionViewDelegate,
|
||||||
|
KBKeyboardSubscriptionViewDelegate,
|
||||||
|
KBChatPanelViewDelegate,
|
||||||
|
KBChatLimitPopViewDelegate>
|
||||||
|
{
|
||||||
|
UIButton *_nextKeyboardButton;
|
||||||
|
UIView *_contentView;
|
||||||
|
KBKeyBoardMainView *_keyBoardMainView;
|
||||||
|
KBFunctionView *_functionView;
|
||||||
|
KBSettingView *_settingView;
|
||||||
|
UIImageView *_bgImageView;
|
||||||
|
KBChatPanelView *_chatPanelView;
|
||||||
|
KBKeyboardSubscriptionView *_subscriptionView;
|
||||||
|
KBSuggestionEngine *_suggestionEngine;
|
||||||
|
NSString *_currentWord;
|
||||||
|
UIControl *_chatLimitMaskView;
|
||||||
|
MASConstraint *_contentWidthConstraint;
|
||||||
|
MASConstraint *_contentHeightConstraint;
|
||||||
|
MASConstraint *_keyBoardMainHeightConstraint;
|
||||||
|
MASConstraint *_chatPanelHeightConstraint;
|
||||||
|
NSLayoutConstraint *_kb_heightConstraint;
|
||||||
|
NSLayoutConstraint *_kb_widthConstraint;
|
||||||
|
CGFloat _kb_lastPortraitWidth;
|
||||||
|
CGFloat _kb_lastKeyboardHeight;
|
||||||
|
UIImage *_kb_cachedGradientImage;
|
||||||
|
CGSize _kb_cachedGradientSize;
|
||||||
|
CAGradientLayer *_kb_defaultGradientLayer;
|
||||||
|
NSString *_kb_lastAppliedThemeKey;
|
||||||
|
NSMutableArray<KBChatMessage *> *_chatMessages;
|
||||||
|
AVAudioPlayer *_chatAudioPlayer;
|
||||||
|
BOOL _suppressSuggestions;
|
||||||
|
BOOL _chatPanelVisible;
|
||||||
|
NSString *_chatPanelBaselineText;
|
||||||
|
id _kb_fullAccessObserverToken;
|
||||||
|
id _kb_skinObserverToken;
|
||||||
|
KBKeyboardPanelMode _kb_panelMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
@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) KBChatPanelView *chatPanelView;
|
||||||
|
@property(nonatomic, strong) KBKeyboardSubscriptionView *subscriptionView;
|
||||||
|
@property(nonatomic, strong) KBSuggestionEngine *suggestionEngine;
|
||||||
|
@property(nonatomic, copy) NSString *currentWord;
|
||||||
|
@property(nonatomic, assign) BOOL suppressSuggestions;
|
||||||
|
@property(nonatomic, strong) UIControl *chatLimitMaskView;
|
||||||
|
@property(nonatomic, strong) MASConstraint *contentWidthConstraint;
|
||||||
|
@property(nonatomic, strong) MASConstraint *contentHeightConstraint;
|
||||||
|
@property(nonatomic, strong) MASConstraint *keyBoardMainHeightConstraint;
|
||||||
|
@property(nonatomic, strong) MASConstraint *chatPanelHeightConstraint;
|
||||||
|
@property(nonatomic, strong) NSLayoutConstraint *kb_heightConstraint;
|
||||||
|
@property(nonatomic, strong) NSLayoutConstraint *kb_widthConstraint;
|
||||||
|
@property(nonatomic, assign) CGFloat kb_lastPortraitWidth;
|
||||||
|
@property(nonatomic, assign) CGFloat kb_lastKeyboardHeight;
|
||||||
|
@property(nonatomic, strong) UIImage *kb_cachedGradientImage;
|
||||||
|
@property(nonatomic, assign) CGSize kb_cachedGradientSize;
|
||||||
|
@property(nonatomic, strong, nullable) CAGradientLayer *kb_defaultGradientLayer;
|
||||||
|
@property(nonatomic, copy, nullable) NSString *kb_lastAppliedThemeKey;
|
||||||
|
@property(nonatomic, strong) NSMutableArray<KBChatMessage *> *chatMessages;
|
||||||
|
@property(nonatomic, strong) AVAudioPlayer *chatAudioPlayer;
|
||||||
|
@property(nonatomic, assign) BOOL chatPanelVisible;
|
||||||
|
@property(nonatomic, copy) NSString *chatPanelBaselineText; // 打开聊天面板时宿主输入框已有的文本
|
||||||
|
@property(nonatomic, strong, nullable) id kb_fullAccessObserverToken;
|
||||||
|
@property(nonatomic, strong, nullable) id kb_skinObserverToken;
|
||||||
|
@property(nonatomic, assign) KBKeyboardPanelMode kb_panelMode;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
@interface KeyboardViewController (KBPrivate)
|
||||||
|
|
||||||
|
// UI
|
||||||
|
- (void)setupUI;
|
||||||
|
- (nullable KBFunctionView *)kb_functionViewIfCreated;
|
||||||
|
|
||||||
|
// Panels
|
||||||
|
- (void)showFunctionPanel:(BOOL)show;
|
||||||
|
- (void)showSettingView:(BOOL)show;
|
||||||
|
- (void)showChatPanel:(BOOL)show;
|
||||||
|
- (void)showSubscriptionPanel;
|
||||||
|
- (void)hideSubscriptionPanel;
|
||||||
|
- (void)kb_setPanelMode:(KBKeyboardPanelMode)mode animated:(BOOL)animated;
|
||||||
|
- (void)kb_ensureFunctionViewIfNeeded;
|
||||||
|
- (void)kb_ensureChatPanelViewIfNeeded;
|
||||||
|
- (void)kb_ensureKeyBoardMainViewIfNeeded;
|
||||||
|
- (void)kb_releaseMemoryWhenKeyboardHidden;
|
||||||
|
|
||||||
|
// Suggestions
|
||||||
|
- (void)kb_updateCurrentWordWithInsertedText:(NSString *)text;
|
||||||
|
- (void)kb_clearCurrentWord;
|
||||||
|
- (void)kb_scheduleContextRefreshResetSuppression:(BOOL)resetSuppression;
|
||||||
|
- (void)kb_refreshCurrentWordFromDocumentContextResetSuppression:
|
||||||
|
(BOOL)resetSuppression;
|
||||||
|
- (void)kb_updateSuggestionsForCurrentWord;
|
||||||
|
|
||||||
|
// Chat
|
||||||
|
- (void)kb_handleChatSendAction;
|
||||||
|
|
||||||
|
// Theme
|
||||||
|
- (void)kb_applyTheme;
|
||||||
|
- (void)kb_applyDefaultSkinIfNeeded;
|
||||||
|
- (void)kb_consumePendingShopSkin;
|
||||||
|
- (void)kb_registerDarwinSkinInstallObserver;
|
||||||
|
- (void)kb_unregisterDarwinSkinInstallObserver;
|
||||||
|
|
||||||
|
// Layout
|
||||||
|
- (CGFloat)kb_portraitWidth;
|
||||||
|
- (CGFloat)kb_keyboardHeightForWidth:(CGFloat)width;
|
||||||
|
- (CGFloat)kb_keyboardBaseHeightForWidth:(CGFloat)width;
|
||||||
|
- (CGFloat)kb_chatPanelHeightForWidth:(CGFloat)width;
|
||||||
|
- (void)kb_updateKeyboardLayoutIfNeeded;
|
||||||
|
|
||||||
|
@end
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
//
|
||||||
|
// KeyboardViewController+Subscription.m
|
||||||
|
// CustomKeyboard
|
||||||
|
//
|
||||||
|
// Created by Codex on 2026/02/22.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import "KeyboardViewController+Private.h"
|
||||||
|
|
||||||
|
#import "KBAuthManager.h"
|
||||||
|
#import "KBFullAccessManager.h"
|
||||||
|
#import "KBHostAppLauncher.h"
|
||||||
|
#import "KBKeyboardSubscriptionProduct.h"
|
||||||
|
#import "KBKeyboardSubscriptionView.h"
|
||||||
|
|
||||||
|
@implementation KeyboardViewController (Subscription)
|
||||||
|
|
||||||
|
- (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;
|
||||||
|
}
|
||||||
|
[self kb_setPanelMode:KBKeyboardPanelModeSubscription animated:YES];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)hideSubscriptionPanel {
|
||||||
|
if (self.kb_panelMode != KBKeyboardPanelModeSubscription) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
[self kb_setPanelMode:KBKeyboardPanelModeMain animated:YES];
|
||||||
|
}
|
||||||
|
|
||||||
|
#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 - 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]
|
||||||
|
?: @"";
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
//
|
||||||
|
// KeyboardViewController+Suggestions.m
|
||||||
|
// CustomKeyboard
|
||||||
|
//
|
||||||
|
// Created by Codex on 2026/02/22.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import "KeyboardViewController+Private.h"
|
||||||
|
|
||||||
|
#import "KBBackspaceUndoManager.h"
|
||||||
|
#import "KBInputBufferManager.h"
|
||||||
|
#import "KBKeyBoardMainView.h"
|
||||||
|
#import "KBSuggestionEngine.h"
|
||||||
|
|
||||||
|
@implementation KeyboardViewController (Suggestions)
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - KBKeyBoardMainViewDelegate (Suggestion)
|
||||||
|
|
||||||
|
- (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];
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
@@ -0,0 +1,376 @@
|
|||||||
|
//
|
||||||
|
// KeyboardViewController+Theme.m
|
||||||
|
// CustomKeyboard
|
||||||
|
//
|
||||||
|
// Created by Codex on 2026/02/22.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import "KeyboardViewController+Private.h"
|
||||||
|
|
||||||
|
#import "KBFunctionView.h"
|
||||||
|
#import "KBKeyBoardMainView.h"
|
||||||
|
#import "KBSkinInstallBridge.h"
|
||||||
|
#import "KBSkinManager.h"
|
||||||
|
#import "UIImage+KBColor.h"
|
||||||
|
#import <QuartzCore/QuartzCore.h>
|
||||||
|
|
||||||
|
static NSString *const kKBDefaultSkinIdLight = @"normal_them";
|
||||||
|
static NSString *const kKBDefaultSkinZipNameLight = @"normal_them";
|
||||||
|
static NSString *const kKBDefaultSkinIdDark = @"normal_hei_them";
|
||||||
|
static NSString *const kKBDefaultSkinZipNameDark = @"normal_hei_them";
|
||||||
|
|
||||||
|
// 提前声明一个类别,使编译器在 static 回调中识别 kb_consumePendingShopSkin 方法。
|
||||||
|
@interface KeyboardViewController (KBSkinShopBridge)
|
||||||
|
- (void)kb_consumePendingShopSkin;
|
||||||
|
@end
|
||||||
|
|
||||||
|
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];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@implementation KeyboardViewController (Theme)
|
||||||
|
|
||||||
|
- (void)kb_registerDarwinSkinInstallObserver {
|
||||||
|
CFNotificationCenterAddObserver(
|
||||||
|
CFNotificationCenterGetDarwinNotifyCenter(),
|
||||||
|
(__bridge const void *)(self), KBSkinInstallNotificationCallback,
|
||||||
|
(__bridge CFStringRef)KBDarwinSkinInstallRequestNotification, NULL,
|
||||||
|
CFNotificationSuspensionBehaviorDeliverImmediately);
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)kb_unregisterDarwinSkinInstallObserver {
|
||||||
|
CFNotificationCenterRemoveObserver(
|
||||||
|
CFNotificationCenterGetDarwinNotifyCenter(),
|
||||||
|
(__bridge const void *)(self),
|
||||||
|
(__bridge CFStringRef)KBDarwinSkinInstallRequestNotification, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)kb_applyTheme {
|
||||||
|
@autoreleasepool {
|
||||||
|
KBSkinTheme *t = [KBSkinManager shared].current;
|
||||||
|
UIImage *img = nil;
|
||||||
|
BOOL isDefaultTheme = [self kb_isDefaultKeyboardTheme:t];
|
||||||
|
BOOL isDarkMode = [self kb_isDarkModeActive];
|
||||||
|
|
||||||
|
NSString *skinId = t.skinId ?: @"";
|
||||||
|
NSString *themeKey =
|
||||||
|
[NSString stringWithFormat:@"%@|default=%d|dark=%d", skinId,
|
||||||
|
isDefaultTheme, isDarkMode];
|
||||||
|
BOOL themeChanged =
|
||||||
|
(self.kb_lastAppliedThemeKey.length == 0 ||
|
||||||
|
![self.kb_lastAppliedThemeKey isEqualToString:themeKey]);
|
||||||
|
if (themeChanged) {
|
||||||
|
self.kb_lastAppliedThemeKey = themeKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
CGSize size = self.bgImageView.bounds.size;
|
||||||
|
if (isDefaultTheme) {
|
||||||
|
if (isDarkMode) {
|
||||||
|
// 暗黑模式:直接使用背景色,不使用图片渲染
|
||||||
|
// 这样可以避免图片渲染时的色彩空间转换导致颜色不一致
|
||||||
|
img = nil;
|
||||||
|
self.bgImageView.image = nil;
|
||||||
|
[self.kb_defaultGradientLayer removeFromSuperlayer];
|
||||||
|
self.kb_defaultGradientLayer = nil;
|
||||||
|
// 使用与系统键盘底部完全相同的颜色
|
||||||
|
if (@available(iOS 13.0, *)) {
|
||||||
|
// iOS 系统键盘使用的实际颜色 (RGB: 44, 44, 46 in sRGB, 或 #2C2C2E)
|
||||||
|
// 但为了完美匹配,我们使用动态颜色并直接设置为背景
|
||||||
|
UIColor *kbBgColor =
|
||||||
|
[UIColor colorWithDynamicProvider:^UIColor *_Nonnull(
|
||||||
|
UITraitCollection *_Nonnull traitCollection) {
|
||||||
|
if (traitCollection.userInterfaceStyle ==
|
||||||
|
UIUserInterfaceStyleDark) {
|
||||||
|
// 暗黑模式下系统键盘实际背景色
|
||||||
|
return [UIColor colorWithRed:43.0 / 255.0
|
||||||
|
green:43.0 / 255.0
|
||||||
|
blue:43.0 / 255.0
|
||||||
|
alpha:1.0];
|
||||||
|
} else {
|
||||||
|
return [UIColor colorWithRed:209.0 / 255.0
|
||||||
|
green:211.0 / 255.0
|
||||||
|
blue:219.0 / 255.0
|
||||||
|
alpha:1.0];
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
self.contentView.backgroundColor = kbBgColor;
|
||||||
|
self.bgImageView.backgroundColor = kbBgColor;
|
||||||
|
} else {
|
||||||
|
UIColor *darkColor = [UIColor colorWithRed:43.0 / 255.0
|
||||||
|
green:43.0 / 255.0
|
||||||
|
blue:43.0 / 255.0
|
||||||
|
alpha:1.0];
|
||||||
|
self.contentView.backgroundColor = darkColor;
|
||||||
|
self.bgImageView.backgroundColor = darkColor;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 浅色模式:使用渐变层(避免生成大位图导致内存上涨)
|
||||||
|
if (size.width <= 0 || size.height <= 0) {
|
||||||
|
[self.view layoutIfNeeded];
|
||||||
|
size = self.bgImageView.bounds.size;
|
||||||
|
}
|
||||||
|
if (size.width <= 0 || size.height <= 0) {
|
||||||
|
size = self.view.bounds.size;
|
||||||
|
}
|
||||||
|
if (size.width <= 0 || size.height <= 0) {
|
||||||
|
size = [UIScreen mainScreen].bounds.size;
|
||||||
|
}
|
||||||
|
UIColor *topColor = [UIColor colorWithHex:0xDEDFE4];
|
||||||
|
UIColor *bottomColor = [UIColor colorWithHex:0xD1D3DB];
|
||||||
|
UIColor *resolvedTopColor = topColor;
|
||||||
|
UIColor *resolvedBottomColor = bottomColor;
|
||||||
|
if (@available(iOS 13.0, *)) {
|
||||||
|
resolvedTopColor =
|
||||||
|
[topColor resolvedColorWithTraitCollection:self.traitCollection];
|
||||||
|
resolvedBottomColor =
|
||||||
|
[bottomColor resolvedColorWithTraitCollection:self.traitCollection];
|
||||||
|
}
|
||||||
|
CAGradientLayer *layer = self.kb_defaultGradientLayer;
|
||||||
|
if (!layer) {
|
||||||
|
layer = [CAGradientLayer layer];
|
||||||
|
layer.startPoint = CGPointMake(0.5, 0.0);
|
||||||
|
layer.endPoint = CGPointMake(0.5, 1.0);
|
||||||
|
[self.bgImageView.layer insertSublayer:layer atIndex:0];
|
||||||
|
self.kb_defaultGradientLayer = layer;
|
||||||
|
}
|
||||||
|
layer.colors =
|
||||||
|
@[ (id)resolvedTopColor.CGColor, (id)resolvedBottomColor.CGColor ];
|
||||||
|
layer.frame = (CGRect){CGPointZero, size};
|
||||||
|
img = nil;
|
||||||
|
self.bgImageView.image = nil;
|
||||||
|
self.contentView.backgroundColor = [UIColor clearColor];
|
||||||
|
self.bgImageView.backgroundColor = [UIColor clearColor];
|
||||||
|
}
|
||||||
|
NSLog(@"===");
|
||||||
|
} else {
|
||||||
|
// 自定义皮肤:清除背景色,使用皮肤图片
|
||||||
|
self.contentView.backgroundColor = [UIColor clearColor];
|
||||||
|
self.bgImageView.backgroundColor = [UIColor clearColor];
|
||||||
|
[self.kb_defaultGradientLayer removeFromSuperlayer];
|
||||||
|
self.kb_defaultGradientLayer = nil;
|
||||||
|
img = [[KBSkinManager shared] currentBackgroundImage];
|
||||||
|
}
|
||||||
|
NSLog(@"⌨️[Keyboard] apply theme id=%@ hasBg=%d", t.skinId, (img != nil));
|
||||||
|
[self kb_logSkinDiagnosticsWithTheme:t backgroundImage:img];
|
||||||
|
self.bgImageView.image = img;
|
||||||
|
|
||||||
|
// 触发键区按主题重绘
|
||||||
|
if (themeChanged &&
|
||||||
|
[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
|
||||||
|
}
|
||||||
|
// 注意:这里不能直接访问 self.functionView,否则会导致功能面板提前创建,占用内存。
|
||||||
|
KBFunctionView *functionView = [self kb_functionViewIfCreated];
|
||||||
|
if (themeChanged && functionView &&
|
||||||
|
[functionView respondsToSelector:@selector(kb_applyTheme)]) {
|
||||||
|
#pragma clang diagnostic push
|
||||||
|
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
||||||
|
[functionView performSelector:@selector(kb_applyTheme)];
|
||||||
|
#pragma clang diagnostic pop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (BOOL)kb_isDefaultKeyboardTheme:(KBSkinTheme *)theme {
|
||||||
|
NSString *skinId = theme.skinId ?: @"";
|
||||||
|
if (skinId.length == 0 || [skinId isEqualToString:@"default"]) {
|
||||||
|
return YES;
|
||||||
|
}
|
||||||
|
if ([skinId isEqualToString:kKBDefaultSkinIdLight]) {
|
||||||
|
return YES;
|
||||||
|
}
|
||||||
|
return [skinId isEqualToString:kKBDefaultSkinIdDark];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (BOOL)kb_isDarkModeActive {
|
||||||
|
if (@available(iOS 13.0, *)) {
|
||||||
|
return self.traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark;
|
||||||
|
}
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSString *)kb_defaultSkinIdForCurrentStyle {
|
||||||
|
return [self kb_isDarkModeActive] ? kKBDefaultSkinIdDark
|
||||||
|
: kKBDefaultSkinIdLight;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSString *)kb_defaultSkinZipNameForCurrentStyle {
|
||||||
|
return [self kb_isDarkModeActive] ? kKBDefaultSkinZipNameDark
|
||||||
|
: kKBDefaultSkinZipNameLight;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (UIImage *)kb_defaultGradientImageWithSize:(CGSize)size
|
||||||
|
topColor:(UIColor *)topColor
|
||||||
|
bottomColor:(UIColor *)bottomColor {
|
||||||
|
if (size.width <= 0 || size.height <= 0) {
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尺寸未变则复用缓存,避免反复创建图片撑爆键盘扩展内存
|
||||||
|
if (self.kb_cachedGradientImage &&
|
||||||
|
CGSizeEqualToSize(self.kb_cachedGradientSize, size)) {
|
||||||
|
return self.kb_cachedGradientImage;
|
||||||
|
}
|
||||||
|
|
||||||
|
UIColor *resolvedTopColor = topColor;
|
||||||
|
UIColor *resolvedBottomColor = bottomColor;
|
||||||
|
if (@available(iOS 13.0, *)) {
|
||||||
|
resolvedTopColor =
|
||||||
|
[topColor resolvedColorWithTraitCollection:self.traitCollection];
|
||||||
|
resolvedBottomColor =
|
||||||
|
[bottomColor resolvedColorWithTraitCollection:self.traitCollection];
|
||||||
|
}
|
||||||
|
|
||||||
|
CAGradientLayer *layer = [CAGradientLayer layer];
|
||||||
|
layer.frame = CGRectMake(0, 0, size.width, size.height);
|
||||||
|
layer.startPoint = CGPointMake(0.5, 0.0);
|
||||||
|
layer.endPoint = CGPointMake(0.5, 1.0);
|
||||||
|
layer.colors =
|
||||||
|
@[ (id)resolvedTopColor.CGColor, (id)resolvedBottomColor.CGColor ];
|
||||||
|
|
||||||
|
UIGraphicsBeginImageContextWithOptions(size, YES, 0);
|
||||||
|
[layer renderInContext:UIGraphicsGetCurrentContext()];
|
||||||
|
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
|
||||||
|
UIGraphicsEndImageContext();
|
||||||
|
|
||||||
|
self.kb_cachedGradientImage = image;
|
||||||
|
self.kb_cachedGradientSize = size;
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)kb_logSkinDiagnosticsWithTheme:(KBSkinTheme *)theme
|
||||||
|
backgroundImage:(UIImage *)image {
|
||||||
|
#if DEBUG
|
||||||
|
NSString *skinId = theme.skinId ?: @"";
|
||||||
|
NSString *name = theme.name ?: @"";
|
||||||
|
NSMutableArray<NSString *> *roots = [NSMutableArray array];
|
||||||
|
NSURL *containerURL = [[NSFileManager defaultManager]
|
||||||
|
containerURLForSecurityApplicationGroupIdentifier:AppGroup];
|
||||||
|
if (containerURL.path.length > 0) {
|
||||||
|
[roots addObject:containerURL.path];
|
||||||
|
}
|
||||||
|
NSString *cacheRoot = NSSearchPathForDirectoriesInDomains(
|
||||||
|
NSCachesDirectory, NSUserDomainMask, YES)
|
||||||
|
.firstObject;
|
||||||
|
if (cacheRoot.length > 0) {
|
||||||
|
[roots addObject:cacheRoot];
|
||||||
|
}
|
||||||
|
|
||||||
|
NSFileManager *fm = [NSFileManager defaultManager];
|
||||||
|
NSMutableArray<NSString *> *lines = [NSMutableArray array];
|
||||||
|
for (NSString *root in roots) {
|
||||||
|
NSString *iconsDir = [[root stringByAppendingPathComponent:@"Skins"]
|
||||||
|
stringByAppendingPathComponent:skinId];
|
||||||
|
iconsDir = [iconsDir stringByAppendingPathComponent:@"icons"];
|
||||||
|
BOOL isDir = NO;
|
||||||
|
BOOL exists = [fm fileExistsAtPath:iconsDir isDirectory:&isDir] && isDir;
|
||||||
|
NSArray *contents =
|
||||||
|
exists ? [fm contentsOfDirectoryAtPath:iconsDir error:nil] : nil;
|
||||||
|
NSUInteger count = contents.count;
|
||||||
|
BOOL hasQ =
|
||||||
|
exists &&
|
||||||
|
[fm fileExistsAtPath:[iconsDir
|
||||||
|
stringByAppendingPathComponent:@"key_q.png"]];
|
||||||
|
BOOL hasQUp =
|
||||||
|
exists && [fm fileExistsAtPath:[iconsDir stringByAppendingPathComponent:
|
||||||
|
@"key_q_up.png"]];
|
||||||
|
BOOL hasDel =
|
||||||
|
exists && [fm fileExistsAtPath:[iconsDir stringByAppendingPathComponent:
|
||||||
|
@"key_del.png"]];
|
||||||
|
BOOL hasShift =
|
||||||
|
exists &&
|
||||||
|
[fm fileExistsAtPath:[iconsDir
|
||||||
|
stringByAppendingPathComponent:@"key_up.png"]];
|
||||||
|
BOOL hasShiftUpper =
|
||||||
|
exists && [fm fileExistsAtPath:[iconsDir stringByAppendingPathComponent:
|
||||||
|
@"key_up_upper.png"]];
|
||||||
|
NSString *line = [NSString
|
||||||
|
stringWithFormat:@"root=%@ icons=%@ exist=%d count=%tu key_q=%d "
|
||||||
|
@"key_q_up=%d key_del=%d key_up=%d key_up_upper=%d",
|
||||||
|
root, iconsDir, exists, count, hasQ, hasQUp, hasDel,
|
||||||
|
hasShift, hasShiftUpper];
|
||||||
|
[lines addObject:line];
|
||||||
|
}
|
||||||
|
|
||||||
|
NSLog(@"[Keyboard] theme id=%@ name=%@ hasBg=%d\n%@", skinId, name,
|
||||||
|
(image != nil), [lines componentsJoinedByString:@"\n"]);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
- (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(
|
||||||
|
@"皮肤已更新,立即体验吧")];
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)kb_applyDefaultSkinIfNeeded {
|
||||||
|
NSDictionary *pending = [KBSkinInstallBridge pendingRequestPayload];
|
||||||
|
if (pending.count > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSString *currentId = [KBSkinManager shared].current.skinId ?: @"";
|
||||||
|
BOOL isDefault =
|
||||||
|
(currentId.length == 0 || [currentId isEqualToString:@"default"]);
|
||||||
|
BOOL isLightDefault = [currentId isEqualToString:kKBDefaultSkinIdLight];
|
||||||
|
BOOL isDarkDefault = [currentId isEqualToString:kKBDefaultSkinIdDark];
|
||||||
|
if (!isDefault && !isLightDefault && !isDarkDefault) {
|
||||||
|
// 用户已应用自定义皮肤:不随深色模式切换默认皮肤
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
NSString *targetId = [self kb_defaultSkinIdForCurrentStyle];
|
||||||
|
NSString *targetZip = [self kb_defaultSkinZipNameForCurrentStyle];
|
||||||
|
if (currentId.length > 0 && [currentId isEqualToString:targetId]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSError *applyError = nil;
|
||||||
|
if ([KBSkinInstallBridge applyInstalledSkinWithId:targetId error:&applyError]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
[KBSkinInstallBridge publishBundleSkinRequestWithId:targetId
|
||||||
|
name:targetId
|
||||||
|
zipName:targetZip
|
||||||
|
iconShortNames:nil];
|
||||||
|
[KBSkinInstallBridge
|
||||||
|
consumePendingRequestFromBundle:NSBundle.mainBundle
|
||||||
|
completion:^(__unused BOOL success,
|
||||||
|
__unused NSError *_Nullable error) {
|
||||||
|
// 已通过通知触发主题刷新,这里无需额外处理
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
//
|
||||||
|
// KeyboardViewController+UI.m
|
||||||
|
// CustomKeyboard
|
||||||
|
//
|
||||||
|
// Created by Codex on 2026/02/22.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import "KeyboardViewController+Private.h"
|
||||||
|
|
||||||
|
#import "KBChatMessage.h"
|
||||||
|
#import "KBChatPanelView.h"
|
||||||
|
#import "KBFunctionView.h"
|
||||||
|
#import "KBKeyBoardMainView.h"
|
||||||
|
#import "KBKeyboardSubscriptionView.h"
|
||||||
|
#import "KBSettingView.h"
|
||||||
|
#import "Masonry.h"
|
||||||
|
|
||||||
|
@implementation KeyboardViewController (UI)
|
||||||
|
|
||||||
|
- (void)setupUI {
|
||||||
|
self.view.translatesAutoresizingMaskIntoConstraints = NO;
|
||||||
|
|
||||||
|
// 按“短边”宽度等比缩放,横屏保持竖屏布局比例
|
||||||
|
CGFloat portraitWidth = [self kb_portraitWidth];
|
||||||
|
CGFloat keyboardHeight = [self kb_keyboardHeightForWidth:portraitWidth];
|
||||||
|
CGFloat keyboardBaseHeight = [self kb_keyboardBaseHeightForWidth:portraitWidth];
|
||||||
|
CGFloat screenWidth = CGRectGetWidth([UIScreen mainScreen].bounds);
|
||||||
|
|
||||||
|
// FIX: iOS 26 键盘闪烁问题
|
||||||
|
// iOS 26 在键盘滑入动画开始前,会对 self.view 做一次离屏预渲染快照(非实时 view),
|
||||||
|
// 该快照会短暂显示在屏幕中间。如果此时 view 已有完整高度和内容,用户就会看到
|
||||||
|
// 键盘 UI 在屏幕中间闪现一帧,然后键盘才从底部正常滑入。
|
||||||
|
// 解决方案:初始高度设为 0,让系统快照时无内容可渲染;
|
||||||
|
// 在 viewWillAppear: 中恢复正确高度,此时系统已准备好滑入动画。
|
||||||
|
// (iOS 18 及更早版本无此预渲染机制,不受影响)
|
||||||
|
NSLayoutConstraint *h = [self.view.heightAnchor constraintEqualToConstant:0];
|
||||||
|
NSLayoutConstraint *w =
|
||||||
|
[self.view.widthAnchor constraintEqualToConstant:screenWidth];
|
||||||
|
self.kb_heightConstraint = h;
|
||||||
|
self.kb_widthConstraint = w;
|
||||||
|
|
||||||
|
h.priority = UILayoutPriorityRequired;
|
||||||
|
w.priority = UILayoutPriorityRequired;
|
||||||
|
[NSLayoutConstraint activateConstraints:@[ h, w ]];
|
||||||
|
// 关闭 UIInputView 自适应(某些系统版本会尝试放大为全屏高度导致冲突)
|
||||||
|
if ([self.view isKindOfClass:[UIInputView class]]) {
|
||||||
|
UIInputView *iv = (UIInputView *)self.view;
|
||||||
|
if ([iv respondsToSelector:@selector(setAllowsSelfSizing:)]) {
|
||||||
|
iv.allowsSelfSizing = NO;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 内容容器:横屏时保持竖屏宽度,居中显示
|
||||||
|
[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.contentView addSubview:self.keyBoardMainView];
|
||||||
|
[self.keyBoardMainView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
|
make.left.right.equalTo(self.contentView);
|
||||||
|
make.bottom.equalTo(self.contentView);
|
||||||
|
self.keyBoardMainHeightConstraint =
|
||||||
|
make.height.mas_equalTo(keyboardBaseHeight);
|
||||||
|
}];
|
||||||
|
|
||||||
|
// 初始隐藏,避免布局完成前闪烁
|
||||||
|
self.contentView.hidden = YES;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - Lazy
|
||||||
|
|
||||||
|
- (nullable KBFunctionView *)kb_functionViewIfCreated {
|
||||||
|
return _functionView;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (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;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (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;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (KBChatPanelView *)chatPanelView {
|
||||||
|
if (!_chatPanelView) {
|
||||||
|
NSLog(@"[Keyboard] ⚠️ chatPanelView 被创建!");
|
||||||
|
_chatPanelView = [[KBChatPanelView alloc] init];
|
||||||
|
_chatPanelView.delegate = self;
|
||||||
|
}
|
||||||
|
return _chatPanelView;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSMutableArray<KBChatMessage *> *)chatMessages {
|
||||||
|
if (!_chatMessages) {
|
||||||
|
_chatMessages = [NSMutableArray array];
|
||||||
|
}
|
||||||
|
return _chatMessages;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (KBKeyboardSubscriptionView *)subscriptionView {
|
||||||
|
if (!_subscriptionView) {
|
||||||
|
_subscriptionView = [[KBKeyboardSubscriptionView alloc] init];
|
||||||
|
_subscriptionView.delegate = self;
|
||||||
|
_subscriptionView.hidden = YES;
|
||||||
|
_subscriptionView.alpha = 0.0;
|
||||||
|
}
|
||||||
|
return _subscriptionView;
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
46
CustomKeyboard/Manager/KBEmojiDataProvider.h
Normal 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
|
||||||
270
CustomKeyboard/Manager/KBEmojiDataProvider.m
Normal 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
|
||||||
43
CustomKeyboard/Manager/KBFullAccessManager.h
Normal 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
|
||||||
|
|
||||||
100
CustomKeyboard/Manager/KBFullAccessManager.m
Normal 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
|
||||||
23
CustomKeyboard/Manager/KBSuggestionEngine.h
Normal 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
|
||||||
167
CustomKeyboard/Manager/KBSuggestionEngine.m
Normal 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
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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))
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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"
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||