Compare commits
415 Commits
efb04d134e
...
dev_上报埋点
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
@@ -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,10 @@
|
|||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
|
<key>LSApplicationQueriesSchemes</key>
|
||||||
|
<array>
|
||||||
|
<string>kbkeyboardAppExtension</string>
|
||||||
|
</array>
|
||||||
<key>NSExtension</key>
|
<key>NSExtension</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSExtensionAttributes</key>
|
<key>NSExtensionAttributes</key>
|
||||||
|
|||||||
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" : "切图 270@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "切图 270@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
CustomKeyboard/KeyboardAssets.xcassets/ai_key_icon.imageset/切图 270@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
CustomKeyboard/KeyboardAssets.xcassets/ai_key_icon.imageset/切图 270@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 11 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/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 |
22
CustomKeyboard/KeyboardAssets.xcassets/kb_del_icon.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "kb_del_icon@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "kb_del_icon@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
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.png
vendored
Normal file
|
After Width: | Height: | Size: 2.0 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 |
@@ -6,74 +6,825 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
#import "KeyboardViewController.h"
|
#import "KeyboardViewController.h"
|
||||||
|
#import "KBKeyBoardMainView.h"
|
||||||
|
|
||||||
static CGFloat KEYBOARDHEIGHT = 256;
|
#import "KBKey.h"
|
||||||
|
#import "KBFunctionView.h"
|
||||||
|
#import "KBSettingView.h"
|
||||||
|
#import "Masonry.h"
|
||||||
|
#import "KBAuthManager.h"
|
||||||
|
#import "KBFullAccessManager.h"
|
||||||
|
#import "KBSkinManager.h"
|
||||||
|
#import "KBSkinInstallBridge.h"
|
||||||
|
#import "KBHostAppLauncher.h"
|
||||||
|
#import "KBKeyboardSubscriptionView.h"
|
||||||
|
#import "KBKeyboardSubscriptionProduct.h"
|
||||||
|
#import "KBBackspaceUndoManager.h"
|
||||||
|
#import "KBInputBufferManager.h"
|
||||||
|
#import "KBSuggestionEngine.h"
|
||||||
|
|
||||||
@interface KeyboardViewController ()
|
// 提前声明一个类别,使编译器在 static 回调中识别 kb_consumePendingShopSkin 方法。
|
||||||
@property (nonatomic, strong) UIButton *nextKeyboardButton;
|
@interface KeyboardViewController (KBSkinShopBridge)
|
||||||
|
- (void)kb_consumePendingShopSkin;
|
||||||
|
@end
|
||||||
|
|
||||||
|
// 以 375 宽设计稿为基准的键盘总高度
|
||||||
|
static const CGFloat kKBKeyboardBaseHeight = 250.0f;
|
||||||
|
|
||||||
|
static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
||||||
|
void *observer,
|
||||||
|
CFStringRef name,
|
||||||
|
const void *object,
|
||||||
|
CFDictionaryRef userInfo) {
|
||||||
|
KeyboardViewController *strongSelf = (__bridge KeyboardViewController *)observer;
|
||||||
|
if (!strongSelf) { return; }
|
||||||
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
|
if ([strongSelf respondsToSelector:@selector(kb_consumePendingShopSkin)]) {
|
||||||
|
[strongSelf kb_consumePendingShopSkin];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@interface KeyboardViewController () <KBKeyBoardMainViewDelegate, KBFunctionViewDelegate, KBKeyboardSubscriptionViewDelegate>
|
||||||
|
@property (nonatomic, strong) UIButton *nextKeyboardButton; // 系统“下一个键盘”按钮(可选)
|
||||||
|
@property (nonatomic, strong) UIView *contentView;
|
||||||
|
@property (nonatomic, strong) KBKeyBoardMainView *keyBoardMainView; // 功能面板视图(点击工具栏第0个时显示)
|
||||||
|
@property (nonatomic, strong) KBFunctionView *functionView; // 功能面板视图(点击工具栏第0个时显示)
|
||||||
|
@property (nonatomic, strong) KBSettingView *settingView; // 设置页
|
||||||
|
@property (nonatomic, strong) UIImageView *bgImageView; // 背景图(在底层)
|
||||||
|
@property (nonatomic, strong) KBKeyboardSubscriptionView *subscriptionView;
|
||||||
|
@property (nonatomic, strong) KBSuggestionEngine *suggestionEngine;
|
||||||
|
@property (nonatomic, copy) NSString *currentWord;
|
||||||
|
@property (nonatomic, assign) BOOL suppressSuggestions;
|
||||||
|
@property (nonatomic, strong) MASConstraint *contentWidthConstraint;
|
||||||
|
@property (nonatomic, strong) MASConstraint *contentHeightConstraint;
|
||||||
|
@property (nonatomic, strong) NSLayoutConstraint *kb_heightConstraint;
|
||||||
|
@property (nonatomic, strong) NSLayoutConstraint *kb_widthConstraint;
|
||||||
|
@property (nonatomic, assign) CGFloat kb_lastPortraitWidth;
|
||||||
|
@property (nonatomic, assign) CGFloat kb_lastKeyboardHeight;
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@implementation KeyboardViewController
|
@implementation KeyboardViewController
|
||||||
|
|
||||||
- (void)updateViewConstraints {
|
{
|
||||||
[super updateViewConstraints];
|
BOOL _kb_didTriggerLoginDeepLinkOnce;
|
||||||
|
|
||||||
// Add custom view sizing constraints here
|
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)viewDidLoad {
|
- (void)viewDidLoad {
|
||||||
[super viewDidLoad];
|
[super viewDidLoad];
|
||||||
//
|
// 撤销删除是“上一段删除操作”的临时状态;键盘被系统回收/重建或跨页面回来时应当清空,避免误显示。
|
||||||
// // Perform custom UI setup here
|
[[KBBackspaceUndoManager shared] registerNonClearAction];
|
||||||
// self.nextKeyboardButton = [UIButton buttonWithType:UIButtonTypeSystem];
|
|
||||||
//
|
|
||||||
// [self.nextKeyboardButton setTitle:NSLocalizedString(@"Next Keyboard", @"Title for 'Next Keyboard' button") forState:UIControlStateNormal];
|
|
||||||
// [self.nextKeyboardButton sizeToFit];
|
|
||||||
// self.nextKeyboardButton.translatesAutoresizingMaskIntoConstraints = NO;
|
|
||||||
//
|
|
||||||
// [self.nextKeyboardButton addTarget:self action:@selector(handleInputModeListFromView:withEvent:) forControlEvents:UIControlEventAllTouchEvents];
|
|
||||||
//
|
|
||||||
// [self.view addSubview:self.nextKeyboardButton];
|
|
||||||
//
|
|
||||||
// [self.nextKeyboardButton.leftAnchor constraintEqualToAnchor:self.view.leftAnchor].active = YES;
|
|
||||||
// [self.nextKeyboardButton.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor].active = YES;
|
|
||||||
|
|
||||||
[self setupUI];
|
[self setupUI];
|
||||||
|
self.suggestionEngine = [KBSuggestionEngine shared];
|
||||||
|
self.currentWord = @"";
|
||||||
|
// 指定 HUD 的承载视图(扩展里无法取到 App 的 KeyWindow)
|
||||||
|
[KBHUD setContainerView:self.view];
|
||||||
|
// 绑定完全访问管理器,便于统一感知和联动网络开关
|
||||||
|
[[KBFullAccessManager shared] bindInputController:self];
|
||||||
|
__unused id token = [[NSNotificationCenter defaultCenter] addObserverForName:KBFullAccessChangedNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(__unused NSNotification * _Nonnull note) {
|
||||||
|
// 如需,可在此刷新与完全访问相关的 UI
|
||||||
|
}];
|
||||||
|
|
||||||
|
// 皮肤变化时,立即应用
|
||||||
|
__unused id token2 = [[NSNotificationCenter defaultCenter] addObserverForName:KBSkinDidChangeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(__unused NSNotification * _Nonnull note) {
|
||||||
|
[self kb_applyTheme];
|
||||||
|
}];
|
||||||
|
[self kb_applyTheme];
|
||||||
|
CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(),
|
||||||
|
(__bridge const void *)(self),
|
||||||
|
KBSkinInstallNotificationCallback,
|
||||||
|
(__bridge CFStringRef)KBDarwinSkinInstallRequestNotification,
|
||||||
|
NULL,
|
||||||
|
CFNotificationSuspensionBehaviorDeliverImmediately);
|
||||||
|
[self kb_consumePendingShopSkin];
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)viewWillAppear:(BOOL)animated{
|
||||||
|
[super viewWillAppear:animated];
|
||||||
|
// 进入/重新进入输入界面时,清理上一次会话残留的撤销状态与缓存,避免显示“撤销删除”但实际上已不可撤销。
|
||||||
|
[[KBBackspaceUndoManager shared] registerNonClearAction];
|
||||||
|
[[KBInputBufferManager shared] resetWithText:@""];
|
||||||
|
[[KBLocalizationManager shared] reloadFromSharedStorageIfNeeded];
|
||||||
|
// 注意:微信/QQ 等宿主的 documentContext 可能是“截断窗口”,这里只更新 liveText,不要把它当作全文 manualSnapshot。
|
||||||
|
[[KBInputBufferManager shared] updateFromExternalContextBefore:self.textDocumentProxy.documentContextBeforeInput
|
||||||
|
after:self.textDocumentProxy.documentContextAfterInput];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)viewWillDisappear:(BOOL)animated {
|
||||||
|
[super viewWillDisappear:animated];
|
||||||
|
[[KBBackspaceUndoManager shared] registerNonClearAction];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)textDidChange:(id<UITextInput>)textInput {
|
||||||
|
[super textDidChange:textInput];
|
||||||
|
[[KBInputBufferManager shared] updateFromExternalContextBefore:self.textDocumentProxy.documentContextBeforeInput
|
||||||
|
after:self.textDocumentProxy.documentContextAfterInput];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
- (void)setupUI {
|
- (void)setupUI {
|
||||||
CGFloat toolBarHeight = 40;
|
self.view.translatesAutoresizingMaskIntoConstraints = NO;
|
||||||
CGFloat bottom = 5;
|
|
||||||
CGFloat buttonSpace = 8;
|
|
||||||
CGFloat eachButtonHeight = (KEYBOARDHEIGHT - toolBarHeight - 10 - 8 * 3 - bottom) / 4;
|
|
||||||
|
|
||||||
UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, SCREEN_WIDTH, 30)];
|
// 按“短边”宽度等比缩放,横屏保持竖屏布局比例
|
||||||
view.backgroundColor = [UIColor redColor];
|
CGFloat portraitWidth = [self kb_portraitWidth];
|
||||||
[self.view addSubview:view];
|
CGFloat keyboardHeight = [self kb_keyboardHeightForWidth:portraitWidth];
|
||||||
}
|
CGFloat screenWidth = CGRectGetWidth([UIScreen mainScreen].bounds);
|
||||||
|
CGFloat outerVerticalInset = KBFit(4.0f);
|
||||||
|
|
||||||
|
NSLayoutConstraint *h = [self.view.heightAnchor constraintEqualToConstant:keyboardHeight];
|
||||||
|
NSLayoutConstraint *w = [self.view.widthAnchor constraintEqualToConstant:screenWidth];
|
||||||
|
self.kb_heightConstraint = h;
|
||||||
|
self.kb_widthConstraint = w;
|
||||||
|
|
||||||
- (void)viewWillLayoutSubviews
|
h.priority = UILayoutPriorityRequired;
|
||||||
{
|
w.priority = UILayoutPriorityRequired;
|
||||||
self.nextKeyboardButton.hidden = !self.needsInputModeSwitchKey;
|
[NSLayoutConstraint activateConstraints:@[h, w]];
|
||||||
[super viewWillLayoutSubviews];
|
// 关闭 UIInputView 自适应(某些系统版本会尝试放大为全屏高度导致冲突)
|
||||||
}
|
if ([self.view isKindOfClass:[UIInputView class]]) {
|
||||||
|
UIInputView *iv = (UIInputView *)self.view;
|
||||||
- (void)textWillChange:(id<UITextInput>)textInput {
|
if ([iv respondsToSelector:@selector(setAllowsSelfSizing:)]) {
|
||||||
// The app is about to change the document's contents. Perform any preparation here.
|
iv.allowsSelfSizing = NO;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)textDidChange:(id<UITextInput>)textInput {
|
|
||||||
// The app has just changed the document's contents, the document context has been updated.
|
|
||||||
|
|
||||||
UIColor *textColor = nil;
|
|
||||||
if (self.textDocumentProxy.keyboardAppearance == UIKeyboardAppearanceDark) {
|
|
||||||
textColor = [UIColor whiteColor];
|
|
||||||
} else {
|
|
||||||
textColor = [UIColor blackColor];
|
|
||||||
}
|
}
|
||||||
[self.nextKeyboardButton setTitleColor:textColor forState:UIControlStateNormal];
|
// 内容容器:横屏时保持竖屏宽度,居中显示
|
||||||
|
[self.view addSubview:self.contentView];
|
||||||
|
[self.contentView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
|
make.centerX.equalTo(self.view);
|
||||||
|
make.bottom.equalTo(self.view);
|
||||||
|
self.contentWidthConstraint = make.width.mas_equalTo(portraitWidth);
|
||||||
|
self.contentHeightConstraint = make.height.mas_equalTo(keyboardHeight);
|
||||||
|
}];
|
||||||
|
|
||||||
|
// 背景图铺底(仅在内容容器内)
|
||||||
|
[self.contentView addSubview:self.bgImageView];
|
||||||
|
[self.bgImageView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
|
make.edges.equalTo(self.contentView);
|
||||||
|
}];
|
||||||
|
// 预置功能面板(默认隐藏),与键盘区域共享相同布局
|
||||||
|
self.functionView.hidden = YES;
|
||||||
|
[self.contentView addSubview:self.functionView];
|
||||||
|
[self.functionView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
|
make.edges.equalTo(self.contentView);
|
||||||
|
}];
|
||||||
|
|
||||||
|
[self.contentView addSubview:self.keyBoardMainView];
|
||||||
|
[self.keyBoardMainView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
|
make.edges.equalTo(self.contentView);
|
||||||
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#pragma mark - Private
|
||||||
|
|
||||||
|
// MARK: - Suggestions
|
||||||
|
|
||||||
|
- (void)kb_updateCurrentWordWithInsertedText:(NSString *)text {
|
||||||
|
if (text.length == 0) { return; }
|
||||||
|
if ([self kb_isAlphabeticString:text]) {
|
||||||
|
NSString *current = self.currentWord ?: @"";
|
||||||
|
self.currentWord = [current stringByAppendingString:text];
|
||||||
|
self.suppressSuggestions = NO;
|
||||||
|
[self kb_updateSuggestionsForCurrentWord];
|
||||||
|
} else {
|
||||||
|
[self kb_clearCurrentWord];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)kb_clearCurrentWord {
|
||||||
|
self.currentWord = @"";
|
||||||
|
[self.keyBoardMainView kb_setSuggestions:@[]];
|
||||||
|
self.suppressSuggestions = NO;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)kb_scheduleContextRefreshResetSuppression:(BOOL)resetSuppression {
|
||||||
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
|
[self kb_refreshCurrentWordFromDocumentContextResetSuppression:resetSuppression];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)kb_refreshCurrentWordFromDocumentContextResetSuppression:(BOOL)resetSuppression {
|
||||||
|
NSString *context = self.textDocumentProxy.documentContextBeforeInput ?: @"";
|
||||||
|
NSString *word = [self kb_extractTrailingWordFromContext:context];
|
||||||
|
self.currentWord = word ?: @"";
|
||||||
|
if (resetSuppression) {
|
||||||
|
self.suppressSuggestions = NO;
|
||||||
|
}
|
||||||
|
[self kb_updateSuggestionsForCurrentWord];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSString *)kb_extractTrailingWordFromContext:(NSString *)context {
|
||||||
|
if (context.length == 0) { return @""; }
|
||||||
|
static NSCharacterSet *letters = nil;
|
||||||
|
static dispatch_once_t onceToken;
|
||||||
|
dispatch_once(&onceToken, ^{
|
||||||
|
letters = [NSCharacterSet characterSetWithCharactersInString:@"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"];
|
||||||
|
});
|
||||||
|
|
||||||
|
NSInteger idx = (NSInteger)context.length - 1;
|
||||||
|
while (idx >= 0) {
|
||||||
|
unichar ch = [context characterAtIndex:(NSUInteger)idx];
|
||||||
|
if (![letters characterIsMember:ch]) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
idx -= 1;
|
||||||
|
}
|
||||||
|
NSUInteger start = (NSUInteger)(idx + 1);
|
||||||
|
if (start >= context.length) { return @""; }
|
||||||
|
return [context substringFromIndex:start];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (BOOL)kb_isAlphabeticString:(NSString *)text {
|
||||||
|
if (text.length == 0) { return NO; }
|
||||||
|
static NSCharacterSet *letters = nil;
|
||||||
|
static dispatch_once_t onceToken;
|
||||||
|
dispatch_once(&onceToken, ^{
|
||||||
|
letters = [NSCharacterSet characterSetWithCharactersInString:@"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"];
|
||||||
|
});
|
||||||
|
for (NSUInteger i = 0; i < text.length; i++) {
|
||||||
|
if (![letters characterIsMember:[text characterAtIndex:i]]) {
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return YES;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)kb_updateSuggestionsForCurrentWord {
|
||||||
|
NSString *prefix = self.currentWord ?: @"";
|
||||||
|
if (prefix.length == 0) {
|
||||||
|
[self.keyBoardMainView kb_setSuggestions:@[]];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (self.suppressSuggestions) {
|
||||||
|
[self.keyBoardMainView kb_setSuggestions:@[]];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
NSArray<NSString *> *items = [self.suggestionEngine suggestionsForPrefix:prefix limit:5];
|
||||||
|
NSArray<NSString *> *cased = [self kb_applyCaseToSuggestions:items prefix:prefix];
|
||||||
|
[self.keyBoardMainView kb_setSuggestions:cased];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSArray<NSString *> *)kb_applyCaseToSuggestions:(NSArray<NSString *> *)items prefix:(NSString *)prefix {
|
||||||
|
if (items.count == 0 || prefix.length == 0) { return items; }
|
||||||
|
BOOL allUpper = [prefix isEqualToString:prefix.uppercaseString];
|
||||||
|
BOOL firstUpper = [[prefix substringToIndex:1] isEqualToString:[[prefix substringToIndex:1] uppercaseString]];
|
||||||
|
|
||||||
|
if (!allUpper && !firstUpper) { return items; }
|
||||||
|
|
||||||
|
NSMutableArray<NSString *> *result = [NSMutableArray arrayWithCapacity:items.count];
|
||||||
|
for (NSString *word in items) {
|
||||||
|
if (allUpper) {
|
||||||
|
[result addObject:word.uppercaseString];
|
||||||
|
} else {
|
||||||
|
NSString *first = [[word substringToIndex:1] uppercaseString];
|
||||||
|
NSString *rest = (word.length > 1) ? [word substringFromIndex:1] : @"";
|
||||||
|
[result addObject:[first stringByAppendingString:rest]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 切换显示功能面板/键盘主视图
|
||||||
|
- (void)showFunctionPanel:(BOOL)show {
|
||||||
|
// 简单显隐切换,复用相同的布局区域
|
||||||
|
self.functionView.hidden = !show;
|
||||||
|
self.keyBoardMainView.hidden = show;
|
||||||
|
|
||||||
|
if (show) {
|
||||||
|
[[KBMaiPointReporter sharedReporter] reportPageExposureWithEventName:@"enter_keyboard_function_panel"
|
||||||
|
pageId:@"keyboard_function_panel"
|
||||||
|
extra:nil
|
||||||
|
completion:nil];
|
||||||
|
[self hideSubscriptionPanel];
|
||||||
|
} else {
|
||||||
|
[[KBMaiPointReporter sharedReporter] reportPageExposureWithEventName:@"enter_keyboard_main_panel"
|
||||||
|
pageId:@"keyboard_main_panel"
|
||||||
|
extra:nil
|
||||||
|
completion:nil];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 可选:把当前显示的视图置顶,避免层级遮挡
|
||||||
|
if (show) {
|
||||||
|
[self.contentView bringSubviewToFront:self.functionView];
|
||||||
|
} else {
|
||||||
|
[self.contentView bringSubviewToFront:self.keyBoardMainView];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 显示/隐藏设置页(高度与 keyBoardMainView 一致),右侧滑入/滑出
|
||||||
|
- (void)showSettingView:(BOOL)show {
|
||||||
|
if (show) {
|
||||||
|
[[KBMaiPointReporter sharedReporter] reportPageExposureWithEventName:@"enter_keyboard_settings"
|
||||||
|
pageId:@"keyboard_settings"
|
||||||
|
extra:nil
|
||||||
|
completion:nil];
|
||||||
|
// if (!self.settingView) {
|
||||||
|
self.settingView = [[KBSettingView alloc] init];
|
||||||
|
self.settingView.hidden = YES;
|
||||||
|
[self.contentView addSubview:self.settingView];
|
||||||
|
[self.settingView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
|
// 与键盘主视图完全等同的区域,保证高度、宽度一致
|
||||||
|
make.edges.equalTo(self.contentView);
|
||||||
|
}];
|
||||||
|
[self.settingView.backButton addTarget:self action:@selector(onTapSettingsBack) forControlEvents:UIControlEventTouchUpInside];
|
||||||
|
// }
|
||||||
|
[self.contentView bringSubviewToFront:self.settingView];
|
||||||
|
// 以 keyBoardMainView 的实际宽度为准,避免首次添加时 self.view 宽度尚未计算
|
||||||
|
[self.contentView layoutIfNeeded];
|
||||||
|
CGFloat w = CGRectGetWidth(self.keyBoardMainView.bounds);
|
||||||
|
if (w <= 0) { w = CGRectGetWidth(self.contentView.bounds); }
|
||||||
|
if (w <= 0) { w = [self kb_portraitWidth]; }
|
||||||
|
self.settingView.transform = CGAffineTransformMakeTranslation(w, 0);
|
||||||
|
self.settingView.hidden = NO;
|
||||||
|
[UIView animateWithDuration:0.25 delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{
|
||||||
|
self.settingView.transform = CGAffineTransformIdentity;
|
||||||
|
} completion:nil];
|
||||||
|
} else {
|
||||||
|
if (!self.settingView || self.settingView.hidden) return;
|
||||||
|
CGFloat w = CGRectGetWidth(self.keyBoardMainView.bounds);
|
||||||
|
if (w <= 0) { w = CGRectGetWidth(self.contentView.bounds); }
|
||||||
|
if (w <= 0) { w = [self kb_portraitWidth]; }
|
||||||
|
[UIView animateWithDuration:0.22 delay:0 options:UIViewAnimationOptionCurveEaseIn animations:^{
|
||||||
|
self.settingView.transform = CGAffineTransformMakeTranslation(w, 0);
|
||||||
|
} completion:^(BOOL finished) {
|
||||||
|
self.settingView.hidden = YES;
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)showSubscriptionPanel {
|
||||||
|
// 1) 先判断权限:未开启“完全访问”则走引导逻辑
|
||||||
|
if (![[KBFullAccessManager shared] hasFullAccess]) {
|
||||||
|
// 未开启完全访问:保持原有引导路径
|
||||||
|
// [KBHUD showInfo:KBLocalized(@"处理中…")];
|
||||||
|
[[KBFullAccessManager shared] ensureFullAccessOrGuideInView:self.view];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 点击充值要先判断是否登录
|
||||||
|
// 2) 权限没问题,再判断是否登录:未登录 -> 直接拉起主 App,由主 App 负责完成登录
|
||||||
|
if (!KBAuthManager.shared.isLoggedIn) {
|
||||||
|
NSString *schemeStr = [NSString stringWithFormat:@"%@://login?src=keyboard", KB_APP_SCHEME];
|
||||||
|
NSURL *scheme = [NSURL URLWithString:schemeStr];
|
||||||
|
// 从当前视图作为起点,通过响应链找到 UIApplication 再调起主 App
|
||||||
|
BOOL ok = [KBHostAppLauncher openHostAppURL:scheme fromResponder:self.view];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
[[KBMaiPointReporter sharedReporter] reportPageExposureWithEventName:@"enter_keyboard_subscription_panel"
|
||||||
|
pageId:@"keyboard_subscription_panel"
|
||||||
|
extra:nil
|
||||||
|
completion:nil];
|
||||||
|
[self showFunctionPanel:NO];
|
||||||
|
KBKeyboardSubscriptionView *panel = self.subscriptionView;
|
||||||
|
if (!panel.superview) {
|
||||||
|
panel.hidden = YES;
|
||||||
|
[self.contentView addSubview:panel];
|
||||||
|
[panel mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
|
make.edges.equalTo(self.contentView);
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
[self.contentView bringSubviewToFront:panel];
|
||||||
|
panel.hidden = NO;
|
||||||
|
panel.alpha = 0.0;
|
||||||
|
CGFloat height = CGRectGetHeight(self.contentView.bounds);
|
||||||
|
if (height <= 0) { height = 260; }
|
||||||
|
panel.transform = CGAffineTransformMakeTranslation(0, height);
|
||||||
|
[panel refreshProductsIfNeeded];
|
||||||
|
[UIView animateWithDuration:0.25 delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{
|
||||||
|
panel.alpha = 1.0;
|
||||||
|
panel.transform = CGAffineTransformIdentity;
|
||||||
|
} completion:nil];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)hideSubscriptionPanel {
|
||||||
|
if (!self.subscriptionView || self.subscriptionView.hidden) { return; }
|
||||||
|
CGFloat height = CGRectGetHeight(self.subscriptionView.bounds);
|
||||||
|
if (height <= 0) { height = CGRectGetHeight(self.contentView.bounds); }
|
||||||
|
KBKeyboardSubscriptionView *panel = self.subscriptionView;
|
||||||
|
[UIView animateWithDuration:0.22 delay:0 options:UIViewAnimationOptionCurveEaseIn animations:^{
|
||||||
|
panel.alpha = 0.0;
|
||||||
|
panel.transform = CGAffineTransformMakeTranslation(0, height);
|
||||||
|
} completion:^(BOOL finished) {
|
||||||
|
panel.hidden = YES;
|
||||||
|
panel.alpha = 1.0;
|
||||||
|
panel.transform = CGAffineTransformIdentity;
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - KBKeyBoardMainViewDelegate
|
||||||
|
- (void)keyBoardMainView:(KBKeyBoardMainView *)keyBoardMainView didTapKey:(KBKey *)key {
|
||||||
|
switch (key.type) {
|
||||||
|
case KBKeyTypeCharacter: {
|
||||||
|
[[KBBackspaceUndoManager shared] registerNonClearAction];
|
||||||
|
NSString *text = key.output ?: key.title ?: @"";
|
||||||
|
[self.textDocumentProxy insertText:text];
|
||||||
|
[self kb_updateCurrentWordWithInsertedText:text];
|
||||||
|
[[KBInputBufferManager shared] appendText:text];
|
||||||
|
} break;
|
||||||
|
case KBKeyTypeBackspace:
|
||||||
|
[[KBInputBufferManager shared] refreshFromProxyIfPossible:self.textDocumentProxy];
|
||||||
|
[[KBInputBufferManager shared] prepareSnapshotForDeleteWithContextBefore:self.textDocumentProxy.documentContextBeforeInput
|
||||||
|
after:self.textDocumentProxy.documentContextAfterInput];
|
||||||
|
[[KBBackspaceUndoManager shared] captureAndDeleteBackwardFromProxy:self.textDocumentProxy count:1];
|
||||||
|
[self kb_scheduleContextRefreshResetSuppression:NO];
|
||||||
|
[[KBInputBufferManager shared] applyHoldDeleteCount:1];
|
||||||
|
break;
|
||||||
|
case KBKeyTypeSpace:
|
||||||
|
[[KBBackspaceUndoManager shared] registerNonClearAction];
|
||||||
|
[self.textDocumentProxy insertText:@" "];
|
||||||
|
[self kb_clearCurrentWord];
|
||||||
|
[[KBInputBufferManager shared] appendText:@" "];
|
||||||
|
break;
|
||||||
|
case KBKeyTypeReturn:
|
||||||
|
[[KBBackspaceUndoManager shared] registerNonClearAction];
|
||||||
|
[self.textDocumentProxy insertText:@"\n"];
|
||||||
|
[self kb_clearCurrentWord];
|
||||||
|
[[KBInputBufferManager shared] appendText:@"\n"];
|
||||||
|
break;
|
||||||
|
case KBKeyTypeGlobe:
|
||||||
|
[self advanceToNextInputMode]; break;
|
||||||
|
case KBKeyTypeCustom:
|
||||||
|
[[KBBackspaceUndoManager shared] registerNonClearAction];
|
||||||
|
// 点击自定义键切换到功能面板
|
||||||
|
[self showFunctionPanel:YES];
|
||||||
|
[self kb_clearCurrentWord];
|
||||||
|
break;
|
||||||
|
case KBKeyTypeModeChange:
|
||||||
|
case KBKeyTypeShift:
|
||||||
|
// 这些已在 KBKeyBoardMainView/KBKeyboardView 内部处理
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)keyBoardMainView:(KBKeyBoardMainView *)keyBoardMainView didTapToolActionAtIndex:(NSInteger)index {
|
||||||
|
NSDictionary *extra = @{@"index": @(index)};
|
||||||
|
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_keyboard_toolbar_action"
|
||||||
|
pageId:@"keyboard_main_panel"
|
||||||
|
elementId:@"toolbar_action"
|
||||||
|
extra:extra
|
||||||
|
completion:nil];
|
||||||
|
if (index == 0) {
|
||||||
|
[self showFunctionPanel:YES];
|
||||||
|
[self kb_clearCurrentWord];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
[self showFunctionPanel:NO];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)keyBoardMainViewDidTapSettings:(KBKeyBoardMainView *)keyBoardMainView {
|
||||||
|
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_keyboard_settings_btn"
|
||||||
|
pageId:@"keyboard_main_panel"
|
||||||
|
elementId:@"settings_btn"
|
||||||
|
extra:nil
|
||||||
|
completion:nil];
|
||||||
|
[self showSettingView:YES];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)keyBoardMainView:(KBKeyBoardMainView *)keyBoardMainView didSelectEmoji:(NSString *)emoji {
|
||||||
|
if (emoji.length == 0) { return; }
|
||||||
|
[[KBBackspaceUndoManager shared] registerNonClearAction];
|
||||||
|
[self.textDocumentProxy insertText:emoji];
|
||||||
|
[self kb_clearCurrentWord];
|
||||||
|
[[KBInputBufferManager shared] appendText:emoji];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)keyBoardMainViewDidTapUndo:(KBKeyBoardMainView *)keyBoardMainView {
|
||||||
|
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_keyboard_undo_btn"
|
||||||
|
pageId:@"keyboard_main_panel"
|
||||||
|
elementId:@"undo_btn"
|
||||||
|
extra:nil
|
||||||
|
completion:nil];
|
||||||
|
[[KBBackspaceUndoManager shared] performUndoFromResponder:self.view];
|
||||||
|
[self kb_scheduleContextRefreshResetSuppression:YES];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)keyBoardMainViewDidTapEmojiSearch:(KBKeyBoardMainView *)keyBoardMainView {
|
||||||
|
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_keyboard_emoji_search_btn"
|
||||||
|
pageId:@"keyboard_main_panel"
|
||||||
|
elementId:@"emoji_search_btn"
|
||||||
|
extra:nil
|
||||||
|
completion:nil];
|
||||||
|
[KBHUD showInfo:KBLocalized(@"Search coming soon")];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)keyBoardMainView:(KBKeyBoardMainView *)keyBoardMainView didSelectSuggestion:(NSString *)suggestion {
|
||||||
|
if (suggestion.length == 0) { return; }
|
||||||
|
NSDictionary *extra = @{@"suggestion_len": @(suggestion.length)};
|
||||||
|
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_keyboard_suggestion_item"
|
||||||
|
pageId:@"keyboard_main_panel"
|
||||||
|
elementId:@"suggestion_item"
|
||||||
|
extra:extra
|
||||||
|
completion:nil];
|
||||||
|
[[KBBackspaceUndoManager shared] registerNonClearAction];
|
||||||
|
NSString *current = self.currentWord ?: @"";
|
||||||
|
if (current.length > 0) {
|
||||||
|
for (NSUInteger i = 0; i < current.length; i++) {
|
||||||
|
[self.textDocumentProxy deleteBackward];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
[self.textDocumentProxy insertText:suggestion];
|
||||||
|
self.currentWord = suggestion;
|
||||||
|
[self.suggestionEngine recordSelection:suggestion];
|
||||||
|
self.suppressSuggestions = YES;
|
||||||
|
[self.keyBoardMainView kb_setSuggestions:@[]];
|
||||||
|
[[KBInputBufferManager shared] replaceTailWithText:suggestion deleteCount:current.length];
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - KBFunctionViewDelegate
|
||||||
|
- (void)functionView:(KBFunctionView *)functionView didTapToolActionAtIndex:(NSInteger)index {
|
||||||
|
// 需求:当 index == 0 时,切回键盘主视图
|
||||||
|
if (index == 0) {
|
||||||
|
[self showFunctionPanel:NO];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
- (void)functionView:(KBFunctionView *_Nullable)functionView didRightTapToolActionAtIndex:(NSInteger)index{
|
||||||
|
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_keyboard_function_right_action"
|
||||||
|
pageId:@"keyboard_function_panel"
|
||||||
|
elementId:@"right_action"
|
||||||
|
extra:@{@"action": @"login_or_recharge"}
|
||||||
|
completion:nil];
|
||||||
|
if (!KBAuthManager.shared.isLoggedIn) {
|
||||||
|
NSString *schemeStr = [NSString stringWithFormat:@"%@://login?src=keyboard", KB_APP_SCHEME];
|
||||||
|
NSURL *scheme = [NSURL URLWithString:schemeStr];
|
||||||
|
// 从当前视图作为起点,通过响应链找到 UIApplication 再调起主 App
|
||||||
|
BOOL ok = [KBHostAppLauncher openHostAppURL:scheme fromResponder:self.view];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
NSString *schemeStr = [NSString stringWithFormat:@"%@://recharge?src=keyboard", KB_APP_SCHEME];
|
||||||
|
NSURL *scheme = [NSURL URLWithString:schemeStr];
|
||||||
|
//
|
||||||
|
// if (!ul && !scheme) { return; }
|
||||||
|
//
|
||||||
|
// 从当前视图作为起点,通过响应链找到 UIApplication 再调起主 App
|
||||||
|
BOOL ok = [KBHostAppLauncher openHostAppURL:scheme fromResponder:self.view];
|
||||||
|
|
||||||
|
if (!ok) {
|
||||||
|
// 失败兜底:给个文案提示
|
||||||
|
// 比如:请回到桌面手动打开 XXX App 进行设置/充值
|
||||||
|
[KBHUD showInfo:@"请回到桌面手动打开App进行充值"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)functionViewDidRequestSubscription:(KBFunctionView *)functionView {
|
||||||
|
[self showSubscriptionPanel];
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - KBKeyboardSubscriptionViewDelegate
|
||||||
|
|
||||||
|
- (void)subscriptionViewDidTapClose:(KBKeyboardSubscriptionView *)view {
|
||||||
|
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_keyboard_subscription_close_btn"
|
||||||
|
pageId:@"keyboard_subscription_panel"
|
||||||
|
elementId:@"close_btn"
|
||||||
|
extra:nil
|
||||||
|
completion:nil];
|
||||||
|
[self hideSubscriptionPanel];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)subscriptionView:(KBKeyboardSubscriptionView *)view didTapPurchaseForProduct:(KBKeyboardSubscriptionProduct *)product {
|
||||||
|
NSMutableDictionary *extra = [NSMutableDictionary dictionary];
|
||||||
|
if ([product.productId isKindOfClass:NSString.class] && product.productId.length > 0) {
|
||||||
|
extra[@"product_id"] = product.productId;
|
||||||
|
}
|
||||||
|
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_keyboard_subscription_product_btn"
|
||||||
|
pageId:@"keyboard_subscription_panel"
|
||||||
|
elementId:@"product_btn"
|
||||||
|
extra:extra.copy
|
||||||
|
completion:nil];
|
||||||
|
[self hideSubscriptionPanel];
|
||||||
|
[self kb_openRechargeForProduct:product];
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - lazy
|
||||||
|
- (KBKeyBoardMainView *)keyBoardMainView{
|
||||||
|
if (!_keyBoardMainView) {
|
||||||
|
_keyBoardMainView = [[KBKeyBoardMainView alloc] init];
|
||||||
|
_keyBoardMainView.delegate = self;
|
||||||
|
}
|
||||||
|
return _keyBoardMainView;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (KBFunctionView *)functionView{
|
||||||
|
if (!_functionView) {
|
||||||
|
_functionView = [[KBFunctionView alloc] init];
|
||||||
|
_functionView.delegate = self; // 监听功能面板顶部Bar点击
|
||||||
|
}
|
||||||
|
return _functionView;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (KBSettingView *)settingView {
|
||||||
|
if (!_settingView) {
|
||||||
|
_settingView = [[KBSettingView alloc] init];
|
||||||
|
}
|
||||||
|
return _settingView;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (KBKeyboardSubscriptionView *)subscriptionView {
|
||||||
|
if (!_subscriptionView) {
|
||||||
|
_subscriptionView = [[KBKeyboardSubscriptionView alloc] init];
|
||||||
|
_subscriptionView.delegate = self;
|
||||||
|
_subscriptionView.hidden = YES;
|
||||||
|
_subscriptionView.alpha = 0.0;
|
||||||
|
}
|
||||||
|
return _subscriptionView;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#pragma mark - Actions
|
||||||
|
|
||||||
|
- (void)kb_openRechargeForProduct:(KBKeyboardSubscriptionProduct *)product {
|
||||||
|
if (![product isKindOfClass:KBKeyboardSubscriptionProduct.class] || product.productId.length == 0) {
|
||||||
|
[KBHUD showInfo:KBLocalized(@"Product unavailable")];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
NSString *encodedId = [self.class kb_urlEncodedString:product.productId];
|
||||||
|
NSString *title = [product displayTitle];
|
||||||
|
NSString *encodedTitle = [self.class kb_urlEncodedString:title];
|
||||||
|
NSMutableArray<NSString *> *params = [NSMutableArray arrayWithObjects:@"autoPay=1", @"prefill=1", nil];
|
||||||
|
if (encodedId.length) {
|
||||||
|
[params addObject:[NSString stringWithFormat:@"productId=%@", encodedId]];
|
||||||
|
}
|
||||||
|
if (encodedTitle.length) {
|
||||||
|
[params addObject:[NSString stringWithFormat:@"productTitle=%@", encodedTitle]];
|
||||||
|
}
|
||||||
|
NSString *query = [params componentsJoinedByString:@"&"];
|
||||||
|
NSString *urlString = [NSString stringWithFormat:@"%@://recharge?src=keyboard&%@", KB_APP_SCHEME, query];
|
||||||
|
NSURL *scheme = [NSURL URLWithString:urlString];
|
||||||
|
BOOL success = [KBHostAppLauncher openHostAppURL:scheme fromResponder:self.view];
|
||||||
|
if (!success) {
|
||||||
|
[KBHUD showInfo:KBLocalized(@"Please open the App to finish purchase")];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
+ (NSString *)kb_urlEncodedString:(NSString *)value {
|
||||||
|
if (value.length == 0) { return @""; }
|
||||||
|
NSString *reserved = @"!*'();:@&=+$,/?%#[]";
|
||||||
|
NSMutableCharacterSet *allowed = [[NSCharacterSet URLQueryAllowedCharacterSet] mutableCopy];
|
||||||
|
[allowed removeCharactersInString:reserved];
|
||||||
|
return [value stringByAddingPercentEncodingWithAllowedCharacters:allowed] ?: @"";
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)onTapSettingsBack {
|
||||||
|
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_keyboard_settings_back_btn"
|
||||||
|
pageId:@"keyboard_settings"
|
||||||
|
elementId:@"back_btn"
|
||||||
|
extra:nil
|
||||||
|
completion:nil];
|
||||||
|
[self showSettingView:NO];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)dealloc {
|
||||||
|
CFNotificationCenterRemoveObserver(CFNotificationCenterGetDarwinNotifyCenter(),
|
||||||
|
(__bridge const void *)(self),
|
||||||
|
(__bridge CFStringRef)KBDarwinSkinInstallRequestNotification,
|
||||||
|
NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 当键盘第一次显示时,尝试唤起主 App 以提示登录(由主 App 决定是否真的弹登录)。
|
||||||
|
- (void)viewDidAppear:(BOOL)animated {
|
||||||
|
[super viewDidAppear:animated];
|
||||||
|
// if (!_kb_didTriggerLoginDeepLinkOnce) {
|
||||||
|
// _kb_didTriggerLoginDeepLinkOnce = YES;
|
||||||
|
// // 仅在未登录时尝试拉起主App登录
|
||||||
|
// if (!KBAuthManager.shared.isLoggedIn) {
|
||||||
|
// [self kb_tryOpenContainerForLoginIfNeeded];
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)viewDidLayoutSubviews {
|
||||||
|
[super viewDidLayoutSubviews];
|
||||||
|
[self kb_updateKeyboardLayoutIfNeeded];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
|
||||||
|
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
|
||||||
|
__weak typeof(self) weakSelf = self;
|
||||||
|
[coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
|
||||||
|
[weakSelf kb_updateKeyboardLayoutIfNeeded];
|
||||||
|
} completion:^(__unused id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
|
||||||
|
[weakSelf kb_updateKeyboardLayoutIfNeeded];
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
//- (void)kb_tryOpenContainerForLoginIfNeeded {
|
||||||
|
// // 使用与主 App 一致的自定义 Scheme
|
||||||
|
// NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"%@@//login?src=keyboard", KB_APP_SCHEME]];
|
||||||
|
// if (!url) return;
|
||||||
|
// KBWeakSelf
|
||||||
|
// [self.extensionContext openURL:url completionHandler:^(__unused BOOL success) {
|
||||||
|
// // 即使失败也不重复尝试;避免打扰。
|
||||||
|
// __unused typeof(weakSelf) selfStrong = weakSelf;
|
||||||
|
// }];
|
||||||
|
//}
|
||||||
|
|
||||||
|
#pragma mark - Theme
|
||||||
|
|
||||||
|
- (void)kb_applyTheme {
|
||||||
|
KBSkinTheme *t = [KBSkinManager shared].current;
|
||||||
|
UIImage *img = [[KBSkinManager shared] currentBackgroundImage];
|
||||||
|
NSLog(@"⌨️[Keyboard] apply theme id=%@ hasBg=%d", t.skinId, (img != nil));
|
||||||
|
self.bgImageView.image = img;
|
||||||
|
BOOL hasImg = (img != nil);
|
||||||
|
self.view.backgroundColor = hasImg ? [UIColor clearColor] : t.keyboardBackground;
|
||||||
|
self.contentView.backgroundColor = hasImg ? [UIColor clearColor] : t.keyboardBackground;
|
||||||
|
self.keyBoardMainView.backgroundColor = hasImg ? [UIColor clearColor] : t.keyboardBackground;
|
||||||
|
// 触发键区按主题重绘
|
||||||
|
if ([self.keyBoardMainView respondsToSelector:@selector(kb_applyTheme)]) {
|
||||||
|
// method declared in KBKeyBoardMainView.h
|
||||||
|
#pragma clang diagnostic push
|
||||||
|
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
||||||
|
[self.keyBoardMainView performSelector:@selector(kb_applyTheme)];
|
||||||
|
#pragma clang diagnostic pop
|
||||||
|
}
|
||||||
|
if ([self.functionView respondsToSelector:@selector(kb_applyTheme)]) {
|
||||||
|
#pragma clang diagnostic push
|
||||||
|
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
||||||
|
[self.functionView performSelector:@selector(kb_applyTheme)];
|
||||||
|
#pragma clang diagnostic pop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)kb_consumePendingShopSkin {
|
||||||
|
KBWeakSelf
|
||||||
|
[KBSkinInstallBridge consumePendingRequestFromBundle:NSBundle.mainBundle
|
||||||
|
completion:^(BOOL success, NSError * _Nullable error) {
|
||||||
|
if (!success) {
|
||||||
|
if (error) {
|
||||||
|
NSLog(@"[Keyboard] skin request failed: %@", error);
|
||||||
|
[KBHUD showInfo:KBLocalized(@"皮肤资源准备失败,请稍后再试")];
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
[weakSelf kb_applyTheme];
|
||||||
|
[KBHUD showInfo:KBLocalized(@"皮肤已更新,立即体验吧")];
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - Layout Helpers
|
||||||
|
|
||||||
|
- (CGFloat)kb_portraitWidth {
|
||||||
|
CGSize s = [UIScreen mainScreen].bounds.size;
|
||||||
|
return MIN(s.width, s.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
- (CGFloat)kb_keyboardHeightForWidth:(CGFloat)width {
|
||||||
|
if (width <= 0) { width = KB_DESIGN_WIDTH; }
|
||||||
|
return kKBKeyboardBaseHeight * (width / KB_DESIGN_WIDTH);
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)kb_updateKeyboardLayoutIfNeeded {
|
||||||
|
CGFloat portraitWidth = [self kb_portraitWidth];
|
||||||
|
CGFloat keyboardHeight = [self kb_keyboardHeightForWidth:portraitWidth];
|
||||||
|
CGFloat containerWidth = CGRectGetWidth(self.view.superview.bounds);
|
||||||
|
if (containerWidth <= 0) {
|
||||||
|
containerWidth = CGRectGetWidth(self.view.window.bounds);
|
||||||
|
}
|
||||||
|
if (containerWidth <= 0) {
|
||||||
|
containerWidth = CGRectGetWidth([UIScreen mainScreen].bounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOL widthChanged = (fabs(self.kb_lastPortraitWidth - portraitWidth) >= 0.5);
|
||||||
|
BOOL heightChanged = (fabs(self.kb_lastKeyboardHeight - keyboardHeight) >= 0.5);
|
||||||
|
if (!widthChanged && !heightChanged && containerWidth > 0 && self.kb_widthConstraint.constant == containerWidth) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.kb_lastPortraitWidth = portraitWidth;
|
||||||
|
self.kb_lastKeyboardHeight = keyboardHeight;
|
||||||
|
|
||||||
|
if (self.kb_heightConstraint) {
|
||||||
|
self.kb_heightConstraint.constant = keyboardHeight;
|
||||||
|
}
|
||||||
|
if (containerWidth > 0 && self.kb_widthConstraint) {
|
||||||
|
self.kb_widthConstraint.constant = containerWidth;
|
||||||
|
}
|
||||||
|
if (self.contentWidthConstraint) {
|
||||||
|
[self.contentWidthConstraint setOffset:portraitWidth];
|
||||||
|
}
|
||||||
|
if (self.contentHeightConstraint) {
|
||||||
|
[self.contentHeightConstraint setOffset:keyboardHeight];
|
||||||
|
}
|
||||||
|
[self.view layoutIfNeeded];
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - Lazy
|
||||||
|
|
||||||
|
- (UIView *)contentView {
|
||||||
|
if (!_contentView) {
|
||||||
|
_contentView = [[UIView alloc] init];
|
||||||
|
_contentView.backgroundColor = [UIColor clearColor];
|
||||||
|
}
|
||||||
|
return _contentView;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (UIImageView *)bgImageView {
|
||||||
|
if (!_bgImageView) {
|
||||||
|
_bgImageView = [[UIImageView alloc] init];
|
||||||
|
_bgImageView.contentMode = UIViewContentModeScaleAspectFill;
|
||||||
|
_bgImageView.clipsToBounds = YES;
|
||||||
|
}
|
||||||
|
return _bgImageView;
|
||||||
|
}
|
||||||
@end
|
@end
|
||||||
|
|||||||
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
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
//
|
|
||||||
// UIViewController+MASAdditions.h
|
|
||||||
// Masonry
|
|
||||||
//
|
|
||||||
// Created by Craig Siemens on 2015-06-23.
|
|
||||||
//
|
|
||||||
//
|
|
||||||
|
|
||||||
#import "MASUtilities.h"
|
|
||||||
#import "MASConstraintMaker.h"
|
|
||||||
#import "MASViewAttribute.h"
|
|
||||||
|
|
||||||
#ifdef MAS_VIEW_CONTROLLER
|
|
||||||
|
|
||||||
@interface MAS_VIEW_CONTROLLER (MASAdditions)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* following properties return a new MASViewAttribute with appropriate UILayoutGuide and NSLayoutAttribute
|
|
||||||
*/
|
|
||||||
@property (nonatomic, strong, readonly) MASViewAttribute *mas_topLayoutGuide;
|
|
||||||
@property (nonatomic, strong, readonly) MASViewAttribute *mas_bottomLayoutGuide;
|
|
||||||
@property (nonatomic, strong, readonly) MASViewAttribute *mas_topLayoutGuideTop;
|
|
||||||
@property (nonatomic, strong, readonly) MASViewAttribute *mas_topLayoutGuideBottom;
|
|
||||||
@property (nonatomic, strong, readonly) MASViewAttribute *mas_bottomLayoutGuideTop;
|
|
||||||
@property (nonatomic, strong, readonly) MASViewAttribute *mas_bottomLayoutGuideBottom;
|
|
||||||
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
//
|
|
||||||
// UIViewController+MASAdditions.m
|
|
||||||
// Masonry
|
|
||||||
//
|
|
||||||
// Created by Craig Siemens on 2015-06-23.
|
|
||||||
//
|
|
||||||
//
|
|
||||||
|
|
||||||
#import "ViewController+MASAdditions.h"
|
|
||||||
|
|
||||||
#ifdef MAS_VIEW_CONTROLLER
|
|
||||||
|
|
||||||
@implementation MAS_VIEW_CONTROLLER (MASAdditions)
|
|
||||||
|
|
||||||
- (MASViewAttribute *)mas_topLayoutGuide {
|
|
||||||
return [[MASViewAttribute alloc] initWithView:self.view item:self.topLayoutGuide layoutAttribute:NSLayoutAttributeBottom];
|
|
||||||
}
|
|
||||||
- (MASViewAttribute *)mas_topLayoutGuideTop {
|
|
||||||
return [[MASViewAttribute alloc] initWithView:self.view item:self.topLayoutGuide layoutAttribute:NSLayoutAttributeTop];
|
|
||||||
}
|
|
||||||
- (MASViewAttribute *)mas_topLayoutGuideBottom {
|
|
||||||
return [[MASViewAttribute alloc] initWithView:self.view item:self.topLayoutGuide layoutAttribute:NSLayoutAttributeBottom];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (MASViewAttribute *)mas_bottomLayoutGuide {
|
|
||||||
return [[MASViewAttribute alloc] initWithView:self.view item:self.bottomLayoutGuide layoutAttribute:NSLayoutAttributeTop];
|
|
||||||
}
|
|
||||||
- (MASViewAttribute *)mas_bottomLayoutGuideTop {
|
|
||||||
return [[MASViewAttribute alloc] initWithView:self.view item:self.bottomLayoutGuide layoutAttribute:NSLayoutAttributeTop];
|
|
||||||
}
|
|
||||||
- (MASViewAttribute *)mas_bottomLayoutGuideBottom {
|
|
||||||
return [[MASViewAttribute alloc] initWithView:self.view item:self.bottomLayoutGuide layoutAttribute:NSLayoutAttributeBottom];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1,415 +0,0 @@
|
|||||||
# Masonry [](https://travis-ci.org/SnapKit/Masonry) [](https://coveralls.io/r/SnapKit/Masonry) [](https://github.com/Carthage/Carthage) 
|
|
||||||
|
|
||||||
**Masonry is still actively maintained, we are committed to fixing bugs and merging good quality PRs from the wider community. However if you're using Swift in your project, we recommend using [SnapKit](https://github.com/SnapKit/SnapKit) as it provides better type safety with a simpler API.**
|
|
||||||
|
|
||||||
Masonry is a light-weight layout framework which wraps AutoLayout with a nicer syntax. Masonry has its own layout DSL which provides a chainable way of describing your NSLayoutConstraints which results in layout code that is more concise and readable.
|
|
||||||
Masonry supports iOS and Mac OS X.
|
|
||||||
|
|
||||||
For examples take a look at the **Masonry iOS Examples** project in the Masonry workspace. You will need to run `pod install` after downloading.
|
|
||||||
|
|
||||||
## What's wrong with NSLayoutConstraints?
|
|
||||||
|
|
||||||
Under the hood Auto Layout is a powerful and flexible way of organising and laying out your views. However creating constraints from code is verbose and not very descriptive.
|
|
||||||
Imagine a simple example in which you want to have a view fill its superview but inset by 10 pixels on every side
|
|
||||||
```obj-c
|
|
||||||
UIView *superview = self.view;
|
|
||||||
|
|
||||||
UIView *view1 = [[UIView alloc] init];
|
|
||||||
view1.translatesAutoresizingMaskIntoConstraints = NO;
|
|
||||||
view1.backgroundColor = [UIColor greenColor];
|
|
||||||
[superview addSubview:view1];
|
|
||||||
|
|
||||||
UIEdgeInsets padding = UIEdgeInsetsMake(10, 10, 10, 10);
|
|
||||||
|
|
||||||
[superview addConstraints:@[
|
|
||||||
|
|
||||||
//view1 constraints
|
|
||||||
[NSLayoutConstraint constraintWithItem:view1
|
|
||||||
attribute:NSLayoutAttributeTop
|
|
||||||
relatedBy:NSLayoutRelationEqual
|
|
||||||
toItem:superview
|
|
||||||
attribute:NSLayoutAttributeTop
|
|
||||||
multiplier:1.0
|
|
||||||
constant:padding.top],
|
|
||||||
|
|
||||||
[NSLayoutConstraint constraintWithItem:view1
|
|
||||||
attribute:NSLayoutAttributeLeft
|
|
||||||
relatedBy:NSLayoutRelationEqual
|
|
||||||
toItem:superview
|
|
||||||
attribute:NSLayoutAttributeLeft
|
|
||||||
multiplier:1.0
|
|
||||||
constant:padding.left],
|
|
||||||
|
|
||||||
[NSLayoutConstraint constraintWithItem:view1
|
|
||||||
attribute:NSLayoutAttributeBottom
|
|
||||||
relatedBy:NSLayoutRelationEqual
|
|
||||||
toItem:superview
|
|
||||||
attribute:NSLayoutAttributeBottom
|
|
||||||
multiplier:1.0
|
|
||||||
constant:-padding.bottom],
|
|
||||||
|
|
||||||
[NSLayoutConstraint constraintWithItem:view1
|
|
||||||
attribute:NSLayoutAttributeRight
|
|
||||||
relatedBy:NSLayoutRelationEqual
|
|
||||||
toItem:superview
|
|
||||||
attribute:NSLayoutAttributeRight
|
|
||||||
multiplier:1
|
|
||||||
constant:-padding.right],
|
|
||||||
|
|
||||||
]];
|
|
||||||
```
|
|
||||||
Even with such a simple example the code needed is quite verbose and quickly becomes unreadable when you have more than 2 or 3 views.
|
|
||||||
Another option is to use Visual Format Language (VFL), which is a bit less long winded.
|
|
||||||
However the ASCII type syntax has its own pitfalls and its also a bit harder to animate as `NSLayoutConstraint constraintsWithVisualFormat:` returns an array.
|
|
||||||
|
|
||||||
## Prepare to meet your Maker!
|
|
||||||
|
|
||||||
Heres the same constraints created using MASConstraintMaker
|
|
||||||
|
|
||||||
```obj-c
|
|
||||||
UIEdgeInsets padding = UIEdgeInsetsMake(10, 10, 10, 10);
|
|
||||||
|
|
||||||
[view1 mas_makeConstraints:^(MASConstraintMaker *make) {
|
|
||||||
make.top.equalTo(superview.mas_top).with.offset(padding.top); //with is an optional semantic filler
|
|
||||||
make.left.equalTo(superview.mas_left).with.offset(padding.left);
|
|
||||||
make.bottom.equalTo(superview.mas_bottom).with.offset(-padding.bottom);
|
|
||||||
make.right.equalTo(superview.mas_right).with.offset(-padding.right);
|
|
||||||
}];
|
|
||||||
```
|
|
||||||
Or even shorter
|
|
||||||
|
|
||||||
```obj-c
|
|
||||||
[view1 mas_makeConstraints:^(MASConstraintMaker *make) {
|
|
||||||
make.edges.equalTo(superview).with.insets(padding);
|
|
||||||
}];
|
|
||||||
```
|
|
||||||
|
|
||||||
Also note in the first example we had to add the constraints to the superview `[superview addConstraints:...`.
|
|
||||||
Masonry however will automagically add constraints to the appropriate view.
|
|
||||||
|
|
||||||
Masonry will also call `view1.translatesAutoresizingMaskIntoConstraints = NO;` for you.
|
|
||||||
|
|
||||||
## Not all things are created equal
|
|
||||||
|
|
||||||
> `.equalTo` equivalent to **NSLayoutRelationEqual**
|
|
||||||
|
|
||||||
> `.lessThanOrEqualTo` equivalent to **NSLayoutRelationLessThanOrEqual**
|
|
||||||
|
|
||||||
> `.greaterThanOrEqualTo` equivalent to **NSLayoutRelationGreaterThanOrEqual**
|
|
||||||
|
|
||||||
These three equality constraints accept one argument which can be any of the following:
|
|
||||||
|
|
||||||
#### 1. MASViewAttribute
|
|
||||||
|
|
||||||
```obj-c
|
|
||||||
make.centerX.lessThanOrEqualTo(view2.mas_left);
|
|
||||||
```
|
|
||||||
|
|
||||||
MASViewAttribute | NSLayoutAttribute
|
|
||||||
------------------------- | --------------------------
|
|
||||||
view.mas_left | NSLayoutAttributeLeft
|
|
||||||
view.mas_right | NSLayoutAttributeRight
|
|
||||||
view.mas_top | NSLayoutAttributeTop
|
|
||||||
view.mas_bottom | NSLayoutAttributeBottom
|
|
||||||
view.mas_leading | NSLayoutAttributeLeading
|
|
||||||
view.mas_trailing | NSLayoutAttributeTrailing
|
|
||||||
view.mas_width | NSLayoutAttributeWidth
|
|
||||||
view.mas_height | NSLayoutAttributeHeight
|
|
||||||
view.mas_centerX | NSLayoutAttributeCenterX
|
|
||||||
view.mas_centerY | NSLayoutAttributeCenterY
|
|
||||||
view.mas_baseline | NSLayoutAttributeBaseline
|
|
||||||
|
|
||||||
#### 2. UIView/NSView
|
|
||||||
|
|
||||||
if you want view.left to be greater than or equal to label.left :
|
|
||||||
```obj-c
|
|
||||||
//these two constraints are exactly the same
|
|
||||||
make.left.greaterThanOrEqualTo(label);
|
|
||||||
make.left.greaterThanOrEqualTo(label.mas_left);
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 3. NSNumber
|
|
||||||
|
|
||||||
Auto Layout allows width and height to be set to constant values.
|
|
||||||
if you want to set view to have a minimum and maximum width you could pass a number to the equality blocks:
|
|
||||||
```obj-c
|
|
||||||
//width >= 200 && width <= 400
|
|
||||||
make.width.greaterThanOrEqualTo(@200);
|
|
||||||
make.width.lessThanOrEqualTo(@400)
|
|
||||||
```
|
|
||||||
|
|
||||||
However Auto Layout does not allow alignment attributes such as left, right, centerY etc to be set to constant values.
|
|
||||||
So if you pass a NSNumber for these attributes Masonry will turn these into constraints relative to the view’s superview ie:
|
|
||||||
```obj-c
|
|
||||||
//creates view.left = view.superview.left + 10
|
|
||||||
make.left.lessThanOrEqualTo(@10)
|
|
||||||
```
|
|
||||||
|
|
||||||
Instead of using NSNumber, you can use primitives and structs to build your constraints, like so:
|
|
||||||
```obj-c
|
|
||||||
make.top.mas_equalTo(42);
|
|
||||||
make.height.mas_equalTo(20);
|
|
||||||
make.size.mas_equalTo(CGSizeMake(50, 100));
|
|
||||||
make.edges.mas_equalTo(UIEdgeInsetsMake(10, 0, 10, 0));
|
|
||||||
make.left.mas_equalTo(view).mas_offset(UIEdgeInsetsMake(10, 0, 10, 0));
|
|
||||||
```
|
|
||||||
|
|
||||||
By default, macros which support [autoboxing](https://en.wikipedia.org/wiki/Autoboxing#Autoboxing) are prefixed with `mas_`. Unprefixed versions are available by defining `MAS_SHORTHAND_GLOBALS` before importing Masonry.
|
|
||||||
|
|
||||||
#### 4. NSArray
|
|
||||||
|
|
||||||
An array of a mixture of any of the previous types
|
|
||||||
```obj-c
|
|
||||||
make.height.equalTo(@[view1.mas_height, view2.mas_height]);
|
|
||||||
make.height.equalTo(@[view1, view2]);
|
|
||||||
make.left.equalTo(@[view1, @100, view3.right]);
|
|
||||||
````
|
|
||||||
|
|
||||||
## Learn to prioritize
|
|
||||||
|
|
||||||
> `.priority` allows you to specify an exact priority
|
|
||||||
|
|
||||||
> `.priorityHigh` equivalent to **UILayoutPriorityDefaultHigh**
|
|
||||||
|
|
||||||
> `.priorityMedium` is half way between high and low
|
|
||||||
|
|
||||||
> `.priorityLow` equivalent to **UILayoutPriorityDefaultLow**
|
|
||||||
|
|
||||||
Priorities are can be tacked on to the end of a constraint chain like so:
|
|
||||||
```obj-c
|
|
||||||
make.left.greaterThanOrEqualTo(label.mas_left).with.priorityLow();
|
|
||||||
|
|
||||||
make.top.equalTo(label.mas_top).with.priority(600);
|
|
||||||
```
|
|
||||||
|
|
||||||
## Composition, composition, composition
|
|
||||||
|
|
||||||
Masonry also gives you a few convenience methods which create multiple constraints at the same time. These are called MASCompositeConstraints
|
|
||||||
|
|
||||||
#### edges
|
|
||||||
|
|
||||||
```obj-c
|
|
||||||
// make top, left, bottom, right equal view2
|
|
||||||
make.edges.equalTo(view2);
|
|
||||||
|
|
||||||
// make top = superview.top + 5, left = superview.left + 10,
|
|
||||||
// bottom = superview.bottom - 15, right = superview.right - 20
|
|
||||||
make.edges.equalTo(superview).insets(UIEdgeInsetsMake(5, 10, 15, 20))
|
|
||||||
```
|
|
||||||
|
|
||||||
#### size
|
|
||||||
|
|
||||||
```obj-c
|
|
||||||
// make width and height greater than or equal to titleLabel
|
|
||||||
make.size.greaterThanOrEqualTo(titleLabel)
|
|
||||||
|
|
||||||
// make width = superview.width + 100, height = superview.height - 50
|
|
||||||
make.size.equalTo(superview).sizeOffset(CGSizeMake(100, -50))
|
|
||||||
```
|
|
||||||
|
|
||||||
#### center
|
|
||||||
```obj-c
|
|
||||||
// make centerX and centerY = button1
|
|
||||||
make.center.equalTo(button1)
|
|
||||||
|
|
||||||
// make centerX = superview.centerX - 5, centerY = superview.centerY + 10
|
|
||||||
make.center.equalTo(superview).centerOffset(CGPointMake(-5, 10))
|
|
||||||
```
|
|
||||||
|
|
||||||
You can chain view attributes for increased readability:
|
|
||||||
|
|
||||||
```obj-c
|
|
||||||
// All edges but the top should equal those of the superview
|
|
||||||
make.left.right.and.bottom.equalTo(superview);
|
|
||||||
make.top.equalTo(otherView);
|
|
||||||
```
|
|
||||||
|
|
||||||
## Hold on for dear life
|
|
||||||
|
|
||||||
Sometimes you need modify existing constraints in order to animate or remove/replace constraints.
|
|
||||||
In Masonry there are a few different approaches to updating constraints.
|
|
||||||
|
|
||||||
#### 1. References
|
|
||||||
You can hold on to a reference of a particular constraint by assigning the result of a constraint make expression to a local variable or a class property.
|
|
||||||
You could also reference multiple constraints by storing them away in an array.
|
|
||||||
|
|
||||||
```obj-c
|
|
||||||
// in public/private interface
|
|
||||||
@property (nonatomic, strong) MASConstraint *topConstraint;
|
|
||||||
|
|
||||||
...
|
|
||||||
|
|
||||||
// when making constraints
|
|
||||||
[view1 mas_makeConstraints:^(MASConstraintMaker *make) {
|
|
||||||
self.topConstraint = make.top.equalTo(superview.mas_top).with.offset(padding.top);
|
|
||||||
make.left.equalTo(superview.mas_left).with.offset(padding.left);
|
|
||||||
}];
|
|
||||||
|
|
||||||
...
|
|
||||||
// then later you can call
|
|
||||||
[self.topConstraint uninstall];
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2. mas_updateConstraints
|
|
||||||
Alternatively if you are only updating the constant value of the constraint you can use the convience method `mas_updateConstraints` instead of `mas_makeConstraints`
|
|
||||||
|
|
||||||
```obj-c
|
|
||||||
// this is Apple's recommended place for adding/updating constraints
|
|
||||||
// this method can get called multiple times in response to setNeedsUpdateConstraints
|
|
||||||
// which can be called by UIKit internally or in your code if you need to trigger an update to your constraints
|
|
||||||
- (void)updateConstraints {
|
|
||||||
[self.growingButton mas_updateConstraints:^(MASConstraintMaker *make) {
|
|
||||||
make.center.equalTo(self);
|
|
||||||
make.width.equalTo(@(self.buttonSize.width)).priorityLow();
|
|
||||||
make.height.equalTo(@(self.buttonSize.height)).priorityLow();
|
|
||||||
make.width.lessThanOrEqualTo(self);
|
|
||||||
make.height.lessThanOrEqualTo(self);
|
|
||||||
}];
|
|
||||||
|
|
||||||
//according to apple super should be called at end of method
|
|
||||||
[super updateConstraints];
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. mas_remakeConstraints
|
|
||||||
`mas_updateConstraints` is useful for updating a set of constraints, but doing anything beyond updating constant values can get exhausting. That's where `mas_remakeConstraints` comes in.
|
|
||||||
|
|
||||||
`mas_remakeConstraints` is similar to `mas_updateConstraints`, but instead of updating constant values, it will remove all of its constraints before installing them again. This lets you provide different constraints without having to keep around references to ones which you want to remove.
|
|
||||||
|
|
||||||
```obj-c
|
|
||||||
- (void)changeButtonPosition {
|
|
||||||
[self.button mas_remakeConstraints:^(MASConstraintMaker *make) {
|
|
||||||
make.size.equalTo(self.buttonSize);
|
|
||||||
|
|
||||||
if (topLeft) {
|
|
||||||
make.top.and.left.offset(10);
|
|
||||||
} else {
|
|
||||||
make.bottom.and.right.offset(-10);
|
|
||||||
}
|
|
||||||
}];
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
You can find more detailed examples of all three approaches in the **Masonry iOS Examples** project.
|
|
||||||
|
|
||||||
## When the ^&*!@ hits the fan!
|
|
||||||
|
|
||||||
Laying out your views doesn't always goto plan. So when things literally go pear shaped, you don't want to be looking at console output like this:
|
|
||||||
|
|
||||||
```obj-c
|
|
||||||
Unable to simultaneously satisfy constraints.....blah blah blah....
|
|
||||||
(
|
|
||||||
"<NSLayoutConstraint:0x7189ac0 V:[UILabel:0x7186980(>=5000)]>",
|
|
||||||
"<NSAutoresizingMaskLayoutConstraint:0x839ea20 h=--& v=--& V:[MASExampleDebuggingView:0x7186560(416)]>",
|
|
||||||
"<NSLayoutConstraint:0x7189c70 UILabel:0x7186980.bottom == MASExampleDebuggingView:0x7186560.bottom - 10>",
|
|
||||||
"<NSLayoutConstraint:0x7189560 V:|-(1)-[UILabel:0x7186980] (Names: '|':MASExampleDebuggingView:0x7186560 )>"
|
|
||||||
)
|
|
||||||
|
|
||||||
Will attempt to recover by breaking constraint
|
|
||||||
<NSLayoutConstraint:0x7189ac0 V:[UILabel:0x7186980(>=5000)]>
|
|
||||||
```
|
|
||||||
|
|
||||||
Masonry adds a category to NSLayoutConstraint which overrides the default implementation of `- (NSString *)description`.
|
|
||||||
Now you can give meaningful names to views and constraints, and also easily pick out the constraints created by Masonry.
|
|
||||||
|
|
||||||
which means your console output can now look like this:
|
|
||||||
|
|
||||||
```obj-c
|
|
||||||
Unable to simultaneously satisfy constraints......blah blah blah....
|
|
||||||
(
|
|
||||||
"<NSAutoresizingMaskLayoutConstraint:0x8887740 MASExampleDebuggingView:superview.height == 416>",
|
|
||||||
"<MASLayoutConstraint:ConstantConstraint UILabel:messageLabel.height >= 5000>",
|
|
||||||
"<MASLayoutConstraint:BottomConstraint UILabel:messageLabel.bottom == MASExampleDebuggingView:superview.bottom - 10>",
|
|
||||||
"<MASLayoutConstraint:ConflictingConstraint[0] UILabel:messageLabel.top == MASExampleDebuggingView:superview.top + 1>"
|
|
||||||
)
|
|
||||||
|
|
||||||
Will attempt to recover by breaking constraint
|
|
||||||
<MASLayoutConstraint:ConstantConstraint UILabel:messageLabel.height >= 5000>
|
|
||||||
```
|
|
||||||
|
|
||||||
For an example of how to set this up take a look at the **Masonry iOS Examples** project in the Masonry workspace.
|
|
||||||
|
|
||||||
## Where should I create my constraints?
|
|
||||||
|
|
||||||
```objc
|
|
||||||
@implementation DIYCustomView
|
|
||||||
|
|
||||||
- (id)init {
|
|
||||||
self = [super init];
|
|
||||||
if (!self) return nil;
|
|
||||||
|
|
||||||
// --- Create your views here ---
|
|
||||||
self.button = [[UIButton alloc] init];
|
|
||||||
|
|
||||||
return self;
|
|
||||||
}
|
|
||||||
|
|
||||||
// tell UIKit that you are using AutoLayout
|
|
||||||
+ (BOOL)requiresConstraintBasedLayout {
|
|
||||||
return YES;
|
|
||||||
}
|
|
||||||
|
|
||||||
// this is Apple's recommended place for adding/updating constraints
|
|
||||||
- (void)updateConstraints {
|
|
||||||
|
|
||||||
// --- remake/update constraints here
|
|
||||||
[self.button remakeConstraints:^(MASConstraintMaker *make) {
|
|
||||||
make.width.equalTo(@(self.buttonSize.width));
|
|
||||||
make.height.equalTo(@(self.buttonSize.height));
|
|
||||||
}];
|
|
||||||
|
|
||||||
//according to apple super should be called at end of method
|
|
||||||
[super updateConstraints];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)didTapButton:(UIButton *)button {
|
|
||||||
// --- Do your changes ie change variables that affect your layout etc ---
|
|
||||||
self.buttonSize = CGSize(200, 200);
|
|
||||||
|
|
||||||
// tell constraints they need updating
|
|
||||||
[self setNeedsUpdateConstraints];
|
|
||||||
}
|
|
||||||
|
|
||||||
@end
|
|
||||||
```
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
Use the [orsome](http://www.youtube.com/watch?v=YaIZF8uUTtk) [CocoaPods](http://github.com/CocoaPods/CocoaPods).
|
|
||||||
|
|
||||||
In your Podfile
|
|
||||||
>`pod 'Masonry'`
|
|
||||||
|
|
||||||
If you want to use masonry without all those pesky 'mas_' prefixes. Add #define MAS_SHORTHAND to your prefix.pch before importing Masonry
|
|
||||||
>`#define MAS_SHORTHAND`
|
|
||||||
|
|
||||||
Get busy Masoning
|
|
||||||
>`#import "Masonry.h"`
|
|
||||||
|
|
||||||
## Code Snippets
|
|
||||||
|
|
||||||
Copy the included code snippets to ``~/Library/Developer/Xcode/UserData/CodeSnippets`` to write your masonry blocks at lightning speed!
|
|
||||||
|
|
||||||
`mas_make` -> ` [<#view#> mas_makeConstraints:^(MASConstraintMaker *make) {
|
|
||||||
<#code#>
|
|
||||||
}];`
|
|
||||||
|
|
||||||
`mas_update` -> ` [<#view#> mas_updateConstraints:^(MASConstraintMaker *make) {
|
|
||||||
<#code#>
|
|
||||||
}];`
|
|
||||||
|
|
||||||
`mas_remake` -> ` [<#view#> mas_remakeConstraints:^(MASConstraintMaker *make) {
|
|
||||||
<#code#>
|
|
||||||
}];`
|
|
||||||
|
|
||||||
## Features
|
|
||||||
* Not limited to subset of Auto Layout. Anything NSLayoutConstraint can do, Masonry can do too!
|
|
||||||
* Great debug support, give your views and constraints meaningful names.
|
|
||||||
* Constraints read like sentences.
|
|
||||||
* No crazy macro magic. Masonry won't pollute the global namespace with macros.
|
|
||||||
* Not string or dictionary based and hence you get compile time checking.
|
|
||||||
|
|
||||||
## TODO
|
|
||||||
* Eye candy
|
|
||||||
* Mac example project
|
|
||||||
* More tests and examples
|
|
||||||
|
|
||||||
50
CustomKeyboard/Model/KBKey.h
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
//
|
||||||
|
// KBKey.h
|
||||||
|
// CustomKeyboard
|
||||||
|
//
|
||||||
|
// 简单的键位数据模型,用于描述键盘上的一个键。
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
#import <UIKit/UIKit.h>
|
||||||
|
|
||||||
|
typedef NS_ENUM(NSInteger, KBKeyType) {
|
||||||
|
KBKeyTypeCharacter = 0, // 普通字符输出
|
||||||
|
KBKeyTypeBackspace, // 删除
|
||||||
|
KBKeyTypeShift, // 大小写切换
|
||||||
|
KBKeyTypeModeChange, // 模式切换(如 123/ABC)
|
||||||
|
KBKeyTypeSpace, // 空格
|
||||||
|
KBKeyTypeReturn, // 回车/发送
|
||||||
|
KBKeyTypeGlobe, // 系统地球键
|
||||||
|
KBKeyTypeCustom, // 自定义功能占位(如 AI/Emoji)
|
||||||
|
KBKeyTypeSymbolsToggle // 数字面板内的“#+=/123”切换
|
||||||
|
};
|
||||||
|
|
||||||
|
FOUNDATION_EXPORT NSString * const KBKeyIdentifierEmojiPanel;
|
||||||
|
|
||||||
|
/// 字母键的大小写变体标记(非字母键使用 KBKeyCaseVariantNone)
|
||||||
|
typedef NS_ENUM(NSInteger, KBKeyCaseVariant) {
|
||||||
|
KBKeyCaseVariantNone = 0,
|
||||||
|
KBKeyCaseVariantLower = 1,
|
||||||
|
KBKeyCaseVariantUpper = 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
@interface KBKey : NSObject
|
||||||
|
|
||||||
|
@property (nonatomic, assign) KBKeyType type;
|
||||||
|
@property (nonatomic, copy) NSString *title; // 显示标题
|
||||||
|
@property (nonatomic, copy) NSString *output; // 字符键插入的文本
|
||||||
|
/// 逻辑按键标识,用于皮肤映射(如 @"letter_q" @"space" @"backspace")
|
||||||
|
@property (nonatomic, copy, nullable) NSString *identifier;
|
||||||
|
/// 字母键的大小写变体(便于皮肤为大小写准备不同图)
|
||||||
|
@property (nonatomic, assign) KBKeyCaseVariant caseVariant;
|
||||||
|
|
||||||
|
+ (instancetype)keyWithTitle:(NSString *)title output:(NSString *)output;
|
||||||
|
+ (instancetype)keyWithTitle:(NSString *)title type:(KBKeyType)type;
|
||||||
|
/// 通用构造方法:用于指定 identifier,便于皮肤做精细控制
|
||||||
|
+ (instancetype)keyWithIdentifier:(nullable NSString *)identifier
|
||||||
|
title:(NSString *)title
|
||||||
|
output:(NSString *)output
|
||||||
|
type:(KBKeyType)type;
|
||||||
|
|
||||||
|
@end
|
||||||
43
CustomKeyboard/Model/KBKey.m
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
//
|
||||||
|
// KBKey.m
|
||||||
|
// CustomKeyboard
|
||||||
|
//
|
||||||
|
|
||||||
|
#import "KBKey.h"
|
||||||
|
|
||||||
|
NSString * const KBKeyIdentifierEmojiPanel = @"emoji_panel";
|
||||||
|
|
||||||
|
@implementation KBKey
|
||||||
|
|
||||||
|
+ (instancetype)keyWithTitle:(NSString *)title output:(NSString *)output {
|
||||||
|
KBKey *k = [[KBKey alloc] init];
|
||||||
|
k.type = KBKeyTypeCharacter;
|
||||||
|
k.title = title ?: @"";
|
||||||
|
k.output = output ?: title ?: @"";
|
||||||
|
k.caseVariant = KBKeyCaseVariantNone;
|
||||||
|
return k;
|
||||||
|
}
|
||||||
|
|
||||||
|
+ (instancetype)keyWithTitle:(NSString *)title type:(KBKeyType)type {
|
||||||
|
KBKey *k = [[KBKey alloc] init];
|
||||||
|
k.type = type;
|
||||||
|
k.title = title ?: @"";
|
||||||
|
k.output = @"";
|
||||||
|
k.caseVariant = KBKeyCaseVariantNone;
|
||||||
|
return k;
|
||||||
|
}
|
||||||
|
|
||||||
|
+ (instancetype)keyWithIdentifier:(NSString *)identifier
|
||||||
|
title:(NSString *)title
|
||||||
|
output:(NSString *)output
|
||||||
|
type:(KBKeyType)type {
|
||||||
|
KBKey *k = [[KBKey alloc] init];
|
||||||
|
k.type = type;
|
||||||
|
k.identifier = identifier;
|
||||||
|
k.title = title ?: @"";
|
||||||
|
k.output = output ?: @"";
|
||||||
|
k.caseVariant = KBKeyCaseVariantNone;
|
||||||
|
return k;
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
43
CustomKeyboard/Model/KBKeyboardSubscriptionProduct.h
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
//
|
||||||
|
// KBKeyboardSubscriptionProduct.h
|
||||||
|
// CustomKeyboard
|
||||||
|
//
|
||||||
|
// 订阅商品模型(键盘扩展专用),用于展示与主 App 相同的订阅列表。
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
@interface KBKeyboardSubscriptionProduct : NSObject
|
||||||
|
/// 主键 id
|
||||||
|
@property (nonatomic, assign) NSInteger identifier;
|
||||||
|
/// Apple 商品编号
|
||||||
|
@property (nonatomic, copy, nullable) NSString *productId;
|
||||||
|
/// 商品名称,如 Monthly
|
||||||
|
@property (nonatomic, copy, nullable) NSString *name;
|
||||||
|
/// 单位,如 Subscription
|
||||||
|
@property (nonatomic, copy, nullable) NSString *unit;
|
||||||
|
/// 商品描述
|
||||||
|
@property (nonatomic, copy, nullable) NSString *productDescription;
|
||||||
|
/// 货币符号
|
||||||
|
@property (nonatomic, copy, nullable) NSString *currency;
|
||||||
|
/// 现价
|
||||||
|
@property (nonatomic, assign) double price;
|
||||||
|
/// 原价(如接口未返回,则回退为 price 的 1.25 倍)
|
||||||
|
@property (nonatomic, assign) double originPrice;
|
||||||
|
/// 有效期数值
|
||||||
|
@property (nonatomic, assign) NSInteger durationValue;
|
||||||
|
/// 有效期单位
|
||||||
|
@property (nonatomic, copy, nullable) NSString *durationUnit;
|
||||||
|
|
||||||
|
/// 标题(描述 > name+unit > name > unit)
|
||||||
|
- (NSString *)displayTitle;
|
||||||
|
/// 当前价格文本
|
||||||
|
- (NSString *)priceDisplayText;
|
||||||
|
/// 划线价文本
|
||||||
|
- (nullable NSString *)strikePriceDisplayText;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
55
CustomKeyboard/Model/KBKeyboardSubscriptionProduct.m
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
//
|
||||||
|
// KBKeyboardSubscriptionProduct.m
|
||||||
|
// CustomKeyboard
|
||||||
|
//
|
||||||
|
|
||||||
|
#import "KBKeyboardSubscriptionProduct.h"
|
||||||
|
#import <MJExtension/MJExtension.h>
|
||||||
|
#import "KBLocalizationManager.h"
|
||||||
|
|
||||||
|
@implementation KBKeyboardSubscriptionProduct
|
||||||
|
|
||||||
|
+ (NSDictionary *)mj_replacedKeyFromPropertyName {
|
||||||
|
return @{
|
||||||
|
@"identifier": @"id",
|
||||||
|
@"productDescription": @"description",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSString *)displayTitle {
|
||||||
|
if (self.productDescription.length > 0) {
|
||||||
|
return self.productDescription;
|
||||||
|
}
|
||||||
|
NSString *name = self.name ?: @"";
|
||||||
|
NSString *unit = self.unit ?: @"";
|
||||||
|
if (name.length && unit.length) {
|
||||||
|
return [NSString stringWithFormat:@"%@ %@", name, unit];
|
||||||
|
}
|
||||||
|
if (name.length) { return name; }
|
||||||
|
if (unit.length) { return unit; }
|
||||||
|
if (self.durationValue > 0 && self.durationUnit.length > 0) {
|
||||||
|
return [NSString stringWithFormat:@"%ld %@", (long)self.durationValue, self.durationUnit];
|
||||||
|
}
|
||||||
|
return KBLocalized(@"Subscription");
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSString *)priceDisplayText {
|
||||||
|
double priceValue = self.price;
|
||||||
|
if (priceValue <= 0) {
|
||||||
|
return @"$0.00";
|
||||||
|
}
|
||||||
|
NSString *currency = self.currency.length ? self.currency : @"$";
|
||||||
|
return [NSString stringWithFormat:@"%@%.2f", currency, priceValue];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (nullable NSString *)strikePriceDisplayText {
|
||||||
|
double rawValue = self.originPrice;
|
||||||
|
if (rawValue <= 0 && self.price > 0) {
|
||||||
|
rawValue = self.price * 1.25;
|
||||||
|
}
|
||||||
|
if (rawValue <= 0) { return nil; }
|
||||||
|
NSString *currency = self.currency.length ? self.currency : @"$";
|
||||||
|
return [NSString stringWithFormat:@"%@%.2f", currency, rawValue];
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
69
CustomKeyboard/Network/KBNetworkManager.h
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
//
|
||||||
|
// KBNetworkManager.h
|
||||||
|
// CustomKeyboard
|
||||||
|
//
|
||||||
|
// 轻量网络层封装(扩展安全)。支持 GET/POST(JSON)。
|
||||||
|
// 注意:键盘扩展需要"允许完全访问"后才可联网,
|
||||||
|
// 建议由宿主控制器在确认后调用 `setEnabled:YES` 再发起请求。
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
extern NSErrorDomain const KBNetworkErrorDomain;
|
||||||
|
typedef NS_ERROR_ENUM(KBNetworkErrorDomain, KBNetworkError) {
|
||||||
|
KBNetworkErrorDisabled = 1, // 未启用网络(例如未开启完全访问)
|
||||||
|
KBNetworkErrorInvalidURL = 2,
|
||||||
|
KBNetworkErrorInvalidResponse = 3,
|
||||||
|
KBNetworkErrorDecodeFailed = 4,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// JSON 回调(扩展侧目前很少使用 JSON,可按需扩展)
|
||||||
|
typedef void(^KBNetworkCompletion)(NSDictionary *_Nullable json,
|
||||||
|
NSURLResponse * _Nullable response,
|
||||||
|
NSError * _Nullable error);
|
||||||
|
|
||||||
|
/// 二进制回调:用于下载 zip、图片等原始数据
|
||||||
|
typedef void(^KBNetworkDataCompletion)(NSData *_Nullable data,
|
||||||
|
NSURLResponse *_Nullable response,
|
||||||
|
NSError *_Nullable error);
|
||||||
|
|
||||||
|
@interface KBNetworkManager : NSObject
|
||||||
|
|
||||||
|
/// 单例
|
||||||
|
+ (instancetype)shared;
|
||||||
|
|
||||||
|
/// 是否允许网络(默认为 NO,宿主在合适时机置 YES)
|
||||||
|
@property (atomic, assign, getter=isEnabled) BOOL enabled;
|
||||||
|
|
||||||
|
/// 可选的基础域名,例如 https://api.example.com
|
||||||
|
@property (nonatomic, strong, nullable) NSURL *baseURL;
|
||||||
|
|
||||||
|
/// 全局默认请求头(每次请求会与局部 headers 合并,局部优先)
|
||||||
|
@property (nonatomic, copy) NSDictionary<NSString *, NSString *> *defaultHeaders;
|
||||||
|
|
||||||
|
/// 超时时间(默认 10s)
|
||||||
|
@property (nonatomic, assign) NSTimeInterval timeout;
|
||||||
|
|
||||||
|
/// GET 请求,parameters 会拼到 URL 上
|
||||||
|
- (nullable NSURLSessionDataTask *)GET:(NSString *)path
|
||||||
|
parameters:(nullable NSDictionary *)parameters
|
||||||
|
headers:(nullable NSDictionary<NSString *, NSString *> *)headers
|
||||||
|
completion:(KBNetworkCompletion)completion;
|
||||||
|
|
||||||
|
/// GET 原始二进制数据(不做 JSON 解析)
|
||||||
|
- (nullable NSURLSessionDataTask *)GETData:(NSString *)path
|
||||||
|
parameters:(nullable NSDictionary *)parameters
|
||||||
|
headers:(nullable NSDictionary<NSString *, NSString *> *)headers
|
||||||
|
completion:(KBNetworkDataCompletion)completion;
|
||||||
|
|
||||||
|
/// POST JSON 请求,jsonBody 会以 application/json 发送
|
||||||
|
- (nullable NSURLSessionDataTask *)POST:(NSString *)path
|
||||||
|
jsonBody:(nullable id)jsonBody
|
||||||
|
headers:(nullable NSDictionary<NSString *, NSString *> *)headers
|
||||||
|
completion:(KBNetworkCompletion)completion;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
306
CustomKeyboard/Network/KBNetworkManager.m
Normal file
@@ -0,0 +1,306 @@
|
|||||||
|
//
|
||||||
|
// KBNetworkManager.m
|
||||||
|
// CustomKeyboard
|
||||||
|
//
|
||||||
|
|
||||||
|
#import "KBNetworkManager.h"
|
||||||
|
#import "AFNetworking.h"
|
||||||
|
#import "KBAuthManager.h"
|
||||||
|
//#import "KBUserSessionManager.h"
|
||||||
|
#import "KBSignUtils.h"
|
||||||
|
NSErrorDomain const KBNetworkErrorDomain = @"com.company.keyboard.network";
|
||||||
|
|
||||||
|
@interface KBNetworkManager ()
|
||||||
|
@property (nonatomic, strong) AFHTTPSessionManager *manager; // AFN 管理器(ephemeral 配置)
|
||||||
|
// 私有错误派发
|
||||||
|
- (void)fail:(KBNetworkError)code completion:(KBNetworkCompletion)completion;
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation KBNetworkManager
|
||||||
|
|
||||||
|
+ (instancetype)shared {
|
||||||
|
static KBNetworkManager *m; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ m = [KBNetworkManager new]; });
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (instancetype)init {
|
||||||
|
if (self = [super init]) {
|
||||||
|
_enabled = NO; // 键盘扩展默认无网络能力,需外部显式开启
|
||||||
|
_timeout = 10.0;
|
||||||
|
// 默认请求头:Accept 任意类型 + 使用项目多语言管理器设置 Accept-Language
|
||||||
|
NSString *lang = [KBLocalizationManager shared].currentLanguageCode ?: KBLanguageCodeEnglish;
|
||||||
|
// NSString *token = [KBUserSessionManager shared].accessToken ? [KBUserSessionManager shared].accessToken : @"";
|
||||||
|
_defaultHeaders = @{
|
||||||
|
@"Accept": @"*/*",
|
||||||
|
@"Accept-Language": lang
|
||||||
|
};
|
||||||
|
// 设置基础域名,路径可相对该地址拼接
|
||||||
|
_baseURL = [NSURL URLWithString:KB_BASE_URL];
|
||||||
|
}
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)getSignWithParare:(NSDictionary *)bodyParams{
|
||||||
|
|
||||||
|
NSString *appId = @"loveKeyboard";
|
||||||
|
NSString *secret = @"kZJM39HYvhxwbJkG1fmquQRVkQiLAh2H"; // 和服务端保持一致
|
||||||
|
NSString *timestamp = [KBSignUtils currentTimestamp];
|
||||||
|
NSString *nonce = [KBSignUtils generateNonceWithLength:16];
|
||||||
|
// 1. 组装参与签名的所有参数
|
||||||
|
NSMutableDictionary<NSString *, NSString *> *signParams = [NSMutableDictionary dictionary];
|
||||||
|
signParams[@"appId"] = appId;
|
||||||
|
signParams[@"timestamp"] = timestamp;
|
||||||
|
signParams[@"nonce"] = nonce;
|
||||||
|
// 把 body 里的字段也加入签名参数
|
||||||
|
[bodyParams enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
|
||||||
|
if ([obj isKindOfClass:[NSString class]]) {
|
||||||
|
signParams[key] = obj;
|
||||||
|
} else {
|
||||||
|
signParams[key] = [obj description];
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
NSString *sign = [KBSignUtils signWithParams:signParams secret:secret];
|
||||||
|
|
||||||
|
// 将签名相关字段合并进默认请求头
|
||||||
|
NSMutableDictionary<NSString *, NSString *> *headers =
|
||||||
|
[self.defaultHeaders mutableCopy] ?: [NSMutableDictionary dictionary];
|
||||||
|
|
||||||
|
if (sign.length > 0) {
|
||||||
|
headers[@"X-Sign"] = sign;
|
||||||
|
}
|
||||||
|
headers[@"X-App-Id"] = appId;
|
||||||
|
headers[@"X-Timestamp"] = timestamp;
|
||||||
|
headers[@"X-Nonce"] = nonce;
|
||||||
|
|
||||||
|
// 触发 copy 语义,确保对外仍是不可变字典
|
||||||
|
self.defaultHeaders = headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - Public
|
||||||
|
|
||||||
|
- (NSURLSessionDataTask *)GET:(NSString *)path
|
||||||
|
parameters:(NSDictionary *)parameters
|
||||||
|
headers:(NSDictionary<NSString *,NSString *> *)headers
|
||||||
|
completion:(KBNetworkCompletion)completion {
|
||||||
|
[self getSignWithParare:parameters];
|
||||||
|
if (![self ensureEnabled:completion]) return nil;
|
||||||
|
NSString *urlString = [self buildURLStringWithPath:path];
|
||||||
|
if (!urlString) { [self fail:KBNetworkErrorInvalidURL completion:completion]; return nil; }
|
||||||
|
// 使用 AFHTTPRequestSerializer 生成带参数与头的请求
|
||||||
|
AFHTTPRequestSerializer *serializer = [AFHTTPRequestSerializer serializer];
|
||||||
|
serializer.timeoutInterval = self.timeout;
|
||||||
|
NSError *serror = nil;
|
||||||
|
NSMutableURLRequest *req = [serializer requestWithMethod:@"GET"
|
||||||
|
URLString:urlString
|
||||||
|
parameters:parameters
|
||||||
|
error:&serror];
|
||||||
|
if (serror || !req) {
|
||||||
|
if (completion) completion(nil, nil, serror ?: [NSError errorWithDomain:KBNetworkErrorDomain code:KBNetworkErrorInvalidURL userInfo:@{NSLocalizedDescriptionKey: KBLocalized(@"Invalid URL")}]);
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
[self applyHeaders:headers toMutableRequest:req contentType:nil];
|
||||||
|
return [self startAFJSONTaskWithRequest:req completion:completion];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSURLSessionDataTask *)POST:(NSString *)path
|
||||||
|
jsonBody:(id)jsonBody
|
||||||
|
headers:(NSDictionary<NSString *,NSString *> *)headers
|
||||||
|
completion:(KBNetworkCompletion)completion {
|
||||||
|
[self getSignWithParare:jsonBody];
|
||||||
|
|
||||||
|
if (![self ensureEnabled:completion]) return nil;
|
||||||
|
NSString *urlString = [self buildURLStringWithPath:path];
|
||||||
|
if (!urlString) { [self fail:KBNetworkErrorInvalidURL completion:completion]; return nil; }
|
||||||
|
// 用 JSON 序列化器生成 JSON Body 的请求
|
||||||
|
AFJSONRequestSerializer *serializer = [AFJSONRequestSerializer serializer];
|
||||||
|
serializer.timeoutInterval = self.timeout;
|
||||||
|
NSError *error = nil;
|
||||||
|
NSMutableURLRequest *req = [serializer requestWithMethod:@"POST"
|
||||||
|
URLString:urlString
|
||||||
|
parameters:jsonBody
|
||||||
|
error:&error];
|
||||||
|
if (error) { if (completion) completion(nil, nil, error); return nil; }
|
||||||
|
[self applyHeaders:headers toMutableRequest:req contentType:nil];
|
||||||
|
return [self startAFJSONTaskWithRequest:req completion:completion];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSURLSessionDataTask *)GETData:(NSString *)path
|
||||||
|
parameters:(NSDictionary *)parameters
|
||||||
|
headers:(NSDictionary<NSString *,NSString *> *)headers
|
||||||
|
completion:(KBNetworkDataCompletion)completion {
|
||||||
|
[self getSignWithParare:parameters];
|
||||||
|
if (!self.isEnabled) {
|
||||||
|
NSError *e = [NSError errorWithDomain:KBNetworkErrorDomain
|
||||||
|
code:KBNetworkErrorDisabled
|
||||||
|
userInfo:@{NSLocalizedDescriptionKey: KBLocalized(@"Network disabled (Full Access may be off)")}];
|
||||||
|
if (completion) completion(nil, nil, e);
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
NSString *urlString = [self buildURLStringWithPath:path];
|
||||||
|
if (!urlString) {
|
||||||
|
NSError *e = [NSError errorWithDomain:KBNetworkErrorDomain
|
||||||
|
code:KBNetworkErrorInvalidURL
|
||||||
|
userInfo:@{NSLocalizedDescriptionKey: KBLocalized(@"Invalid URL")}];
|
||||||
|
if (completion) completion(nil, nil, e);
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
AFHTTPRequestSerializer *serializer = [AFHTTPRequestSerializer serializer];
|
||||||
|
serializer.timeoutInterval = self.timeout;
|
||||||
|
NSError *serror = nil;
|
||||||
|
NSMutableURLRequest *req = [serializer requestWithMethod:@"GET"
|
||||||
|
URLString:urlString
|
||||||
|
parameters:parameters
|
||||||
|
error:&serror];
|
||||||
|
if (serror || !req) {
|
||||||
|
if (completion) completion(nil, nil, serror ?: [NSError errorWithDomain:KBNetworkErrorDomain code:KBNetworkErrorInvalidURL userInfo:@{NSLocalizedDescriptionKey: KBLocalized(@"Invalid URL")}]);
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
[self applyHeaders:headers toMutableRequest:req contentType:nil];
|
||||||
|
return [self startAFDataTaskWithRequest:req completion:completion];
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - Core
|
||||||
|
|
||||||
|
- (BOOL)ensureEnabled:(KBNetworkCompletion)completion {
|
||||||
|
if (!self.isEnabled) {
|
||||||
|
NSError *e = [NSError errorWithDomain:KBNetworkErrorDomain code:KBNetworkErrorDisabled userInfo:@{NSLocalizedDescriptionKey: KBLocalized(@"Network disabled (Full Access may be off)")}];
|
||||||
|
if (completion) completion(nil, nil, e);
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
|
return YES;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSString *)buildURLStringWithPath:(NSString *)path {
|
||||||
|
if (path.length == 0) return nil;
|
||||||
|
if ([path hasPrefix:@"http://"] || [path hasPrefix:@"https://"]) {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
if (self.baseURL) {
|
||||||
|
// 统一为目录型 base(以 / 结尾),并剥掉 path 的前导 /,避免覆盖 base 路径
|
||||||
|
NSString *base = self.baseURL.absoluteString ?: @"";
|
||||||
|
if (![base hasSuffix:@"/"]) { base = [base stringByAppendingString:@"/"]; }
|
||||||
|
NSURL *dirBase = [NSURL URLWithString:base];
|
||||||
|
NSString *relative = ([path hasPrefix:@"/"]) ? [path substringFromIndex:1] : path;
|
||||||
|
return [NSURL URLWithString:relative relativeToURL:dirBase].absoluteURL.absoluteString;
|
||||||
|
}
|
||||||
|
return path; // 当无 baseURL 且 path 不是完整 URL 时,让 AFN 处理(可能失败)
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)applyHeaders:(NSDictionary<NSString *,NSString *> *)headers toMutableRequest:(NSMutableURLRequest *)req contentType:(NSString *)contentType {
|
||||||
|
// 合并默认头与局部头,并注入授权头(若可用)。局部覆盖优先。
|
||||||
|
NSMutableDictionary *all = [self.defaultHeaders mutableCopy] ?: [NSMutableDictionary new];
|
||||||
|
NSString *token = [KBAuthManager shared].current.accessToken;
|
||||||
|
if (token.length > 0) {
|
||||||
|
all[@"auth-token"] = token;
|
||||||
|
} else {
|
||||||
|
[all removeObjectForKey:@"auth-token"];
|
||||||
|
}
|
||||||
|
NSDictionary *auth = [[KBAuthManager shared] authorizationHeader];
|
||||||
|
[auth enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *obj, BOOL *stop) { all[key] = obj; }];
|
||||||
|
if (contentType) all[@"Content-Type"] = contentType;
|
||||||
|
[headers enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *obj, BOOL *stop) { all[key] = obj; }];
|
||||||
|
[all enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *obj, BOOL *stop) { [req setValue:obj forHTTPHeaderField:key]; }];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSURLSessionDataTask *)startAFJSONTaskWithRequest:(NSURLRequest *)req completion:(KBNetworkCompletion)completion {
|
||||||
|
// 响应先用原始数据返回,再按 Content-Type 解析 JSON(与原实现一致)
|
||||||
|
self.manager.responseSerializer = [AFHTTPResponseSerializer serializer];
|
||||||
|
NSURLSessionDataTask *task = [self.manager dataTaskWithRequest:req uploadProgress:nil downloadProgress:nil completionHandler:^(NSURLResponse *response, id responseObject, NSError *error) {
|
||||||
|
// AFN 默认对非 2xx 的状态码返回 error;这里直接回调上层
|
||||||
|
if (error) {
|
||||||
|
if (completion) completion(nil, response, error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
NSData *data = (NSData *)responseObject;
|
||||||
|
if (![data isKindOfClass:[NSData class]]) {
|
||||||
|
if (completion) completion(nil, response, [NSError errorWithDomain:KBNetworkErrorDomain code:KBNetworkErrorInvalidResponse userInfo:@{NSLocalizedDescriptionKey:KBLocalized(@"No data")}]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
NSString *ct = nil;
|
||||||
|
if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
|
||||||
|
ct = ((NSHTTPURLResponse *)response).allHeaderFields[@"Content-Type"];
|
||||||
|
}
|
||||||
|
// 更宽松的 JSON 判定:Content-Type 里包含 json;或首字符是 { / [
|
||||||
|
BOOL looksJSON = (ct && [[ct lowercaseString] containsString:@"json"]);
|
||||||
|
if (!looksJSON) {
|
||||||
|
// 内容嗅探(不依赖服务端声明)
|
||||||
|
const unsigned char *bytes = data.bytes;
|
||||||
|
NSUInteger len = data.length;
|
||||||
|
for (NSUInteger i = 0; !looksJSON && i < len; i++) {
|
||||||
|
unsigned char c = bytes[i];
|
||||||
|
if (c == ' ' || c == '\n' || c == '\r' || c == '\t') continue;
|
||||||
|
looksJSON = (c == '{' || c == '[');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (looksJSON) {
|
||||||
|
NSError *jsonErr = nil;
|
||||||
|
id json = [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonErr];
|
||||||
|
if (jsonErr) {
|
||||||
|
if (completion) completion(nil, response, [NSError errorWithDomain:KBNetworkErrorDomain code:KBNetworkErrorDecodeFailed userInfo:@{NSLocalizedDescriptionKey:KBLocalized(@"Failed to parse JSON")}]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (![json isKindOfClass:[NSDictionary class]]) {
|
||||||
|
if (completion) completion(nil, response, [NSError errorWithDomain:KBNetworkErrorDomain code:KBNetworkErrorInvalidResponse userInfo:@{NSLocalizedDescriptionKey:KBLocalized(@"Invalid response")}]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (completion) completion((NSDictionary *)json, response, nil);
|
||||||
|
} else {
|
||||||
|
if (completion) completion(nil, response, [NSError errorWithDomain:KBNetworkErrorDomain code:KBNetworkErrorInvalidResponse userInfo:@{NSLocalizedDescriptionKey:KBLocalized(@"Invalid response")}]);
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
[task resume];
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSURLSessionDataTask *)startAFDataTaskWithRequest:(NSURLRequest *)req completion:(KBNetworkDataCompletion)completion {
|
||||||
|
self.manager.responseSerializer = [AFHTTPResponseSerializer serializer];
|
||||||
|
NSURLSessionDataTask *task = [self.manager dataTaskWithRequest:req uploadProgress:nil downloadProgress:nil completionHandler:^(NSURLResponse *response, id responseObject, NSError *error) {
|
||||||
|
if (error) {
|
||||||
|
if (completion) completion(nil, response, error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
NSData *data = (NSData *)responseObject;
|
||||||
|
if (![data isKindOfClass:[NSData class]]) {
|
||||||
|
if (completion) completion(nil, response, [NSError errorWithDomain:KBNetworkErrorDomain code:KBNetworkErrorInvalidResponse userInfo:@{NSLocalizedDescriptionKey:KBLocalized(@"No data")}]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (completion) completion(data, response, nil);
|
||||||
|
}];
|
||||||
|
[task resume];
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - AFHTTPSessionManager
|
||||||
|
|
||||||
|
- (AFHTTPSessionManager *)manager {
|
||||||
|
if (!_manager) {
|
||||||
|
NSURLSessionConfiguration *cfg = [NSURLSessionConfiguration ephemeralSessionConfiguration];
|
||||||
|
cfg.requestCachePolicy = NSURLRequestReloadIgnoringLocalCacheData;
|
||||||
|
// 不在会话级别设置超时,避免与 per-request 的 serializer.timeoutInterval 产生不一致
|
||||||
|
if (@available(iOS 11.0, *)) { cfg.waitsForConnectivity = YES; }
|
||||||
|
_manager = [[AFHTTPSessionManager alloc] initWithBaseURL:nil sessionConfiguration:cfg];
|
||||||
|
// 默认不使用 JSON 解析器,保持原生数据,再按需解析
|
||||||
|
_manager.responseSerializer = [AFHTTPResponseSerializer serializer];
|
||||||
|
}
|
||||||
|
return _manager;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - Private helpers
|
||||||
|
|
||||||
|
- (void)fail:(KBNetworkError)code completion:(KBNetworkCompletion)completion {
|
||||||
|
NSString *msg = KBLocalized(@"Network error");
|
||||||
|
switch (code) {
|
||||||
|
case KBNetworkErrorDisabled: msg = KBLocalized(@"Network disabled (Full Access may be off)"); break;
|
||||||
|
case KBNetworkErrorInvalidURL: msg = KBLocalized(@"Invalid URL"); break;
|
||||||
|
case KBNetworkErrorInvalidResponse: msg = KBLocalized(@"Invalid response"); break;
|
||||||
|
case KBNetworkErrorDecodeFailed: msg = KBLocalized(@"Parse failed"); break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
NSError *e = [NSError errorWithDomain:KBNetworkErrorDomain
|
||||||
|
code:code
|
||||||
|
userInfo:@{NSLocalizedDescriptionKey: msg}];
|
||||||
|
if (completion) completion(nil, nil, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
60
CustomKeyboard/Network/KBStreamFetcher.h
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
//
|
||||||
|
// KBStreamFetcher.h
|
||||||
|
// CustomKeyboard
|
||||||
|
//
|
||||||
|
// 轻量网络流拉取器:支持纯文本分块与 SSE(text/event-stream) 两种形式的“边下边显”。
|
||||||
|
// - 增量解码:按 UTF-8 安全前缀逐步转成字符串,避免半个多字节字符导致阻塞/乱码
|
||||||
|
// - SSE 解析:按 \n\n 切事件,合并 data: 行,移除前缀,仅回传正文
|
||||||
|
// - 兼容后端“/t”作为分段标记:可自动替换为制表符“\t”
|
||||||
|
// - 首段去首个“\t”:若首次正文以一个制表符起始(允许前导空白),可只移除“一个”\t
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
typedef void (^KBStreamFetcherChunkHandler)(NSString *chunk);
|
||||||
|
typedef void (^KBStreamFetcherFinishHandler)(NSError *_Nullable error);
|
||||||
|
|
||||||
|
@interface KBStreamFetcher : NSObject <NSURLSessionDataDelegate>
|
||||||
|
|
||||||
|
// 便利构造
|
||||||
|
+ (instancetype)fetcherWithURL:(NSURL *)url;
|
||||||
|
|
||||||
|
// 必填:请求地址
|
||||||
|
@property (nonatomic, strong) NSURL *url;
|
||||||
|
|
||||||
|
/// HTTP Method,默认为 GET
|
||||||
|
@property (nonatomic, copy, nullable) NSString *httpMethod;
|
||||||
|
|
||||||
|
/// 自定义请求体(例如 POST 的 JSON body)
|
||||||
|
@property (nonatomic, strong, nullable) NSData *httpBody;
|
||||||
|
|
||||||
|
// 可选 Header
|
||||||
|
@property (nonatomic, copy, nullable) NSDictionary<NSString *, NSString *> *extraHeaders;
|
||||||
|
|
||||||
|
// 配置项(默认值见注释)
|
||||||
|
@property (nonatomic, assign) BOOL acceptEventStream; // 默认 NO;置 YES 时发送 Accept: text/event-stream
|
||||||
|
@property (nonatomic, assign) BOOL disableCompression; // 默认 YES;发送 Accept-Encoding: identity
|
||||||
|
@property (nonatomic, assign) BOOL treatSlashTAsTab; // 默认 YES;将“/t”替换为“\t”
|
||||||
|
@property (nonatomic, assign) BOOL trimLeadingTabOnce; // 默认 YES;首次正文起始的“\t”删一个(忽略前导空白)
|
||||||
|
@property (nonatomic, assign) NSTimeInterval requestTimeout; // 默认 30s
|
||||||
|
/// UI 刷新节奏:当一次回调内解析出多段(如多条 SSE 事件)时,按该间隔逐条回调(默认 0.10s)。
|
||||||
|
@property (nonatomic, assign) NSTimeInterval flushInterval;
|
||||||
|
/// 非 SSE 且一次性拿到大段文本时,是否按空格切词逐条回调(模拟“逐词流式”),默认 YES。
|
||||||
|
@property (nonatomic, assign) BOOL splitLargeDeltasOnWhitespace;
|
||||||
|
|
||||||
|
/// 调试日志:默认 YES。输出起止时刻、首包耗时、各分片内容(截断)等关键信息。
|
||||||
|
@property (nonatomic, assign) BOOL loggingEnabled;
|
||||||
|
|
||||||
|
// 回调(统一在主线程触发)
|
||||||
|
@property (nonatomic, copy, nullable) KBStreamFetcherChunkHandler onChunk;
|
||||||
|
@property (nonatomic, copy, nullable) KBStreamFetcherFinishHandler onFinish;
|
||||||
|
|
||||||
|
// 控制
|
||||||
|
- (void)start;
|
||||||
|
- (void)cancel;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
519
CustomKeyboard/Network/KBStreamFetcher.m
Normal file
@@ -0,0 +1,519 @@
|
|||||||
|
//
|
||||||
|
// KBStreamFetcher.m
|
||||||
|
//
|
||||||
|
|
||||||
|
#import "KBStreamFetcher.h"
|
||||||
|
#import "KBLocalizationManager.h"
|
||||||
|
|
||||||
|
@interface KBStreamFetcher () <NSURLSessionDataDelegate>
|
||||||
|
@property (nonatomic, strong) NSURLSession *session;
|
||||||
|
@property (nonatomic, strong) NSURLSessionDataTask *task;
|
||||||
|
@property (nonatomic, strong) NSMutableData *buffer; // 网络原始字节累加
|
||||||
|
@property (nonatomic, assign) NSStringEncoding textEncoding; // 推断得到的文本编码(默认 UTF-8)
|
||||||
|
@property (nonatomic, assign) BOOL isSSE; // 是否为 SSE 响应
|
||||||
|
@property (nonatomic, strong) NSMutableString *sseTextBuffer; // SSE 文本缓冲(已解码)
|
||||||
|
@property (nonatomic, assign) NSInteger decodedPrefixBytes; // 已解码并写入 sseTextBuffer 的字节数(SSE)
|
||||||
|
@property (nonatomic, assign) NSInteger deliveredCharCount; // 已回传的字符数(非 SSE,用于做增量)
|
||||||
|
@property (nonatomic, assign) BOOL hasEmitted; // 是否已经输出过正文(用于“首段删 1 个 \t”)
|
||||||
|
@property (nonatomic, assign) BOOL lastChunkEndedWithTab; // 上一个已输出分片是否以 "\t" 结尾(用于跨分片去除“\t 后空格”)
|
||||||
|
@property (nonatomic, strong) NSMutableArray<NSString *> *pendingQueue; // 待回调的分片(节流输出)
|
||||||
|
@property (nonatomic, strong) NSTimer *flushTimer; // 定时从队列取出一条回调
|
||||||
|
@property (nonatomic, strong, nullable) NSError *finishError; // 结束时的错误(需要等队列清空再回调)
|
||||||
|
@property (nonatomic, copy, nullable) NSString *pendingSplitTokenPrefix; // `<SPLIT>` 跨分片残留
|
||||||
|
|
||||||
|
// Metrics
|
||||||
|
@property (nonatomic, assign) CFAbsoluteTime tStart; // start() 被调用的时刻
|
||||||
|
@property (nonatomic, assign) CFAbsoluteTime tFirstByte; // 第一次拿到可解码内容
|
||||||
|
@property (nonatomic, assign) CFAbsoluteTime tFinish; // 完成/失败时刻
|
||||||
|
@property (nonatomic, assign) NSInteger emittedChunkCount; // 已输出分片数量
|
||||||
|
@end
|
||||||
|
|
||||||
|
// 计算数据中以 UTF-8 编码可完整解码的“前缀字节长度”,避免切断多字节字符
|
||||||
|
static NSUInteger kb_validUTF8PrefixLen(NSData *data) {
|
||||||
|
const unsigned char *bytes = (const unsigned char *)data.bytes;
|
||||||
|
NSUInteger n = data.length;
|
||||||
|
if (n == 0) return 0;
|
||||||
|
NSInteger i = (NSInteger)n - 1;
|
||||||
|
while (i >= 0 && (bytes[i] & 0xC0) == 0x80) { i--; } // 10xxxxxx 续字节
|
||||||
|
if (i < 0) return 0; // 全是续字节,等下次
|
||||||
|
unsigned char b = bytes[i];
|
||||||
|
NSUInteger expected = 1;
|
||||||
|
if ((b & 0x80) == 0x00) expected = 1; // 0xxxxxxx
|
||||||
|
else if ((b & 0xE0) == 0xC0) expected = 2; // 110xxxxx
|
||||||
|
else if ((b & 0xF0) == 0xE0) expected = 3; // 1110xxxx
|
||||||
|
else if ((b & 0xF8) == 0xF0) expected = 4; // 11110xxx
|
||||||
|
else return (NSUInteger)i; // 非法起始,截到 i 之前
|
||||||
|
NSUInteger remain = n - (NSUInteger)i;
|
||||||
|
return (remain >= expected) ? n : (NSUInteger)i;
|
||||||
|
}
|
||||||
|
|
||||||
|
static NSString * const kKBStreamSplitToken = @"<SPLIT>";
|
||||||
|
|
||||||
|
@implementation KBStreamFetcher
|
||||||
|
|
||||||
|
+ (instancetype)fetcherWithURL:(NSURL *)url {
|
||||||
|
KBStreamFetcher *f = [[self alloc] init];
|
||||||
|
f.url = url;
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (instancetype)init {
|
||||||
|
if (self = [super init]) {
|
||||||
|
_httpMethod = @"GET";
|
||||||
|
_acceptEventStream = NO;
|
||||||
|
_disableCompression = YES;
|
||||||
|
_treatSlashTAsTab = YES;
|
||||||
|
_trimLeadingTabOnce = YES;
|
||||||
|
_requestTimeout = 30.0;
|
||||||
|
_textEncoding = NSUTF8StringEncoding;
|
||||||
|
_buffer = [NSMutableData data];
|
||||||
|
_sseTextBuffer = [NSMutableString string];
|
||||||
|
_pendingQueue = [NSMutableArray array];
|
||||||
|
_flushInterval = 0.1;
|
||||||
|
_splitLargeDeltasOnWhitespace = YES;
|
||||||
|
_loggingEnabled = YES;
|
||||||
|
_pendingSplitTokenPrefix = nil;
|
||||||
|
}
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)start {
|
||||||
|
if (!self.url) return;
|
||||||
|
[self cancel];
|
||||||
|
|
||||||
|
NSURLSessionConfiguration *cfg = [NSURLSessionConfiguration defaultSessionConfiguration];
|
||||||
|
cfg.requestCachePolicy = NSURLRequestReloadIgnoringLocalCacheData;
|
||||||
|
cfg.timeoutIntervalForRequest = self.requestTimeout;
|
||||||
|
cfg.timeoutIntervalForResource = MAX(self.requestTimeout, 60.0);
|
||||||
|
|
||||||
|
self.session = [NSURLSession sessionWithConfiguration:cfg delegate:self delegateQueue:[NSOperationQueue mainQueue]];
|
||||||
|
|
||||||
|
NSMutableURLRequest *req = [NSMutableURLRequest requestWithURL:self.url];
|
||||||
|
NSString *method = self.httpMethod.length > 0 ? self.httpMethod : @"GET";
|
||||||
|
req.HTTPMethod = method;
|
||||||
|
if (self.httpBody.length > 0) {
|
||||||
|
req.HTTPBody = self.httpBody;
|
||||||
|
}
|
||||||
|
if (self.disableCompression) { [req setValue:@"identity" forHTTPHeaderField:@"Accept-Encoding"]; }
|
||||||
|
if (self.acceptEventStream) { [req setValue:@"text/event-stream" forHTTPHeaderField:@"Accept"]; }
|
||||||
|
[req setValue:@"no-cache" forHTTPHeaderField:@"Cache-Control"];
|
||||||
|
[req setValue:@"keep-alive" forHTTPHeaderField:@"Connection"];
|
||||||
|
[self.extraHeaders enumerateKeysAndObjectsUsingBlock:^(NSString *k, NSString *v, BOOL *stop){ [req setValue:v forHTTPHeaderField:k]; }];
|
||||||
|
|
||||||
|
// 状态复位
|
||||||
|
[self.buffer setLength:0];
|
||||||
|
[self.sseTextBuffer setString:@""];
|
||||||
|
self.isSSE = NO;
|
||||||
|
self.textEncoding = NSUTF8StringEncoding;
|
||||||
|
self.decodedPrefixBytes = 0;
|
||||||
|
self.deliveredCharCount = 0;
|
||||||
|
self.hasEmitted = NO;
|
||||||
|
self.lastChunkEndedWithTab = NO;
|
||||||
|
[self.pendingQueue removeAllObjects];
|
||||||
|
[self.flushTimer invalidate]; self.flushTimer = nil;
|
||||||
|
self.finishError = nil;
|
||||||
|
self.pendingSplitTokenPrefix = nil;
|
||||||
|
|
||||||
|
self.tStart = CFAbsoluteTimeGetCurrent();
|
||||||
|
self.tFirstByte = 0;
|
||||||
|
self.tFinish = 0;
|
||||||
|
self.emittedChunkCount = 0;
|
||||||
|
if (self.loggingEnabled) {
|
||||||
|
NSLog(@"[KBStream] start url=%@ acceptSSE=%@ disableCompression=%@ flush=%.0fms splitWords=%@",
|
||||||
|
self.url.absoluteString,
|
||||||
|
self.acceptEventStream?@"YES":@"NO",
|
||||||
|
self.disableCompression?@"YES":@"NO",
|
||||||
|
self.flushInterval*1000.0,
|
||||||
|
self.splitLargeDeltasOnWhitespace?@"YES":@"NO");
|
||||||
|
}
|
||||||
|
self.task = [self.session dataTaskWithRequest:req];
|
||||||
|
[self.task resume];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)cancel {
|
||||||
|
[self.task cancel];
|
||||||
|
self.task = nil;
|
||||||
|
[self.session invalidateAndCancel];
|
||||||
|
self.session = nil;
|
||||||
|
[self.buffer setLength:0];
|
||||||
|
[self.sseTextBuffer setString:@""];
|
||||||
|
self.decodedPrefixBytes = 0;
|
||||||
|
self.deliveredCharCount = 0;
|
||||||
|
self.hasEmitted = NO;
|
||||||
|
self.lastChunkEndedWithTab = NO;
|
||||||
|
[self.pendingQueue removeAllObjects];
|
||||||
|
[self.flushTimer invalidate]; self.flushTimer = nil;
|
||||||
|
self.finishError = nil;
|
||||||
|
self.pendingSplitTokenPrefix = nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - NSURLSessionDataDelegate
|
||||||
|
|
||||||
|
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler {
|
||||||
|
self.isSSE = NO;
|
||||||
|
self.textEncoding = NSUTF8StringEncoding;
|
||||||
|
if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
|
||||||
|
NSHTTPURLResponse *r = (NSHTTPURLResponse *)response;
|
||||||
|
NSString *ct = r.allHeaderFields[@"Content-Type"] ?: r.allHeaderFields[@"content-type"];
|
||||||
|
if ([ct isKindOfClass:[NSString class]]) {
|
||||||
|
NSString *lower = [ct lowercaseString];
|
||||||
|
if ([lower containsString:@"text/event-stream"]) self.isSSE = YES;
|
||||||
|
NSRange pos = [lower rangeOfString:@"charset="];
|
||||||
|
if (pos.location != NSNotFound) {
|
||||||
|
NSString *charset = [[lower substringFromIndex:pos.location + pos.length] componentsSeparatedByString:@";"][0];
|
||||||
|
if ([charset containsString:@"utf-8"] || [charset containsString:@"utf8"]) {
|
||||||
|
self.textEncoding = NSUTF8StringEncoding;
|
||||||
|
} else if ([charset containsString:@"iso-8859-1"] || [charset containsString:@"latin1"]) {
|
||||||
|
self.textEncoding = NSISOLatin1StringEncoding;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
[self.sseTextBuffer setString:@""];
|
||||||
|
self.decodedPrefixBytes = 0;
|
||||||
|
if (completionHandler) completionHandler(NSURLSessionResponseAllow);
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
|
||||||
|
if (data.length == 0) return;
|
||||||
|
[self.buffer appendData:data];
|
||||||
|
|
||||||
|
NSUInteger validLen = (self.textEncoding == NSUTF8StringEncoding)
|
||||||
|
? kb_validUTF8PrefixLen(self.buffer)
|
||||||
|
: self.buffer.length;
|
||||||
|
if (validLen > 0 && self.tFirstByte == 0) {
|
||||||
|
self.tFirstByte = CFAbsoluteTimeGetCurrent();
|
||||||
|
if (self.loggingEnabled) {
|
||||||
|
NSLog(@"[KBStream] first-bytes after %.0fms (encoding=%@, SSE=%@)",
|
||||||
|
(self.tFirstByte - self.tStart)*1000.0,
|
||||||
|
(self.textEncoding==NSUTF8StringEncoding?@"UTF-8":@"Other"),
|
||||||
|
self.isSSE?@"YES":@"NO");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (validLen == 0) return; // 末尾可能卡着半个字符
|
||||||
|
|
||||||
|
if (self.isSSE) {
|
||||||
|
if ((NSUInteger)self.decodedPrefixBytes < validLen) {
|
||||||
|
NSRange rng = NSMakeRange((NSUInteger)self.decodedPrefixBytes, validLen - (NSUInteger)self.decodedPrefixBytes);
|
||||||
|
NSString *piece = [[NSString alloc] initWithBytes:(const char *)self.buffer.bytes + rng.location
|
||||||
|
length:rng.length
|
||||||
|
encoding:self.textEncoding];
|
||||||
|
if (piece.length > 0) {
|
||||||
|
[self.sseTextBuffer appendString:piece];
|
||||||
|
self.decodedPrefixBytes = (NSInteger)validLen;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 统一换行并按 SSE 事件 \n\n 切开
|
||||||
|
if (self.sseTextBuffer.length > 0) {
|
||||||
|
NSString *normalized = [self.sseTextBuffer stringByReplacingOccurrencesOfString:@"\r\n" withString:@"\n"];
|
||||||
|
[self.sseTextBuffer setString:normalized];
|
||||||
|
while (1) {
|
||||||
|
NSRange sep = [self.sseTextBuffer rangeOfString:@"\n\n"]; // 完整事件
|
||||||
|
if (sep.location == NSNotFound) break;
|
||||||
|
NSString *event = [self.sseTextBuffer substringToIndex:sep.location];
|
||||||
|
[self.sseTextBuffer deleteCharactersInRange:NSMakeRange(0, sep.location + sep.length)];
|
||||||
|
|
||||||
|
// 合并 data: 行为正文
|
||||||
|
NSArray<NSString *> *lines = [event componentsSeparatedByString:@"\n"];
|
||||||
|
NSMutableString *payload = [NSMutableString string];
|
||||||
|
for (NSString *ln in lines) {
|
||||||
|
if ([ln hasPrefix:@"data:"]) {
|
||||||
|
NSString *v = [ln substringFromIndex:5];
|
||||||
|
if (v.length > 0 && [v hasPrefix:@" "]) v = [v substringFromIndex:1];
|
||||||
|
[payload appendString:v ?: @""];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (payload.length > 0) {
|
||||||
|
if (self.loggingEnabled) {
|
||||||
|
NSLog(@"[KBStream] SSE raw payload: %@", payload);
|
||||||
|
}
|
||||||
|
NSString *llmText = nil;
|
||||||
|
if ([self processLLMChunkPayload:payload output:&llmText]) {
|
||||||
|
if (llmText.length > 0) { [self enqueueChunk:llmText]; }
|
||||||
|
} else {
|
||||||
|
[self enqueueChunk:payload];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 非 SSE:直接对“可解码前缀”做增量输出
|
||||||
|
NSString *prefix = [[NSString alloc] initWithBytes:self.buffer.bytes length:validLen encoding:self.textEncoding];
|
||||||
|
if (!prefix) return;
|
||||||
|
if (self.deliveredCharCount < (NSInteger)prefix.length) {
|
||||||
|
NSString *delta = [prefix substringFromIndex:self.deliveredCharCount];
|
||||||
|
self.deliveredCharCount = prefix.length;
|
||||||
|
if (self.splitLargeDeltasOnWhitespace && delta.length > 16) {
|
||||||
|
// 按空格切词逐条回调(保留空格,使观感更自然)
|
||||||
|
NSArray<NSString *> *parts = [delta componentsSeparatedByString:@" "];
|
||||||
|
for (NSUInteger i = 0; i < parts.count; i++) {
|
||||||
|
NSString *w = parts[i];
|
||||||
|
if (w.length == 0) { [self enqueueChunk:@" "]; continue; }
|
||||||
|
if (i + 1 < parts.count) {
|
||||||
|
[self enqueueChunk:[w stringByAppendingString:@" "]];
|
||||||
|
} else {
|
||||||
|
[self enqueueChunk:w];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
[self enqueueChunk:delta];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
|
||||||
|
if (!error && self.isSSE && self.sseTextBuffer.length > 0) {
|
||||||
|
// 处理最后一条未以 \n\n 结束的事件
|
||||||
|
NSString *normalized = [self.sseTextBuffer stringByReplacingOccurrencesOfString:@"\r\n" withString:@"\n"];
|
||||||
|
NSArray<NSString *> *lines = [normalized componentsSeparatedByString:@"\n"];
|
||||||
|
NSMutableString *payload = [NSMutableString string];
|
||||||
|
for (NSString *ln in lines) {
|
||||||
|
if ([ln hasPrefix:@"data:"]) {
|
||||||
|
NSString *v = [ln substringFromIndex:5];
|
||||||
|
if (v.length > 0 && [v hasPrefix:@" "]) v = [v substringFromIndex:1];
|
||||||
|
[payload appendString:v ?: @""];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (payload.length > 0) {
|
||||||
|
if (self.loggingEnabled) {
|
||||||
|
NSLog(@"[KBStream] SSE raw payload: %@", payload);
|
||||||
|
}
|
||||||
|
NSString *delta = nil;
|
||||||
|
if ((NSInteger)payload.length >= self.deliveredCharCount) {
|
||||||
|
delta = [payload substringFromIndex:self.deliveredCharCount];
|
||||||
|
} else {
|
||||||
|
delta = payload;
|
||||||
|
}
|
||||||
|
self.deliveredCharCount = payload.length;
|
||||||
|
if (delta.length > 0) {
|
||||||
|
NSString *llmText = nil;
|
||||||
|
if ([self processLLMChunkPayload:delta output:&llmText]) {
|
||||||
|
if (llmText.length > 0) { [self emitChunk:llmText]; }
|
||||||
|
} else {
|
||||||
|
[self emitChunk:delta];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (self.pendingSplitTokenPrefix.length > 0) {
|
||||||
|
NSString *carry = self.pendingSplitTokenPrefix;
|
||||||
|
self.pendingSplitTokenPrefix = nil;
|
||||||
|
if (carry.length > 0) { [self enqueueChunk:carry]; }
|
||||||
|
}
|
||||||
|
|
||||||
|
self.tFinish = CFAbsoluteTimeGetCurrent();
|
||||||
|
if (self.loggingEnabled) {
|
||||||
|
double t0 = (self.tFirstByte>0? (self.tFirstByte - self.tStart)*1000.0 : -1);
|
||||||
|
double t1 = (self.tFirstByte>0? (self.tFinish - self.tFirstByte)*1000.0 : -1);
|
||||||
|
double tt = (self.tFinish - self.tStart)*1000.0;
|
||||||
|
NSLog(@"[KBStream] finish chunks=%ld firstByte=%.0fms after start, tail=%.0fms, total=%.0fms error=%@",
|
||||||
|
(long)self.emittedChunkCount, t0, t1, tt, error);
|
||||||
|
}
|
||||||
|
// 若队列还有待输出内容,等队列清空再回调 finish
|
||||||
|
if (self.pendingQueue.count > 0) {
|
||||||
|
self.finishError = error;
|
||||||
|
[self startFlushTimerIfNeeded];
|
||||||
|
} else {
|
||||||
|
if (self.onFinish) dispatch_async(dispatch_get_main_queue(), ^{ self.onFinish(error); });
|
||||||
|
[self cancel];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - Helpers
|
||||||
|
|
||||||
|
- (void)emitChunk:(NSString *)rawText {
|
||||||
|
if (rawText.length == 0) return;
|
||||||
|
// 调试:在任何处理之前打印后端“原始文本分片”,便于对照排查
|
||||||
|
if (self.loggingEnabled) {
|
||||||
|
// NSLog(@"[KBStream] RAW chunk#%ld len=%lu text=\"%@\"",
|
||||||
|
// (long)(self.emittedChunkCount + 1),
|
||||||
|
// (unsigned long)rawText.length,
|
||||||
|
// KBPrintableSnippet(rawText, 160));
|
||||||
|
}
|
||||||
|
NSString *text = rawText;
|
||||||
|
// 0) 规范化换行与段起始:去掉位于片段开头的 \r/\n;将 "\n\t"、"\r\n\t"、"\r\t" 归一为 "\t"
|
||||||
|
text = [text stringByReplacingOccurrencesOfString:@"\r\n\t" withString:@"\t"];
|
||||||
|
text = [text stringByReplacingOccurrencesOfString:@"\n\t" withString:@"\t"];
|
||||||
|
text = [text stringByReplacingOccurrencesOfString:@"\r\t" withString:@"\t"];
|
||||||
|
while (text.length > 0) {
|
||||||
|
unichar c0 = [text characterAtIndex:0];
|
||||||
|
if (c0 == '\n' || c0 == '\r') { text = [text substringFromIndex:1]; continue; }
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// 1) 统一处理 “/t” -> “\t”
|
||||||
|
if (self.treatSlashTAsTab) {
|
||||||
|
text = [text stringByReplacingOccurrencesOfString:@"/t" withString:@"\t"];
|
||||||
|
}
|
||||||
|
// 2) 仅在整体首段:去掉一个起始的 "\t",以及其后紧邻的一个空格(若存在)
|
||||||
|
if (!self.hasEmitted && self.trimLeadingTabOnce) {
|
||||||
|
if (text.length > 0 && [text characterAtIndex:0] == '\t') {
|
||||||
|
NSUInteger start = 1;
|
||||||
|
if (start < text.length && [text characterAtIndex:start] == ' ') start++;
|
||||||
|
text = [text substringFromIndex:start];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 3) 从第二段开始:去掉每个段首的一个空格(即 “\t ” -> “\t”),跨分片也处理
|
||||||
|
if (text.length > 0) {
|
||||||
|
// 跨分片:若上个分片以 \t 结尾,本分片起始的一个或多个空格去掉一个
|
||||||
|
if (self.lastChunkEndedWithTab) {
|
||||||
|
NSUInteger j = 0;
|
||||||
|
while (j < text.length && [text characterAtIndex:j] == ' ') { j++; }
|
||||||
|
if (j > 0) {
|
||||||
|
text = [text substringFromIndex:1]; // 仅去一个空格
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 同一分片内:将 “\t ” 规范化为 “\t”(仅去一个空格)
|
||||||
|
text = [text stringByReplacingOccurrencesOfString:@"\t " withString:@"\t"];
|
||||||
|
}
|
||||||
|
if (text.length == 0) { self.lastChunkEndedWithTab = NO; return; }
|
||||||
|
self.emittedChunkCount += 1;
|
||||||
|
if (self.loggingEnabled) {
|
||||||
|
NSLog(@"[KBStream] chunk#%ld len=%lu text=\"%@\"",
|
||||||
|
(long)self.emittedChunkCount, (unsigned long)text.length, KBPrintableSnippet(text, 160));
|
||||||
|
}
|
||||||
|
if (self.onChunk) dispatch_async(dispatch_get_main_queue(), ^{ self.onChunk(text); });
|
||||||
|
self.hasEmitted = YES;
|
||||||
|
// 记录末尾是否为分段分隔符 \t(用于跨分片处理)
|
||||||
|
unichar lastc = [text characterAtIndex:text.length - 1];
|
||||||
|
self.lastChunkEndedWithTab = (lastc == '\t');
|
||||||
|
}
|
||||||
|
|
||||||
|
- (BOOL)processLLMChunkPayload:(NSString *)payload output:(NSString * _Nullable __autoreleasing *)output {
|
||||||
|
if (output) { *output = nil; }
|
||||||
|
if (payload.length == 0) { return NO; }
|
||||||
|
NSData *jsonData = [payload dataUsingEncoding:NSUTF8StringEncoding];
|
||||||
|
if (!jsonData) { return NO; }
|
||||||
|
NSError *jsonError = nil;
|
||||||
|
id obj = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&jsonError];
|
||||||
|
if (jsonError || ![obj isKindOfClass:[NSDictionary class]]) { return NO; }
|
||||||
|
NSString *type = ((NSDictionary *)obj)[@"type"];
|
||||||
|
if (![type isKindOfClass:[NSString class]]) { return NO; }
|
||||||
|
if ([type isEqualToString:@"llm_chunk"]) {
|
||||||
|
id dataValue = ((NSDictionary *)obj)[@"data"];
|
||||||
|
if (![dataValue isKindOfClass:[NSString class]]) {
|
||||||
|
if (output) { *output = @""; }
|
||||||
|
return YES;
|
||||||
|
}
|
||||||
|
NSString *normalized = [self normalizedLLMDataString:(NSString *)dataValue];
|
||||||
|
if (output) { *output = normalized; }
|
||||||
|
return YES;
|
||||||
|
}
|
||||||
|
if ([type isEqualToString:@"search_result"]) {
|
||||||
|
NSString *searchText = [self normalizedSearchResultString:((NSDictionary *)obj)[@"data"]];
|
||||||
|
if (output) { *output = searchText ?: @""; }
|
||||||
|
return YES;
|
||||||
|
}
|
||||||
|
if ([type isEqualToString:@"done"]) {
|
||||||
|
if (output) { *output = @""; }
|
||||||
|
return YES;
|
||||||
|
}
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSString *)normalizedLLMDataString:(NSString *)dataString {
|
||||||
|
NSString *combined = dataString ?: @"";
|
||||||
|
if (self.pendingSplitTokenPrefix.length > 0) {
|
||||||
|
combined = [self.pendingSplitTokenPrefix stringByAppendingString:combined];
|
||||||
|
self.pendingSplitTokenPrefix = nil;
|
||||||
|
}
|
||||||
|
if (combined.length == 0) { return @""; }
|
||||||
|
NSString *result = [combined stringByReplacingOccurrencesOfString:kKBStreamSplitToken withString:@"\t"];
|
||||||
|
NSString *suffix = [self pendingSplitPrefixSuffixForString:result];
|
||||||
|
if (suffix.length > 0) {
|
||||||
|
self.pendingSplitTokenPrefix = suffix;
|
||||||
|
result = [result substringToIndex:result.length - suffix.length];
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSString *)normalizedSearchResultString:(id)dataValue {
|
||||||
|
if (![dataValue isKindOfClass:[NSArray class]]) { return @""; }
|
||||||
|
NSArray *list = (NSArray *)dataValue;
|
||||||
|
NSMutableArray<NSString *> *segments = [NSMutableArray array];
|
||||||
|
for (NSUInteger i = 0; i < list.count; i++) {
|
||||||
|
id item = list[i];
|
||||||
|
NSString *payload = nil;
|
||||||
|
if ([item isKindOfClass:[NSDictionary class]]) {
|
||||||
|
id val = ((NSDictionary *)item)[@"payload"];
|
||||||
|
if ([val isKindOfClass:[NSString class]]) {
|
||||||
|
payload = (NSString *)val;
|
||||||
|
}
|
||||||
|
} else if ([item isKindOfClass:[NSString class]]) {
|
||||||
|
payload = (NSString *)item;
|
||||||
|
}
|
||||||
|
payload = [payload stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
||||||
|
if (payload.length == 0) { continue; }
|
||||||
|
NSString *line = [NSString stringWithFormat:@"%lu. %@", (unsigned long)(segments.count + 1), payload];
|
||||||
|
[segments addObject:line];
|
||||||
|
}
|
||||||
|
if (segments.count == 0) { return @""; }
|
||||||
|
NSString *title = KBLocalized(@"Search result");
|
||||||
|
NSMutableString *text = [NSMutableString string];
|
||||||
|
[text appendString:@"\t"];
|
||||||
|
[text appendFormat:@"%@:", title.length > 0 ? title : @"Search result"];
|
||||||
|
for (NSString *line in segments) {
|
||||||
|
[text appendString:@"\t"];
|
||||||
|
[text appendString:line];
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSString *)pendingSplitPrefixSuffixForString:(NSString *)text {
|
||||||
|
if (text.length == 0) { return @""; }
|
||||||
|
NSUInteger tokenLen = kKBStreamSplitToken.length;
|
||||||
|
if (tokenLen <= 1) { return @""; }
|
||||||
|
NSUInteger maxLen = MIN(tokenLen - 1, text.length);
|
||||||
|
for (NSUInteger len = maxLen; len > 0; len--) {
|
||||||
|
NSString *suffix = [text substringFromIndex:text.length - len];
|
||||||
|
NSString *prefix = [kKBStreamSplitToken substringToIndex:len];
|
||||||
|
if ([suffix isEqualToString:prefix]) {
|
||||||
|
return suffix;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return @"";
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - Queue/Flush
|
||||||
|
|
||||||
|
- (void)enqueueChunk:(NSString *)s {
|
||||||
|
if (s.length == 0) return;
|
||||||
|
[self.pendingQueue addObject:s];
|
||||||
|
[self startFlushTimerIfNeeded];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)startFlushTimerIfNeeded {
|
||||||
|
if (self.flushTimer) return;
|
||||||
|
__weak typeof(self) weakSelf = self;
|
||||||
|
self.flushTimer = [NSTimer scheduledTimerWithTimeInterval:MAX(0.01, self.flushInterval)
|
||||||
|
repeats:YES
|
||||||
|
block:^(NSTimer * _Nonnull t) {
|
||||||
|
__strong typeof(weakSelf) self = weakSelf; if (!self) { [t invalidate]; return; }
|
||||||
|
if (self.pendingQueue.count == 0) {
|
||||||
|
[t invalidate]; self.flushTimer = nil;
|
||||||
|
if (self.finishError || self.finishError == nil) {
|
||||||
|
NSError *err = self.finishError; self.finishError = nil;
|
||||||
|
if (self.onFinish) dispatch_async(dispatch_get_main_queue(), ^{ self.onFinish(err); });
|
||||||
|
[self cancel];
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
NSString *first = self.pendingQueue.firstObject;
|
||||||
|
[self.pendingQueue removeObjectAtIndex:0];
|
||||||
|
[self emitChunk:first];
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - Logging helpers
|
||||||
|
|
||||||
|
static NSString *KBPrintableSnippet(NSString *s, NSUInteger maxLen) {
|
||||||
|
if (!s) return @"";
|
||||||
|
NSString *x = [s stringByReplacingOccurrencesOfString:@"\n" withString:@"\\n"];
|
||||||
|
if (x.length > maxLen) {
|
||||||
|
x = [[x substringToIndex:maxLen] stringByAppendingString:@"…"];
|
||||||
|
}
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
71
CustomKeyboard/Network/NetworkStreamHandler.h
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
//
|
||||||
|
// NetworkStreamHandler.h
|
||||||
|
// CustomKeyboard
|
||||||
|
//
|
||||||
|
// Created by Mac on 2025/11/12.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
typedef NS_ENUM(NSUInteger, NetworkStreamState) {
|
||||||
|
NetworkStreamStateIdle,
|
||||||
|
NetworkStreamStateConnecting,
|
||||||
|
NetworkStreamStateReceiving,
|
||||||
|
NetworkStreamStateCompleted,
|
||||||
|
NetworkStreamStateError
|
||||||
|
};
|
||||||
|
|
||||||
|
@class NetworkStreamHandler;
|
||||||
|
|
||||||
|
@protocol NetworkStreamDelegate <NSObject>
|
||||||
|
|
||||||
|
@optional
|
||||||
|
// 接收到数据块
|
||||||
|
- (void)networkStream:(NetworkStreamHandler *)stream didReceiveData:(NSData *)data;
|
||||||
|
// 接收到文本数据(如果是文本内容)
|
||||||
|
- (void)networkStream:(NetworkStreamHandler *)stream didReceiveText:(NSString *)text;
|
||||||
|
// 进度更新
|
||||||
|
- (void)networkStream:(NetworkStreamHandler *)stream downloadProgress:(float)progress;
|
||||||
|
// 状态改变
|
||||||
|
- (void)networkStream:(NetworkStreamHandler *)stream stateChanged:(NetworkStreamState)state;
|
||||||
|
// 请求完成
|
||||||
|
- (void)networkStream:(NetworkStreamHandler *)stream didCompleteWithError:(NSError * _Nullable)error;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
typedef void (^NetworkStreamProgressBlock)(float progress);
|
||||||
|
typedef void (^NetworkStreamDataBlock)(NSData *data);
|
||||||
|
typedef void (^NetworkStreamTextBlock)(NSString *text);
|
||||||
|
typedef void (^NetworkStreamCompletionBlock)(NSError * _Nullable error);
|
||||||
|
|
||||||
|
@interface NetworkStreamHandler : NSObject <NSURLSessionDataDelegate>
|
||||||
|
|
||||||
|
@property (nonatomic, weak) id<NetworkStreamDelegate> delegate;
|
||||||
|
@property (nonatomic, assign, readonly) NetworkStreamState state;
|
||||||
|
@property (nonatomic, strong, readonly) NSURLResponse *response;
|
||||||
|
@property (nonatomic, assign, readonly) long long totalBytesReceived;
|
||||||
|
|
||||||
|
// 初始化方法
|
||||||
|
- (instancetype)initWithURL:(NSURL *)url;
|
||||||
|
- (instancetype)initWithRequest:(NSURLRequest *)request;
|
||||||
|
|
||||||
|
// 开始请求(使用代理回调)
|
||||||
|
- (void)startRequest;
|
||||||
|
|
||||||
|
// 开始请求(使用 Block 回调)
|
||||||
|
- (void)startRequestWithProgress:(NetworkStreamProgressBlock _Nullable)progress
|
||||||
|
onData:(NetworkStreamDataBlock _Nullable)dataBlock
|
||||||
|
onText:(NetworkStreamTextBlock _Nullable)textBlock
|
||||||
|
completion:(NetworkStreamCompletionBlock _Nullable)completion;
|
||||||
|
|
||||||
|
// 取消请求
|
||||||
|
- (void)cancelRequest;
|
||||||
|
|
||||||
|
// 构建默认请求(包含常见的请求头)
|
||||||
|
+ (NSURLRequest *)createDefaultRequestWithURL:(NSURL *)url method:(NSString *)method;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
253
CustomKeyboard/Network/NetworkStreamHandler.m
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
//
|
||||||
|
// NetworkStreamHandler.m
|
||||||
|
// CustomKeyboard
|
||||||
|
//
|
||||||
|
// Created by Mac on 2025/11/12.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import "NetworkStreamHandler.h"
|
||||||
|
|
||||||
|
@interface NetworkStreamHandler ()
|
||||||
|
|
||||||
|
@property (nonatomic, strong) NSURLSession *session;
|
||||||
|
@property (nonatomic, strong) NSURLSessionDataTask *dataTask;
|
||||||
|
@property (nonatomic, strong) NSURLRequest *request;
|
||||||
|
@property (nonatomic, strong) NSMutableData *receivedData;
|
||||||
|
@property (nonatomic, assign) long long expectedContentLength;
|
||||||
|
@property (nonatomic, assign) NetworkStreamState state;
|
||||||
|
@property (nonatomic, strong) NSURLResponse *response;
|
||||||
|
|
||||||
|
// Block 回调
|
||||||
|
@property (nonatomic, copy) NetworkStreamProgressBlock progressBlock;
|
||||||
|
@property (nonatomic, copy) NetworkStreamDataBlock dataBlock;
|
||||||
|
@property (nonatomic, copy) NetworkStreamTextBlock textBlock;
|
||||||
|
@property (nonatomic, copy) NetworkStreamCompletionBlock completionBlock;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation NetworkStreamHandler
|
||||||
|
|
||||||
|
- (instancetype)initWithURL:(NSURL *)url {
|
||||||
|
NSURLRequest *request = [NetworkStreamHandler createDefaultRequestWithURL:url method:@"GET"];
|
||||||
|
return [self initWithRequest:request];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (instancetype)initWithRequest:(NSURLRequest *)request {
|
||||||
|
self = [super init];
|
||||||
|
if (self) {
|
||||||
|
_request = request;
|
||||||
|
_receivedData = [NSMutableData data];
|
||||||
|
_state = NetworkStreamStateIdle;
|
||||||
|
_totalBytesReceived = 0;
|
||||||
|
|
||||||
|
// 创建 URLSession 配置
|
||||||
|
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
|
||||||
|
config.timeoutIntervalForRequest = 30.0;
|
||||||
|
config.timeoutIntervalForResource = 300.0;
|
||||||
|
config.requestCachePolicy = NSURLRequestReloadIgnoringLocalCacheData;
|
||||||
|
|
||||||
|
// 创建 URLSession
|
||||||
|
_session = [NSURLSession sessionWithConfiguration:config
|
||||||
|
delegate:self
|
||||||
|
delegateQueue:[NSOperationQueue mainQueue]];
|
||||||
|
}
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)dealloc {
|
||||||
|
[self cancelRequest];
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - Public Methods
|
||||||
|
|
||||||
|
- (void)startRequest {
|
||||||
|
if (self.state != NetworkStreamStateIdle) {
|
||||||
|
NSLog(@"Request already in progress");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
[self updateState:NetworkStreamStateConnecting];
|
||||||
|
self.dataTask = [self.session dataTaskWithRequest:self.request];
|
||||||
|
[self.dataTask resume];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)startRequestWithProgress:(NetworkStreamProgressBlock)progress
|
||||||
|
onData:(NetworkStreamDataBlock)dataBlock
|
||||||
|
onText:(NetworkStreamTextBlock)textBlock
|
||||||
|
completion:(NetworkStreamCompletionBlock)completion {
|
||||||
|
|
||||||
|
self.progressBlock = progress;
|
||||||
|
self.dataBlock = dataBlock;
|
||||||
|
self.textBlock = textBlock;
|
||||||
|
self.completionBlock = completion;
|
||||||
|
|
||||||
|
[self startRequest];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)cancelRequest {
|
||||||
|
if (self.dataTask) {
|
||||||
|
[self.dataTask cancel];
|
||||||
|
self.dataTask = nil;
|
||||||
|
}
|
||||||
|
[self updateState:NetworkStreamStateIdle];
|
||||||
|
}
|
||||||
|
|
||||||
|
+ (NSURLRequest *)createDefaultRequestWithURL:(NSURL *)url method:(NSString *)method {
|
||||||
|
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
|
||||||
|
request.HTTPMethod = method;
|
||||||
|
request.timeoutInterval = 30.0;
|
||||||
|
|
||||||
|
// 设置常见的请求头(根据您的截图)
|
||||||
|
[request setValue:@"text/html, application/xhtml+xml, application/xml; q=0.9, image/avif, image/webp, image/apng, */*; q=0.8, application/signed-exchange; v=b3; q=0.7" forHTTPHeaderField:@"Accept"];
|
||||||
|
[request setValue:@"gzip, deflate" forHTTPHeaderField:@"Accept-Encoding"];
|
||||||
|
[request setValue:@"zh-CN, zh; q=0.9, ko; q=0.8, ja; q=0.7" forHTTPHeaderField:@"Accept-Language"];
|
||||||
|
[request setValue:@"keep-alive" forHTTPHeaderField:@"Connection"];
|
||||||
|
[request setValue:@"1" forHTTPHeaderField:@"Upgrade-Insecure-Requests"];
|
||||||
|
|
||||||
|
// 用户代理(可选)
|
||||||
|
NSString *userAgent = @"Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Mobile/15E148 Safari/604.1";
|
||||||
|
[request setValue:userAgent forHTTPHeaderField:@"User-Agent"];
|
||||||
|
|
||||||
|
return [request copy];
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - Private Methods
|
||||||
|
|
||||||
|
- (void)updateState:(NetworkStreamState)newState {
|
||||||
|
if (_state != newState) {
|
||||||
|
_state = newState;
|
||||||
|
|
||||||
|
// 通知代理状态改变
|
||||||
|
if ([self.delegate respondsToSelector:@selector(networkStream:stateChanged:)]) {
|
||||||
|
[self.delegate networkStream:self stateChanged:newState];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)notifyProgress:(float)progress {
|
||||||
|
if (self.progressBlock) {
|
||||||
|
self.progressBlock(progress);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([self.delegate respondsToSelector:@selector(networkStream:downloadProgress:)]) {
|
||||||
|
[self.delegate networkStream:self downloadProgress:progress];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)notifyReceivedData:(NSData *)data {
|
||||||
|
if (self.dataBlock) {
|
||||||
|
self.dataBlock(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([self.delegate respondsToSelector:@selector(networkStream:didReceiveData:)]) {
|
||||||
|
[self.delegate networkStream:self didReceiveData:data];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是文本数据,尝试转换为字符串
|
||||||
|
if (self.textBlock || [self.delegate respondsToSelector:@selector(networkStream:didReceiveText:)]) {
|
||||||
|
NSString *text = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
|
||||||
|
if (text) {
|
||||||
|
if (self.textBlock) {
|
||||||
|
self.textBlock(text);
|
||||||
|
}
|
||||||
|
if ([self.delegate respondsToSelector:@selector(networkStream:didReceiveText:)]) {
|
||||||
|
[self.delegate networkStream:self didReceiveText:text];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)notifyCompletionWithError:(NSError * _Nullable)error {
|
||||||
|
if (self.completionBlock) {
|
||||||
|
self.completionBlock(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([self.delegate respondsToSelector:@selector(networkStream:didCompleteWithError:)]) {
|
||||||
|
[self.delegate networkStream:self didCompleteWithError:error];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - NSURLSessionDataDelegate
|
||||||
|
|
||||||
|
- (void)URLSession:(NSURLSession *)session
|
||||||
|
dataTask:(NSURLSessionDataTask *)dataTask
|
||||||
|
didReceiveResponse:(NSURLResponse *)response
|
||||||
|
completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler {
|
||||||
|
|
||||||
|
self.response = response;
|
||||||
|
self.expectedContentLength = response.expectedContentLength;
|
||||||
|
_totalBytesReceived = 0;
|
||||||
|
[self.receivedData setLength:0];
|
||||||
|
|
||||||
|
[self updateState:NetworkStreamStateReceiving];
|
||||||
|
|
||||||
|
// 检查响应头,处理 CORS 等
|
||||||
|
if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
|
||||||
|
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
|
||||||
|
NSLog(@"Response headers: %@", httpResponse.allHeaderFields);
|
||||||
|
|
||||||
|
// 可以在这里检查 CORS 头等信息
|
||||||
|
NSString *allowOrigin = httpResponse.allHeaderFields[@"Access-Control-Allow-Origin"];
|
||||||
|
if (allowOrigin) {
|
||||||
|
NSLog(@"CORS Allow Origin: %@", allowOrigin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
completionHandler(NSURLSessionResponseAllow);
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)URLSession:(NSURLSession *)session
|
||||||
|
dataTask:(NSURLSessionDataTask *)dataTask
|
||||||
|
didReceiveData:(NSData *)data {
|
||||||
|
|
||||||
|
_totalBytesReceived += data.length;
|
||||||
|
[self.receivedData appendData:data];
|
||||||
|
|
||||||
|
// 通知接收到数据块
|
||||||
|
[self notifyReceivedData:data];
|
||||||
|
|
||||||
|
// 计算并通知进度
|
||||||
|
if (self.expectedContentLength != NSURLResponseUnknownLength) {
|
||||||
|
float progress = (float)self.totalBytesReceived / (float)self.expectedContentLength;
|
||||||
|
[self notifyProgress:progress];
|
||||||
|
} else {
|
||||||
|
// 对于 chunked 传输,可能没有确切的内容长度
|
||||||
|
[self notifyProgress:-1]; // 使用 -1 表示未知进度
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)URLSession:(NSURLSession *)session
|
||||||
|
task:(NSURLSessionTask *)task
|
||||||
|
didCompleteWithError:(NSError *)error {
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
[self updateState:NetworkStreamStateError];
|
||||||
|
NSLog(@"Request failed with error: %@", error);
|
||||||
|
} else {
|
||||||
|
[self updateState:NetworkStreamStateCompleted];
|
||||||
|
NSLog(@"Request completed successfully. Total bytes: %lld", self.totalBytesReceived);
|
||||||
|
}
|
||||||
|
|
||||||
|
[self notifyCompletionWithError:error];
|
||||||
|
|
||||||
|
// 清理
|
||||||
|
[self.session finishTasksAndInvalidate];
|
||||||
|
self.dataTask = nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - URL Session Delegate (处理 SSL/认证)
|
||||||
|
|
||||||
|
- (void)URLSession:(NSURLSession *)session
|
||||||
|
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
|
||||||
|
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential * _Nullable))completionHandler {
|
||||||
|
|
||||||
|
// 处理 SSL 认证挑战
|
||||||
|
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
|
||||||
|
NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
|
||||||
|
completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
|
||||||
|
} else {
|
||||||
|
completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
70
CustomKeyboard/Network/WJXEventSource/WJXEventSource.h
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
//
|
||||||
|
// WJXEventSource.h
|
||||||
|
// WJXEventSource
|
||||||
|
//
|
||||||
|
// Created by JiuxingWang on 2025/2/9.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
#define WJX_EXTERN extern "C" __attribute__((visibility ("default")))
|
||||||
|
#else
|
||||||
|
#define WJX_EXTERN extern __attribute__((visibility ("default")))
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/// 消息事件
|
||||||
|
typedef NSString *WJXEventName NS_TYPED_EXTENSIBLE_ENUM;
|
||||||
|
|
||||||
|
/// 消息事件
|
||||||
|
WJX_EXTERN WJXEventName const WJXEventNameMessage;
|
||||||
|
|
||||||
|
/// readyState 变化事件
|
||||||
|
WJX_EXTERN WJXEventName const WJXEventNameReadyState;
|
||||||
|
|
||||||
|
/// open 事件
|
||||||
|
WJX_EXTERN WJXEventName const WJXEventNameOpen;
|
||||||
|
|
||||||
|
/// error 事件
|
||||||
|
WJX_EXTERN WJXEventName const WJXEventNameError;
|
||||||
|
|
||||||
|
typedef NS_ENUM(NSUInteger, WJXEventState) {
|
||||||
|
WJXEventStateConnecting = 0,
|
||||||
|
WJXEventStateOpen,
|
||||||
|
WJXEventStateClosed,
|
||||||
|
};
|
||||||
|
|
||||||
|
@interface WJXEvent : NSObject
|
||||||
|
|
||||||
|
@property (nonatomic, strong, nullable) id eventId;
|
||||||
|
|
||||||
|
@property (nonatomic, copy, nullable) NSString *event;
|
||||||
|
@property (nonatomic, copy, nullable) NSString *data;
|
||||||
|
|
||||||
|
@property (nonatomic, assign) WJXEventState readyState;
|
||||||
|
@property (nonatomic, strong, nullable) NSError *error;
|
||||||
|
|
||||||
|
- (instancetype)initWithReadyState:(WJXEventState)readyState;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
typedef void(^WJXEventSourceEventHandler)(WJXEvent *event);
|
||||||
|
|
||||||
|
@interface WJXEventSource : NSObject
|
||||||
|
|
||||||
|
@property (nonatomic, assign) BOOL ignoreRetryAction;
|
||||||
|
|
||||||
|
- (instancetype)initWithRquest:(NSURLRequest *)request;
|
||||||
|
|
||||||
|
- (void)addListener:(WJXEventSourceEventHandler)listener
|
||||||
|
forEvent:(WJXEventName)eventName
|
||||||
|
queue:(nullable NSOperationQueue *)queue;
|
||||||
|
|
||||||
|
- (void)open;
|
||||||
|
- (void)close;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
309
CustomKeyboard/Network/WJXEventSource/WJXEventSource.m
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
//
|
||||||
|
// WJXEventSource.m
|
||||||
|
// WJXEventSource
|
||||||
|
//
|
||||||
|
// Created by JiuxingWang on 2025/2/9.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import "WJXEventSource.h"
|
||||||
|
|
||||||
|
/// 消息事件
|
||||||
|
WJXEventName const WJXEventNameMessage = @"message";
|
||||||
|
|
||||||
|
/// readyState 变化事件
|
||||||
|
WJXEventName const WJXEventNameReadyState = @"readyState";
|
||||||
|
|
||||||
|
/// open 事件
|
||||||
|
WJXEventName const WJXEventNameOpen = @"open";
|
||||||
|
|
||||||
|
/// error 事件
|
||||||
|
WJXEventName const WJXEventNameError = @"error";
|
||||||
|
|
||||||
|
#pragma mark -
|
||||||
|
#pragma mark WJXEvent
|
||||||
|
|
||||||
|
@implementation WJXEvent
|
||||||
|
|
||||||
|
- (instancetype)initWithReadyState:(WJXEventState)readyState;
|
||||||
|
{
|
||||||
|
if (self = [super init]) {
|
||||||
|
self.readyState = readyState;
|
||||||
|
}
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSString *)description
|
||||||
|
{
|
||||||
|
NSString *state = nil;
|
||||||
|
switch (_readyState) {
|
||||||
|
case WJXEventStateConnecting: {
|
||||||
|
state = @"CONNECTING";
|
||||||
|
} break;
|
||||||
|
|
||||||
|
case WJXEventStateOpen: {
|
||||||
|
state = @"OPEN";
|
||||||
|
} break;
|
||||||
|
|
||||||
|
case WJXEventStateClosed: {
|
||||||
|
state = @"CLOSED";
|
||||||
|
} break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [NSString stringWithFormat:@"<%@: readyState: %@, id: %@; event: %@; data: %@>", [self class], state, _eventId, _event, _data];
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
|
||||||
|
#pragma mark -
|
||||||
|
#pragma mark WJXEventHandler
|
||||||
|
|
||||||
|
@interface WJXEventHandler : NSObject
|
||||||
|
|
||||||
|
@property (nonatomic, copy, nonnull) WJXEventSourceEventHandler handler;
|
||||||
|
@property (nonatomic, strong, nullable) NSOperationQueue *queue;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation WJXEventHandler
|
||||||
|
|
||||||
|
- (instancetype)initWithHandler:(WJXEventSourceEventHandler)handler queue:(NSOperationQueue *)queue
|
||||||
|
{
|
||||||
|
if (self = [super init]) {
|
||||||
|
self.handler = handler;
|
||||||
|
self.queue = queue;
|
||||||
|
}
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
|
||||||
|
#pragma mark -
|
||||||
|
#pragma mark WJXEventSource
|
||||||
|
|
||||||
|
@interface WJXEventSource () <NSURLSessionDataDelegate>
|
||||||
|
|
||||||
|
@property (nonatomic, strong) NSMutableURLRequest *request;
|
||||||
|
@property (nonatomic, strong) NSMutableDictionary<WJXEventName, NSMutableArray<WJXEventHandler *> *> *listeners;
|
||||||
|
|
||||||
|
@property (nonatomic, strong) NSURLSession *session;
|
||||||
|
@property (nonatomic, strong) NSURLSessionDataTask *dataTask;
|
||||||
|
@property (nonatomic, copy) NSString *lastEventId;
|
||||||
|
@property (nonatomic, assign) NSTimeInterval retryInterval;
|
||||||
|
|
||||||
|
@property (nonatomic, assign) BOOL closedByUser;
|
||||||
|
@property (nonatomic, strong) NSMutableData *buffer;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation WJXEventSource
|
||||||
|
|
||||||
|
- (instancetype)initWithRquest:(NSURLRequest *)request;
|
||||||
|
{
|
||||||
|
if (self = [super init]) {
|
||||||
|
self.request = [request mutableCopy];
|
||||||
|
self.listeners = [NSMutableDictionary dictionary];
|
||||||
|
self.session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration ephemeralSessionConfiguration] delegate:self delegateQueue:NSOperationQueue.mainQueue];
|
||||||
|
self.buffer = [NSMutableData data];
|
||||||
|
}
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)dealloc
|
||||||
|
{
|
||||||
|
[_session finishTasksAndInvalidate];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)addListener:(WJXEventSourceEventHandler)listener
|
||||||
|
forEvent:(WJXEventName)eventName
|
||||||
|
queue:(nullable NSOperationQueue *)queue;
|
||||||
|
{
|
||||||
|
if (nil == listener) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSMutableArray *listeners = self.listeners[eventName];
|
||||||
|
if (nil == listeners) {
|
||||||
|
self.listeners[eventName] = listeners = [NSMutableArray array];
|
||||||
|
}
|
||||||
|
[listeners addObject:[[WJXEventHandler alloc] initWithHandler:listener queue:queue]];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)open;
|
||||||
|
{
|
||||||
|
if (_lastEventId.length) {
|
||||||
|
[_request setValue:_lastEventId forHTTPHeaderField:@"Last-Event-ID"];
|
||||||
|
}
|
||||||
|
|
||||||
|
self.dataTask = [_session dataTaskWithRequest:_request];
|
||||||
|
[_dataTask resume];
|
||||||
|
|
||||||
|
WJXEvent *event = [[WJXEvent alloc] initWithReadyState:WJXEventStateConnecting];
|
||||||
|
[self _dispatchEvent:event forName:WJXEventNameReadyState];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)close;
|
||||||
|
{
|
||||||
|
self.closedByUser = YES;
|
||||||
|
[_dataTask cancel];
|
||||||
|
[_session finishTasksAndInvalidate];
|
||||||
|
_buffer = [NSMutableData data];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#pragma mark -
|
||||||
|
#pragma mark NSURLSessionDataDelegate
|
||||||
|
|
||||||
|
- (void)URLSession:(NSURLSession *)session
|
||||||
|
dataTask:(NSURLSessionDataTask *)dataTask
|
||||||
|
didReceiveResponse:(NSURLResponse *)response
|
||||||
|
completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler;
|
||||||
|
{
|
||||||
|
NSHTTPURLResponse *HTTPResponse = (NSHTTPURLResponse *)response;
|
||||||
|
if (200 == HTTPResponse.statusCode) {
|
||||||
|
WJXEvent *event = [[WJXEvent alloc] initWithReadyState:WJXEventStateOpen];
|
||||||
|
[self _dispatchEvent:event forName:WJXEventNameReadyState];
|
||||||
|
[self _dispatchEvent:event forName:WJXEventNameOpen];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nil != completionHandler) {
|
||||||
|
completionHandler(NSURLSessionResponseAllow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)URLSession:(NSURLSession *)session
|
||||||
|
dataTask:(NSURLSessionDataTask *)dataTask
|
||||||
|
didReceiveData:(NSData *)data;
|
||||||
|
{
|
||||||
|
[_buffer appendData:data];
|
||||||
|
[self _processBuffer];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)URLSession:(NSURLSession *)session
|
||||||
|
task:(NSURLSessionTask *)task
|
||||||
|
didCompleteWithError:(nullable NSError *)error;
|
||||||
|
{
|
||||||
|
if (_closedByUser) {
|
||||||
|
_buffer = [NSMutableData data];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
[self _dispatchPlainBufferIfNeeded];
|
||||||
|
|
||||||
|
WJXEvent *event = [[WJXEvent alloc] initWithReadyState:WJXEventStateClosed];
|
||||||
|
if (nil == (event.error = error)) {
|
||||||
|
event.error = [NSError errorWithDomain:@"WJXEventSource" code:event.readyState userInfo:@{
|
||||||
|
NSLocalizedDescriptionKey: @"Connection with the event source was closed without error",
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
[self _dispatchEvent:event forName:WJXEventNameReadyState];
|
||||||
|
|
||||||
|
if (nil != error) {
|
||||||
|
[self _dispatchEvent:event forName:WJXEventNameError];
|
||||||
|
if (!_ignoreRetryAction) {
|
||||||
|
[self performSelector:@selector(open) withObject:nil afterDelay:_retryInterval];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#pragma mark -
|
||||||
|
#pragma mark Private
|
||||||
|
|
||||||
|
- (void)_processBuffer
|
||||||
|
{
|
||||||
|
NSData *separatorLFLFData = [NSData dataWithBytes:"\n\n" length:2];
|
||||||
|
|
||||||
|
NSRange range = [_buffer rangeOfData:separatorLFLFData options:kNilOptions range:(NSRange) {
|
||||||
|
.length = _buffer.length
|
||||||
|
}];
|
||||||
|
|
||||||
|
while (NSNotFound != range.location) {
|
||||||
|
// Extract event data
|
||||||
|
NSData *eventData = [_buffer subdataWithRange:(NSRange) {
|
||||||
|
.length = range.location
|
||||||
|
}];
|
||||||
|
[_buffer replaceBytesInRange:(NSRange) {
|
||||||
|
.length = range.location + 2
|
||||||
|
} withBytes:NULL length:0];
|
||||||
|
|
||||||
|
[self _parseEventData:eventData];
|
||||||
|
|
||||||
|
// Look for next event
|
||||||
|
range = [_buffer rangeOfData:separatorLFLFData options:kNilOptions range:(NSRange) {
|
||||||
|
.length = _buffer.length
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)_parseEventData:(NSData *)data
|
||||||
|
{
|
||||||
|
WJXEvent *event = [[WJXEvent alloc] initWithReadyState:WJXEventStateOpen];
|
||||||
|
|
||||||
|
NSString *eventString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
|
||||||
|
if (eventString.length == 0) { return; }
|
||||||
|
NSArray *lines = [eventString componentsSeparatedByCharactersInSet:NSCharacterSet.newlineCharacterSet];
|
||||||
|
BOOL hasDataLine = NO;
|
||||||
|
for (NSString *line in lines) {
|
||||||
|
if ([line hasPrefix:@"id:"]) {
|
||||||
|
event.eventId = [[line substringFromIndex:3] stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceCharacterSet];
|
||||||
|
} else if ([line hasPrefix:@"event:"]) {
|
||||||
|
event.event = [[line substringFromIndex:6] stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceCharacterSet];
|
||||||
|
} else if ([line hasPrefix:@"data:"]) {
|
||||||
|
hasDataLine = YES;
|
||||||
|
NSString *data = [[line substringFromIndex:5] stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceCharacterSet];
|
||||||
|
event.data = event.data ? [event.data stringByAppendingFormat:@"\n%@", data] : data;
|
||||||
|
} else if ([line hasPrefix:@"retry:"]) {
|
||||||
|
NSString *retryString = [[line substringFromIndex:6] stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceCharacterSet];
|
||||||
|
self.retryInterval = [retryString doubleValue] / 1000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasDataLine) {
|
||||||
|
NSString *trimmed = [eventString stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
||||||
|
if (trimmed.length > 0) {
|
||||||
|
event.data = trimmed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.eventId) {
|
||||||
|
self.lastEventId = event.eventId;
|
||||||
|
}
|
||||||
|
|
||||||
|
[self _dispatchEvent:event forName:WJXEventNameMessage];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)_dispatchEvent:(WJXEvent *)event forName:(WJXEventName)name
|
||||||
|
{
|
||||||
|
NSMutableArray<WJXEventHandler *> *listeners = self.listeners[name];
|
||||||
|
[listeners enumerateObjectsUsingBlock:^(WJXEventHandler * _Nonnull handler, NSUInteger idx, BOOL * _Nonnull stop) {
|
||||||
|
NSOperationQueue *queue = handler.queue ?: NSOperationQueue.mainQueue;
|
||||||
|
[queue addOperationWithBlock:^{
|
||||||
|
handler.handler(event);
|
||||||
|
}];
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)_dispatchPlainBufferIfNeeded
|
||||||
|
{
|
||||||
|
if (_buffer.length == 0) { return; }
|
||||||
|
NSData *data = [_buffer copy];
|
||||||
|
[_buffer setLength:0];
|
||||||
|
if (data.length == 0) { return; }
|
||||||
|
[self _parseEventData:data];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#pragma mark -
|
||||||
|
#pragma mark Setters
|
||||||
|
|
||||||
|
- (void)setDataTask:(NSURLSessionDataTask *)dataTask
|
||||||
|
{
|
||||||
|
self.closedByUser = YES; {
|
||||||
|
[_dataTask cancel];
|
||||||
|
_dataTask = dataTask;
|
||||||
|
} self.closedByUser = NO;
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
@@ -12,10 +12,30 @@
|
|||||||
// You will also need to set the Prefix Header build setting of one or more of your targets to reference this file.
|
// You will also need to set the Prefix Header build setting of one or more of your targets to reference this file.
|
||||||
|
|
||||||
#define SCREEN_WIDTH [UIScreen mainScreen].bounds.size.width
|
#define SCREEN_WIDTH [UIScreen mainScreen].bounds.size.width
|
||||||
#define SCREEN_HEIGHT [UIScreen mainScreen].bounds.size.width
|
#define SCREEN_HEIGHT [UIScreen mainScreen].bounds.size.height
|
||||||
#define imageNamed(s) [UIImage imageNamed:s]
|
#define imageNamed(s) [UIImage imageNamed:s]
|
||||||
|
|
||||||
|
// 公共配置
|
||||||
|
#import "KBConfig.h"
|
||||||
|
#import "KBAPI.h" // 接口路径宏(统一管理)
|
||||||
#import "Masonry.h"
|
#import "Masonry.h"
|
||||||
|
#import "KBHUD.h" // 复用 App 内的 HUD 封装
|
||||||
|
#import "KBLocalizationManager.h" // 复用多语言封装(可在扩展内使用)
|
||||||
|
#import "KBMaiPointReporter.h"
|
||||||
|
//#import "KBLog.h"
|
||||||
|
|
||||||
|
|
||||||
|
// 通用链接(Universal Links)统一配置
|
||||||
|
// 配置好 AASA 与 Associated Domains 后,只需修改这里即可切换域名/path。
|
||||||
|
#define KB_UL_BASE @"https://app.tknb.net/ul" // 与 Associated Domains 一致
|
||||||
|
#define KB_UL_LOGIN KB_UL_BASE @"/login"
|
||||||
|
#define KB_UL_SETTINGS KB_UL_BASE @"/settings"
|
||||||
|
|
||||||
|
// 在扩展内,启用 URL Bridge(仅在显式的用户点击动作中使用)
|
||||||
|
// 这样即便宿主 App(如备忘录)拒绝 extensionContext 的 openURL,仍可通过响应链兜底拉起容器 App。
|
||||||
|
#ifndef KB_URL_BRIDGE_ENABLE
|
||||||
|
#define KB_URL_BRIDGE_ENABLE 1
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
#endif /* PrefixHeader_pch */
|
#endif /* PrefixHeader_pch */
|
||||||
|
|||||||
BIN
CustomKeyboard/Resource/002.zip
Normal file
BIN
CustomKeyboard/Resource/Christmas.zip
Normal file
249
CustomKeyboard/Resource/KBSkinIconMap.strings
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
/* 字母 q(小写) */
|
||||||
|
"letter_q_lower" = "key_q";
|
||||||
|
/* 字母 Q(大写) */
|
||||||
|
"letter_q_upper" = "key_q_up";
|
||||||
|
|
||||||
|
/* 字母 w(小写) */
|
||||||
|
"letter_w_lower" = "key_w";
|
||||||
|
/* 字母 W(大写) */
|
||||||
|
"letter_w_upper" = "key_w_up";
|
||||||
|
|
||||||
|
/* 字母 e(小写) */
|
||||||
|
"letter_e_lower" = "key_e";
|
||||||
|
/* 字母 E(大写) */
|
||||||
|
"letter_e_upper" = "key_e_up";
|
||||||
|
|
||||||
|
/* 字母 r(小写) */
|
||||||
|
"letter_r_lower" = "key_r";
|
||||||
|
/* 字母 R(大写) */
|
||||||
|
"letter_r_upper" = "key_r_up";
|
||||||
|
|
||||||
|
/* 字母 t(小写) */
|
||||||
|
"letter_t_lower" = "key_t";
|
||||||
|
/* 字母 T(大写) */
|
||||||
|
"letter_t_upper" = "key_t_up";
|
||||||
|
|
||||||
|
/* 字母 y(小写) */
|
||||||
|
"letter_y_lower" = "key_y";
|
||||||
|
/* 字母 Y(大写) */
|
||||||
|
"letter_y_upper" = "key_y_up";
|
||||||
|
|
||||||
|
/* 字母 u(小写) */
|
||||||
|
"letter_u_lower" = "key_u";
|
||||||
|
/* 字母 U(大写) */
|
||||||
|
"letter_u_upper" = "key_u_up";
|
||||||
|
|
||||||
|
/* 字母 i(小写) */
|
||||||
|
"letter_i_lower" = "key_i";
|
||||||
|
/* 字母 I(大写) */
|
||||||
|
"letter_i_upper" = "key_i_up";
|
||||||
|
|
||||||
|
/* 字母 o(小写) */
|
||||||
|
"letter_o_lower" = "key_o";
|
||||||
|
/* 字母 O(大写) */
|
||||||
|
"letter_o_upper" = "key_o_up";
|
||||||
|
|
||||||
|
/* 字母 p(小写) */
|
||||||
|
"letter_p_lower" = "key_p";
|
||||||
|
/* 字母 P(大写) */
|
||||||
|
"letter_p_upper" = "key_p_up";
|
||||||
|
|
||||||
|
/* 字母 a(小写) */
|
||||||
|
"letter_a_lower" = "key_a";
|
||||||
|
/* 字母 A(大写) */
|
||||||
|
"letter_a_upper" = "key_a_up";
|
||||||
|
|
||||||
|
/* 字母 s(小写) */
|
||||||
|
"letter_s_lower" = "key_s";
|
||||||
|
/* 字母 S(大写) */
|
||||||
|
"letter_s_upper" = "key_s_up";
|
||||||
|
|
||||||
|
/* 字母 d(小写) */
|
||||||
|
"letter_d_lower" = "key_d";
|
||||||
|
/* 字母 D(大写) */
|
||||||
|
"letter_d_upper" = "key_d_up";
|
||||||
|
|
||||||
|
/* 字母 f(小写) */
|
||||||
|
"letter_f_lower" = "key_f";
|
||||||
|
/* 字母 F(大写) */
|
||||||
|
"letter_f_upper" = "key_f_up";
|
||||||
|
|
||||||
|
/* 字母 g(小写) */
|
||||||
|
"letter_g_lower" = "key_g";
|
||||||
|
/* 字母 G(大写) */
|
||||||
|
"letter_g_upper" = "key_f_up";
|
||||||
|
|
||||||
|
/* 字母 h(小写) */
|
||||||
|
"letter_h_lower" = "key_h";
|
||||||
|
/* 字母 H(大写) */
|
||||||
|
"letter_h_upper" = "key_h_up";
|
||||||
|
|
||||||
|
/* 字母 j(小写) */
|
||||||
|
"letter_j_lower" = "key_j";
|
||||||
|
/* 字母 J(大写) */
|
||||||
|
"letter_j_upper" = "key_j_up";
|
||||||
|
|
||||||
|
/* 字母 k(小写) */
|
||||||
|
"letter_k_lower" = "key_k";
|
||||||
|
/* 字母 K(大写) */
|
||||||
|
"letter_k_upper" = "key_k_up";
|
||||||
|
|
||||||
|
/* 字母 l(小写) */
|
||||||
|
"letter_l_lower" = "key_l";
|
||||||
|
/* 字母 L(大写) */
|
||||||
|
"letter_l_upper" = "key_l_up";
|
||||||
|
|
||||||
|
/* 字母 z(小写) */
|
||||||
|
"letter_z_lower" = "key_z";
|
||||||
|
/* 字母 Z(大写) */
|
||||||
|
"letter_z_upper" = "key_z_up";
|
||||||
|
|
||||||
|
/* 字母 x(小写) */
|
||||||
|
"letter_x_lower" = "key_x";
|
||||||
|
/* 字母 X(大写) */
|
||||||
|
"letter_x_upper" = "key_x_up";
|
||||||
|
|
||||||
|
/* 字母 c(小写) */
|
||||||
|
"letter_c_lower" = "key_c";
|
||||||
|
/* 字母 C(大写) */
|
||||||
|
"letter_c_upper" = "key_c_up";
|
||||||
|
|
||||||
|
/* 字母 v(小写) */
|
||||||
|
"letter_v_lower" = "key_v";
|
||||||
|
/* 字母 V(大写) */
|
||||||
|
"letter_v_upper" = "key_v_up";
|
||||||
|
|
||||||
|
/* 字母 b(小写) */
|
||||||
|
"letter_b_lower" = "key_b";
|
||||||
|
/* 字母 B(大写) */
|
||||||
|
"letter_b_upper" = "key_b_up";
|
||||||
|
|
||||||
|
/* 字母 n(小写) */
|
||||||
|
"letter_n_lower" = "key_n";
|
||||||
|
/* 字母 N(大写) */
|
||||||
|
"letter_n_upper" = "key_n_up";
|
||||||
|
|
||||||
|
/* 字母 m(小写) */
|
||||||
|
"letter_m_lower" = "key_m";
|
||||||
|
/* 字母 M(大写) */
|
||||||
|
"letter_m_upper" = "key_m_up";
|
||||||
|
|
||||||
|
/* 数字 1 */
|
||||||
|
"digit_1" = "key_1";
|
||||||
|
/* 数字 2 */
|
||||||
|
"digit_2" = "key_2";
|
||||||
|
/* 数字 3 */
|
||||||
|
"digit_3" = "key_3";
|
||||||
|
/* 数字 4 */
|
||||||
|
"digit_4" = "key_4";
|
||||||
|
/* 数字 5 */
|
||||||
|
"digit_5" = "key_5";
|
||||||
|
/* 数字 6 */
|
||||||
|
"digit_6" = "key_6";
|
||||||
|
/* 数字 7 */
|
||||||
|
"digit_7" = "key_7";
|
||||||
|
/* 数字 8 */
|
||||||
|
"digit_8" = "key_8";
|
||||||
|
/* 数字 9 */
|
||||||
|
"digit_9" = "key_9";
|
||||||
|
/* 数字 0 */
|
||||||
|
"digit_0" = "key_0";
|
||||||
|
|
||||||
|
/* '-' */
|
||||||
|
"sym_minus" = "key_minus";
|
||||||
|
/* '/' */
|
||||||
|
"sym_slash" = "key_slash";
|
||||||
|
/* ':' */
|
||||||
|
"sym_colon" = "key_colon";
|
||||||
|
/* ';' */
|
||||||
|
"sym_semicolon" = "key_semicolon";
|
||||||
|
/* '(' */
|
||||||
|
"sym_paren_l" = "key_paren_l";
|
||||||
|
/* ')' */
|
||||||
|
"sym_paren_r" = "key_paren_r";
|
||||||
|
/* '$' */
|
||||||
|
"sym_dollar" = "key_dollar";
|
||||||
|
/* '&' */
|
||||||
|
"sym_amp" = "key_amp";
|
||||||
|
/* '@' */
|
||||||
|
"sym_at" = "key_at";
|
||||||
|
/* 双引号 " */
|
||||||
|
"sym_quote_double" = "key_quote_d";
|
||||||
|
|
||||||
|
/* ',' */
|
||||||
|
"sym_comma" = "key_comma";
|
||||||
|
/* '.' */
|
||||||
|
"sym_dot" = "key_dot";
|
||||||
|
/* '?' */
|
||||||
|
"sym_question" = "key_question";
|
||||||
|
/* '!' */
|
||||||
|
"sym_exclam" = "key_exclam";
|
||||||
|
/* 单引号 ' */
|
||||||
|
"sym_quote_single" = "key_quote";
|
||||||
|
|
||||||
|
/* '[' */
|
||||||
|
"sym_bracket_l" = "key_bracket_l";
|
||||||
|
/* ']' */
|
||||||
|
"sym_bracket_r" = "key_bracket_r";
|
||||||
|
/* '{' */
|
||||||
|
"sym_brace_l" = "key_brace_l";
|
||||||
|
/* '}' */
|
||||||
|
"sym_brace_r" = "key_brace_r";
|
||||||
|
/* '#' */
|
||||||
|
"sym_hash" = "key_hash";
|
||||||
|
/* '%' */
|
||||||
|
"sym_percent" = "key_percent";
|
||||||
|
/* '^' */
|
||||||
|
"sym_caret" = "key_caret";
|
||||||
|
/* '*' */
|
||||||
|
"sym_asterisk" = "key_asterisk";
|
||||||
|
/* '+' */
|
||||||
|
"sym_plus" = "key_plus";
|
||||||
|
/* '=' */
|
||||||
|
"sym_equal" = "key_equal";
|
||||||
|
|
||||||
|
/* '_' */
|
||||||
|
"sym_underscore" = "key_underscore";
|
||||||
|
/* '\' */
|
||||||
|
"sym_backslash" = "key_backslash";
|
||||||
|
/* '|' */
|
||||||
|
"sym_pipe" = "key_pipe";
|
||||||
|
/* '~' */
|
||||||
|
"sym_tilde" = "key_tilde";
|
||||||
|
/* '<' */
|
||||||
|
"sym_lt" = "key_lt";
|
||||||
|
/* '>' */
|
||||||
|
"sym_gt" = "key_gt";
|
||||||
|
/* '¥' */
|
||||||
|
"sym_money" = "key_money";
|
||||||
|
/* '€' */
|
||||||
|
"sym_euro" = "key_euro";
|
||||||
|
/* '£' */
|
||||||
|
"sym_pound" = "key_pound";
|
||||||
|
/* '•' */
|
||||||
|
"sym_bullet" = "key_bullet";
|
||||||
|
|
||||||
|
/* 空格键 */
|
||||||
|
"space" = "key_space";
|
||||||
|
/* 删除键(⌫) */
|
||||||
|
"backspace" = "key_del";
|
||||||
|
/* Shift(⇧) */
|
||||||
|
"shift" = "key_up";
|
||||||
|
/* Shift(⇧)大写 */
|
||||||
|
"shift_upper" = "key_up_upper";
|
||||||
|
/* 字母面板左下角 "123" */
|
||||||
|
"mode_123" = "key_123";
|
||||||
|
/* 数字面板左下角 "abc" */
|
||||||
|
"mode_abc" = "key_abc";
|
||||||
|
/* 数字面板内 "123 -> #+=" */
|
||||||
|
"symbols_toggle_more" = "key_symbols_more";
|
||||||
|
/* 数字面板内 "#+= -> 123" */
|
||||||
|
"symbols_toggle_123" = "key_symbols_123";
|
||||||
|
/* 自定义 AI 功能键 */
|
||||||
|
"ai" = "key_ai";
|
||||||
|
/* Emoji功能键 */
|
||||||
|
//"emoji" = "key_emoji";
|
||||||
|
"emoji_panel" = "key_emoji";
|
||||||
|
/* 发送/换行键 */
|
||||||
|
"return" = "key_send";
|
||||||
|
|
||||||
14802
CustomKeyboard/Resource/emoji_categories.json
Normal file
BIN
CustomKeyboard/Resource/fense.zip
Normal file
234454
CustomKeyboard/Resource/kb_words.txt
Normal file
22
CustomKeyboard/Utils/KBBackspaceLongPressHandler.h
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
//
|
||||||
|
// KBBackspaceLongPressHandler.h
|
||||||
|
// CustomKeyboard
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <UIKit/UIKit.h>
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
@interface KBBackspaceLongPressHandler : NSObject
|
||||||
|
|
||||||
|
- (instancetype)initWithContainerView:(UIView *)containerView;
|
||||||
|
|
||||||
|
/// 配置删除按钮(包含长按删除;可选是否显示“上滑清空”提示)
|
||||||
|
- (void)bindDeleteButton:(nullable UIView *)button showClearLabel:(BOOL)showClearLabel;
|
||||||
|
|
||||||
|
/// 触发“上滑清空”逻辑(可用于功能面板的清空按钮)
|
||||||
|
- (void)performClearAction;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
662
CustomKeyboard/Utils/KBBackspaceLongPressHandler.m
Normal file
@@ -0,0 +1,662 @@
|
|||||||
|
//
|
||||||
|
// KBBackspaceLongPressHandler.m
|
||||||
|
// CustomKeyboard
|
||||||
|
//
|
||||||
|
|
||||||
|
#import "KBBackspaceLongPressHandler.h"
|
||||||
|
#import "KBResponderUtils.h"
|
||||||
|
#import "KBSkinManager.h"
|
||||||
|
#import "KBBackspaceUndoManager.h"
|
||||||
|
#import "KBInputBufferManager.h"
|
||||||
|
|
||||||
|
static const NSTimeInterval kKBBackspaceLongPressMinDuration = 0.35;
|
||||||
|
static const NSTimeInterval kKBBackspaceRepeatInterval = 0.06;
|
||||||
|
static const NSTimeInterval kKBBackspaceChunkStartDelay = 0.6;
|
||||||
|
static const NSTimeInterval kKBBackspaceChunkRepeatInterval = 0.1;
|
||||||
|
static const NSTimeInterval kKBBackspaceChunkFastDelay = 1.2;
|
||||||
|
static const NSInteger kKBBackspaceChunkSize = 8;
|
||||||
|
static const NSInteger kKBBackspaceChunkSizeFast = 16;
|
||||||
|
static const CGFloat kKBBackspaceClearLabelCornerRadius = 8.0;
|
||||||
|
static const CGFloat kKBBackspaceClearLabelHeight = 34;
|
||||||
|
static const CGFloat kKBBackspaceClearLabelPaddingX = 10.0;
|
||||||
|
static const CGFloat kKBBackspaceClearLabelTopGap = 6.0;
|
||||||
|
static const CGFloat kKBBackspaceClearLabelHorizontalInset = 6.0;
|
||||||
|
static const NSTimeInterval kKBBackspaceClearBatchInterval = 0.02;
|
||||||
|
static const NSInteger kKBBackspaceClearMaxDeletes = 10000;
|
||||||
|
static const NSInteger kKBBackspaceClearEmptyContextMaxRounds = 40;
|
||||||
|
static const NSInteger kKBBackspaceClearMaxStep = 80;
|
||||||
|
static const NSInteger kKBBackspaceClearDeletesPerTick = 10;
|
||||||
|
|
||||||
|
typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
|
||||||
|
KBBackspaceChunkClassUnknown = 0,
|
||||||
|
KBBackspaceChunkClassWhitespace,
|
||||||
|
KBBackspaceChunkClassASCIIWord,
|
||||||
|
KBBackspaceChunkClassPunctuation,
|
||||||
|
KBBackspaceChunkClassOther
|
||||||
|
};
|
||||||
|
|
||||||
|
typedef NS_ENUM(NSInteger, KBClearPhase) {
|
||||||
|
KBClearPhaseSkipWhitespace = 0,
|
||||||
|
KBClearPhaseSkipTrailingBoundary,
|
||||||
|
KBClearPhaseDeleteUntilBoundary
|
||||||
|
};
|
||||||
|
|
||||||
|
@interface KBBackspaceLongPressHandler ()
|
||||||
|
@property (nonatomic, weak) UIView *containerView;
|
||||||
|
@property (nonatomic, weak) UIView *backspaceButton;
|
||||||
|
@property (nonatomic, strong) UILongPressGestureRecognizer *longPress;
|
||||||
|
@property (nonatomic, assign) BOOL showClearLabelEnabled;
|
||||||
|
@property (nonatomic, assign) BOOL backspaceHoldActive;
|
||||||
|
@property (nonatomic, assign) NSTimeInterval backspaceHoldStartTime;
|
||||||
|
@property (nonatomic, assign) BOOL backspaceChunkModeActive;
|
||||||
|
@property (nonatomic, assign) BOOL backspaceClearHighlighted;
|
||||||
|
@property (nonatomic, assign) NSUInteger backspaceHoldToken;
|
||||||
|
@property (nonatomic, assign) BOOL backspaceHasLastTouchPoint;
|
||||||
|
@property (nonatomic, assign) CGPoint backspaceLastTouchPointInSelf;
|
||||||
|
@property (nonatomic, assign) NSUInteger backspaceClearToken;
|
||||||
|
@property (nonatomic, strong) UILabel *backspaceClearLabel;
|
||||||
|
@property (nonatomic, copy) NSString *pendingClearBefore;
|
||||||
|
@property (nonatomic, copy) NSString *pendingClearAfter;
|
||||||
|
@property (nonatomic, assign) KBClearPhase backspaceClearPhase;
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation KBBackspaceLongPressHandler
|
||||||
|
|
||||||
|
- (instancetype)initWithContainerView:(UIView *)containerView {
|
||||||
|
if (self = [super init]) {
|
||||||
|
_containerView = containerView;
|
||||||
|
_backspaceClearPhase = KBClearPhaseSkipWhitespace;
|
||||||
|
}
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)bindDeleteButton:(UIView *)button showClearLabel:(BOOL)showClearLabel {
|
||||||
|
if (self.backspaceButton == button) { return; }
|
||||||
|
|
||||||
|
if (self.longPress && self.backspaceButton) {
|
||||||
|
[self.backspaceButton removeGestureRecognizer:self.longPress];
|
||||||
|
}
|
||||||
|
self.backspaceButton = button;
|
||||||
|
self.showClearLabelEnabled = showClearLabel;
|
||||||
|
self.backspaceHoldActive = NO;
|
||||||
|
self.backspaceChunkModeActive = NO;
|
||||||
|
self.backspaceClearHighlighted = NO;
|
||||||
|
self.backspaceHasLastTouchPoint = NO;
|
||||||
|
self.backspaceHoldToken += 1;
|
||||||
|
[self kb_hideBackspaceClearLabel];
|
||||||
|
self.pendingClearBefore = nil;
|
||||||
|
self.pendingClearAfter = nil;
|
||||||
|
|
||||||
|
if (!button) { return; }
|
||||||
|
|
||||||
|
self.longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self
|
||||||
|
action:@selector(onBackspaceLongPress:)];
|
||||||
|
self.longPress.minimumPressDuration = kKBBackspaceLongPressMinDuration;
|
||||||
|
self.longPress.allowableMovement = CGFLOAT_MAX;
|
||||||
|
self.longPress.cancelsTouchesInView = YES;
|
||||||
|
[button addGestureRecognizer:self.longPress];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)performClearAction {
|
||||||
|
[self kb_clearAllInput];
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - Long Press
|
||||||
|
|
||||||
|
- (void)onBackspaceLongPress:(UILongPressGestureRecognizer *)gr {
|
||||||
|
UIView *hostView = [self kb_hostView];
|
||||||
|
if (!hostView) { return; }
|
||||||
|
if (gr) {
|
||||||
|
self.backspaceLastTouchPointInSelf = [gr locationInView:hostView];
|
||||||
|
self.backspaceHasLastTouchPoint = YES;
|
||||||
|
}
|
||||||
|
switch (gr.state) {
|
||||||
|
case UIGestureRecognizerStateBegan: {
|
||||||
|
UIResponder *start = (UIResponder *)([self kb_hostView] ?: self.backspaceButton);
|
||||||
|
UIInputViewController *ivc = KBFindInputViewController(start);
|
||||||
|
if (ivc) {
|
||||||
|
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
||||||
|
[[KBInputBufferManager shared] refreshFromProxyIfPossible:proxy];
|
||||||
|
[[KBInputBufferManager shared] prepareSnapshotForDeleteWithContextBefore:proxy.documentContextBeforeInput
|
||||||
|
after:proxy.documentContextAfterInput];
|
||||||
|
}
|
||||||
|
if (self.showClearLabelEnabled) {
|
||||||
|
[self kb_capturePendingClearSnapshotIfNeeded];
|
||||||
|
[[KBInputBufferManager shared] beginPendingClearSnapshot];
|
||||||
|
}
|
||||||
|
self.backspaceHoldToken += 1;
|
||||||
|
NSUInteger token = self.backspaceHoldToken;
|
||||||
|
self.backspaceHoldActive = YES;
|
||||||
|
self.backspaceHoldStartTime = [NSDate date].timeIntervalSinceReferenceDate;
|
||||||
|
self.backspaceChunkModeActive = NO;
|
||||||
|
[self kb_setBackspaceClearHighlighted:NO];
|
||||||
|
[self kb_hideBackspaceClearLabel];
|
||||||
|
if (self.showClearLabelEnabled) {
|
||||||
|
[self kb_showBackspaceClearLabelIfNeeded];
|
||||||
|
}
|
||||||
|
[self kb_backspaceStepForToken:token];
|
||||||
|
} break;
|
||||||
|
case UIGestureRecognizerStateChanged: {
|
||||||
|
[self kb_handleBackspaceLongPressChanged:gr];
|
||||||
|
} break;
|
||||||
|
case UIGestureRecognizerStateEnded:
|
||||||
|
case UIGestureRecognizerStateCancelled:
|
||||||
|
case UIGestureRecognizerStateFailed: {
|
||||||
|
[self kb_handleBackspaceLongPressEnded:gr];
|
||||||
|
} break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - Delete Steps
|
||||||
|
|
||||||
|
- (void)kb_backspaceStepForToken:(NSUInteger)token {
|
||||||
|
if (!self.backspaceHoldActive) { return; }
|
||||||
|
if (token != self.backspaceHoldToken) { return; }
|
||||||
|
UIResponder *start = (UIResponder *)([self kb_hostView] ?: self.backspaceButton);
|
||||||
|
UIInputViewController *ivc = KBFindInputViewController(start);
|
||||||
|
if (!ivc) { self.backspaceHoldActive = NO; return; }
|
||||||
|
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
||||||
|
NSString *before = proxy.documentContextBeforeInput ?: @"";
|
||||||
|
if (before.length == 0) { before = [KBInputBufferManager shared].liveText ?: @""; }
|
||||||
|
NSTimeInterval elapsed = [NSDate date].timeIntervalSinceReferenceDate - self.backspaceHoldStartTime;
|
||||||
|
NSInteger deleteCount = 1;
|
||||||
|
if (before.length > 0) {
|
||||||
|
deleteCount = [self kb_backspaceDeleteCountForContext:before elapsed:elapsed];
|
||||||
|
}
|
||||||
|
if (!self.backspaceChunkModeActive && elapsed >= kKBBackspaceChunkStartDelay) {
|
||||||
|
self.backspaceChunkModeActive = YES;
|
||||||
|
if (self.showClearLabelEnabled) {
|
||||||
|
[self kb_showBackspaceClearLabelIfNeeded];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
[[KBBackspaceUndoManager shared] captureAndDeleteBackwardFromProxy:proxy count:(NSUInteger)deleteCount];
|
||||||
|
[[KBInputBufferManager shared] applyHoldDeleteCount:(NSUInteger)deleteCount];
|
||||||
|
|
||||||
|
NSTimeInterval interval = [self kb_backspaceRepeatIntervalForElapsed:elapsed];
|
||||||
|
__weak typeof(self) weakSelf = self;
|
||||||
|
dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
|
||||||
|
(int64_t)(interval * NSEC_PER_SEC)),
|
||||||
|
dispatch_get_main_queue(), ^{
|
||||||
|
__strong typeof(weakSelf) selfStrong = weakSelf;
|
||||||
|
[selfStrong kb_backspaceStepForToken:token];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSTimeInterval)kb_backspaceRepeatIntervalForElapsed:(NSTimeInterval)elapsed {
|
||||||
|
if (elapsed >= kKBBackspaceChunkStartDelay) {
|
||||||
|
return kKBBackspaceChunkRepeatInterval;
|
||||||
|
}
|
||||||
|
return kKBBackspaceRepeatInterval;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSInteger)kb_backspaceDeleteCountForContext:(NSString *)context elapsed:(NSTimeInterval)elapsed {
|
||||||
|
if (elapsed < kKBBackspaceChunkStartDelay) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
NSInteger maxCount = (elapsed >= kKBBackspaceChunkFastDelay)
|
||||||
|
? kKBBackspaceChunkSizeFast : kKBBackspaceChunkSize;
|
||||||
|
return [self kb_backspaceChunkDeleteCountForContext:context maxCount:maxCount];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSInteger)kb_backspaceChunkDeleteCountForContext:(NSString *)context maxCount:(NSInteger)maxCount {
|
||||||
|
if (context.length == 0) { return 1; }
|
||||||
|
|
||||||
|
static NSCharacterSet *whitespaceSet = nil;
|
||||||
|
static NSCharacterSet *asciiWordSet = nil;
|
||||||
|
static NSCharacterSet *punctuationSet = nil;
|
||||||
|
static dispatch_once_t onceToken;
|
||||||
|
dispatch_once(&onceToken, ^{
|
||||||
|
whitespaceSet = [NSCharacterSet whitespaceAndNewlineCharacterSet];
|
||||||
|
asciiWordSet = [NSCharacterSet characterSetWithCharactersInString:
|
||||||
|
@"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"];
|
||||||
|
NSMutableCharacterSet *punct = [[NSCharacterSet punctuationCharacterSet] mutableCopy];
|
||||||
|
// 补齐常见中文/全角标点(避免 chunk 总是只删 1 个符号)
|
||||||
|
[punct addCharactersInString:@",。!?;:、()【】《》“”‘’·…—"];
|
||||||
|
punctuationSet = [punct copy];
|
||||||
|
});
|
||||||
|
|
||||||
|
__block NSInteger deleteCount = 0;
|
||||||
|
typedef NS_ENUM(NSInteger, KBBackspaceChunkPhase) {
|
||||||
|
KBBackspaceChunkPhaseWhitespace = 0,
|
||||||
|
KBBackspaceChunkPhasePunctuation,
|
||||||
|
KBBackspaceChunkPhaseCore
|
||||||
|
};
|
||||||
|
__block KBBackspaceChunkPhase phase = KBBackspaceChunkPhaseWhitespace;
|
||||||
|
__block KBBackspaceChunkClass coreClass = KBBackspaceChunkClassUnknown;
|
||||||
|
|
||||||
|
[context enumerateSubstringsInRange:NSMakeRange(0, context.length)
|
||||||
|
options:NSStringEnumerationByComposedCharacterSequences | NSStringEnumerationReverse
|
||||||
|
usingBlock:^(NSString *substring, __unused NSRange substringRange, __unused NSRange enclosingRange, BOOL *stop) {
|
||||||
|
if (substring.length == 0) { return; }
|
||||||
|
if (deleteCount >= maxCount) {
|
||||||
|
*stop = YES;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
KBBackspaceChunkClass currentClass = KBBackspaceChunkClassOther;
|
||||||
|
if ([substring rangeOfCharacterFromSet:whitespaceSet].location != NSNotFound) {
|
||||||
|
currentClass = KBBackspaceChunkClassWhitespace;
|
||||||
|
} else if ([substring rangeOfCharacterFromSet:punctuationSet].location != NSNotFound) {
|
||||||
|
currentClass = KBBackspaceChunkClassPunctuation;
|
||||||
|
} else if ([substring rangeOfCharacterFromSet:asciiWordSet].location != NSNotFound) {
|
||||||
|
currentClass = KBBackspaceChunkClassASCIIWord;
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOL consumed = NO;
|
||||||
|
while (!consumed) {
|
||||||
|
if (phase == KBBackspaceChunkPhaseWhitespace) {
|
||||||
|
if (currentClass == KBBackspaceChunkClassWhitespace) {
|
||||||
|
deleteCount += 1;
|
||||||
|
consumed = YES;
|
||||||
|
} else {
|
||||||
|
phase = KBBackspaceChunkPhasePunctuation;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (phase == KBBackspaceChunkPhasePunctuation) {
|
||||||
|
if (currentClass == KBBackspaceChunkClassPunctuation) {
|
||||||
|
deleteCount += 1;
|
||||||
|
consumed = YES;
|
||||||
|
} else {
|
||||||
|
phase = KBBackspaceChunkPhaseCore;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// phase == Core:连续删同一类(ASCII 单词 / 其它),让效果更像微信“几个字一组”
|
||||||
|
if (coreClass == KBBackspaceChunkClassUnknown) {
|
||||||
|
coreClass = currentClass;
|
||||||
|
}
|
||||||
|
if (currentClass != coreClass) {
|
||||||
|
*stop = YES;
|
||||||
|
consumed = YES;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
deleteCount += 1;
|
||||||
|
consumed = YES;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deleteCount >= maxCount) {
|
||||||
|
*stop = YES;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
|
||||||
|
return MAX(deleteCount, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSInteger)kb_clearDeleteCountForContext:(NSString *)context
|
||||||
|
hitBoundary:(BOOL *)hitBoundary {
|
||||||
|
if (context.length == 0) {
|
||||||
|
if (hitBoundary) { *hitBoundary = NO; }
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static NSCharacterSet *sentenceBoundarySet = nil;
|
||||||
|
static NSCharacterSet *whitespaceSet = nil;
|
||||||
|
static dispatch_once_t onceToken;
|
||||||
|
dispatch_once(&onceToken, ^{
|
||||||
|
sentenceBoundarySet = [NSCharacterSet characterSetWithCharactersInString:@".!?。!?"];
|
||||||
|
whitespaceSet = [NSCharacterSet whitespaceAndNewlineCharacterSet];
|
||||||
|
});
|
||||||
|
|
||||||
|
NSInteger length = context.length;
|
||||||
|
NSInteger end = length;
|
||||||
|
while (end > 0) {
|
||||||
|
unichar ch = [context characterAtIndex:end - 1];
|
||||||
|
if ([whitespaceSet characterIsMember:ch]) {
|
||||||
|
end -= 1;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NSInteger searchEnd = end;
|
||||||
|
while (searchEnd > 0) {
|
||||||
|
unichar ch = [context characterAtIndex:searchEnd - 1];
|
||||||
|
if ([sentenceBoundarySet characterIsMember:ch]) {
|
||||||
|
searchEnd -= 1;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NSInteger boundaryIndex = NSNotFound;
|
||||||
|
for (NSInteger i = searchEnd - 1; i >= 0; i--) {
|
||||||
|
unichar ch = [context characterAtIndex:i];
|
||||||
|
if ([sentenceBoundarySet characterIsMember:ch]) {
|
||||||
|
boundaryIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOL boundaryFound = (boundaryIndex != NSNotFound);
|
||||||
|
NSInteger deleteCount = length;
|
||||||
|
if (boundaryIndex != NSNotFound) {
|
||||||
|
deleteCount = length - (boundaryIndex + 1);
|
||||||
|
}
|
||||||
|
deleteCount = MAX(deleteCount, 1);
|
||||||
|
if (hitBoundary) {
|
||||||
|
*hitBoundary = boundaryFound;
|
||||||
|
}
|
||||||
|
return MIN(deleteCount, kKBBackspaceClearMaxStep);
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - Long Press State
|
||||||
|
|
||||||
|
- (void)kb_handleBackspaceLongPressChanged:(UILongPressGestureRecognizer *)gr {
|
||||||
|
if (!self.backspaceHoldActive) { return; }
|
||||||
|
if (!self.showClearLabelEnabled) { return; }
|
||||||
|
[self kb_showBackspaceClearLabelIfNeeded];
|
||||||
|
UIView *hostView = [self kb_hostView];
|
||||||
|
if (!hostView) { return; }
|
||||||
|
CGPoint point = [gr locationInView:hostView];
|
||||||
|
self.backspaceLastTouchPointInSelf = point;
|
||||||
|
self.backspaceHasLastTouchPoint = YES;
|
||||||
|
BOOL inside = [self kb_isPointInsideBackspaceClearLabel:point];
|
||||||
|
[self kb_setBackspaceClearHighlighted:inside];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)kb_handleBackspaceLongPressEnded:(UILongPressGestureRecognizer *)gr {
|
||||||
|
BOOL shouldClear = NO;
|
||||||
|
if (self.showClearLabelEnabled) {
|
||||||
|
shouldClear = self.backspaceClearHighlighted;
|
||||||
|
if (!shouldClear) {
|
||||||
|
UIView *hostView = [self kb_hostView];
|
||||||
|
CGPoint point = CGPointZero;
|
||||||
|
if (gr && hostView) {
|
||||||
|
point = [gr locationInView:hostView];
|
||||||
|
} else if (self.backspaceHasLastTouchPoint) {
|
||||||
|
point = self.backspaceLastTouchPointInSelf;
|
||||||
|
}
|
||||||
|
shouldClear = [self kb_isPointInsideBackspaceClearLabel:point];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#if DEBUG
|
||||||
|
NSLog(@"[kb_handleBackspaceLongPressEnded] shouldClear=%@ highlighted=%@ labelHidden=%@",
|
||||||
|
shouldClear ? @"YES" : @"NO",
|
||||||
|
self.backspaceClearHighlighted ? @"YES" : @"NO",
|
||||||
|
self.backspaceClearLabel.hidden ? @"YES" : @"NO");
|
||||||
|
#endif
|
||||||
|
self.backspaceHoldActive = NO;
|
||||||
|
self.backspaceChunkModeActive = NO;
|
||||||
|
self.backspaceHoldToken += 1;
|
||||||
|
self.backspaceHasLastTouchPoint = NO;
|
||||||
|
[self kb_hideBackspaceClearLabel];
|
||||||
|
if (shouldClear) {
|
||||||
|
[self kb_clearAllInput];
|
||||||
|
} else {
|
||||||
|
self.pendingClearBefore = nil;
|
||||||
|
self.pendingClearAfter = nil;
|
||||||
|
[[KBInputBufferManager shared] clearPendingClearSnapshot];
|
||||||
|
[[KBInputBufferManager shared] commitLiveToManual];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - Clear Label
|
||||||
|
|
||||||
|
- (void)kb_showBackspaceClearLabelIfNeeded {
|
||||||
|
UIView *hostView = [self kb_hostView];
|
||||||
|
if (!hostView || !self.backspaceButton) { return; }
|
||||||
|
UILabel *label = self.backspaceClearLabel;
|
||||||
|
[self kb_refreshBackspaceClearLabelColors];
|
||||||
|
if (!label.superview) {
|
||||||
|
[hostView addSubview:label];
|
||||||
|
}
|
||||||
|
[self kb_updateBackspaceClearLabelFrame];
|
||||||
|
[hostView bringSubviewToFront:label];
|
||||||
|
if (label.hidden) {
|
||||||
|
label.alpha = 0.0;
|
||||||
|
label.hidden = NO;
|
||||||
|
[self kb_playLightHaptic];
|
||||||
|
[UIView animateWithDuration:0.12 animations:^{
|
||||||
|
label.alpha = 1.0;
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)kb_hideBackspaceClearLabel {
|
||||||
|
if (!_backspaceClearLabel || _backspaceClearLabel.hidden) { return; }
|
||||||
|
_backspaceClearLabel.hidden = YES;
|
||||||
|
_backspaceClearLabel.alpha = 1.0;
|
||||||
|
[self kb_setBackspaceClearHighlighted:NO];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)kb_updateBackspaceClearLabelFrame {
|
||||||
|
UIView *hostView = [self kb_hostView];
|
||||||
|
if (!hostView || !self.backspaceButton || !self.backspaceClearLabel) { return; }
|
||||||
|
CGRect btnFrame = [self.backspaceButton convertRect:self.backspaceButton.bounds toView:hostView];
|
||||||
|
UILabel *label = self.backspaceClearLabel;
|
||||||
|
CGSize textSize = [label sizeThatFits:CGSizeMake(CGFLOAT_MAX, kKBBackspaceClearLabelHeight)];
|
||||||
|
CGFloat width = MAX(textSize.width + kKBBackspaceClearLabelPaddingX * 2.0, 60.0);
|
||||||
|
CGFloat height = kKBBackspaceClearLabelHeight;
|
||||||
|
CGFloat x = CGRectGetMidX(btnFrame) - width * 0.5;
|
||||||
|
CGFloat y = CGRectGetMinY(btnFrame) - height - kKBBackspaceClearLabelTopGap;
|
||||||
|
if (x < kKBBackspaceClearLabelHorizontalInset) { x = kKBBackspaceClearLabelHorizontalInset; }
|
||||||
|
CGFloat maxX = CGRectGetWidth(hostView.bounds) - kKBBackspaceClearLabelHorizontalInset - width;
|
||||||
|
if (x > maxX) { x = maxX; }
|
||||||
|
if (y < 0) { y = 0; }
|
||||||
|
label.frame = CGRectIntegral(CGRectMake(x, y, width, height));
|
||||||
|
}
|
||||||
|
|
||||||
|
- (BOOL)kb_isPointInsideBackspaceClearLabel:(CGPoint)point {
|
||||||
|
if (!self.backspaceClearLabel || self.backspaceClearLabel.hidden) { return NO; }
|
||||||
|
[self kb_updateBackspaceClearLabelFrame];
|
||||||
|
CGRect hitFrame = CGRectInset(self.backspaceClearLabel.frame, -12.0, -10.0);
|
||||||
|
return CGRectContainsPoint(hitFrame, point);
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)kb_setBackspaceClearHighlighted:(BOOL)highlighted {
|
||||||
|
if (self.backspaceClearHighlighted == highlighted) { return; }
|
||||||
|
self.backspaceClearHighlighted = highlighted;
|
||||||
|
[self kb_refreshBackspaceClearLabelColors];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)kb_refreshBackspaceClearLabelColors {
|
||||||
|
UILabel *label = self.backspaceClearLabel;
|
||||||
|
label.textColor = [KBSkinManager shared].current.keyTextColor ?: UIColor.blackColor;
|
||||||
|
label.backgroundColor = self.backspaceClearHighlighted
|
||||||
|
? [self kb_backspaceClearLabelHighlightedColor]
|
||||||
|
: [self kb_backspaceClearLabelNormalColor];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (UIColor *)kb_backspaceClearLabelNormalColor {
|
||||||
|
KBSkinTheme *t = [KBSkinManager shared].current;
|
||||||
|
return t.keyHighlightBackground ?: [UIColor colorWithWhite:0.9 alpha:1.0];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (UIColor *)kb_backspaceClearLabelHighlightedColor {
|
||||||
|
KBSkinTheme *t = [KBSkinManager shared].current;
|
||||||
|
return t.accentColor ?: t.keyHighlightBackground ?: [UIColor colorWithWhite:0.8 alpha:1.0];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)kb_playLightHaptic {
|
||||||
|
if (@available(iOS 10.0, *)) {
|
||||||
|
UIImpactFeedbackGenerator *gen = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleLight];
|
||||||
|
[gen prepare];
|
||||||
|
[gen impactOccurred];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (UILabel *)backspaceClearLabel {
|
||||||
|
if (!_backspaceClearLabel) {
|
||||||
|
UILabel *label = [[UILabel alloc] initWithFrame:CGRectZero];
|
||||||
|
label.text = KBLocalized(@"Clear");
|
||||||
|
label.textAlignment = NSTextAlignmentCenter;
|
||||||
|
label.font = [UIFont systemFontOfSize:16 weight:UIFontWeightSemibold];
|
||||||
|
label.textColor = [KBSkinManager shared].current.keyTextColor ?: UIColor.blackColor;
|
||||||
|
label.backgroundColor = [self kb_backspaceClearLabelNormalColor];
|
||||||
|
label.layer.cornerRadius = kKBBackspaceClearLabelCornerRadius;
|
||||||
|
label.layer.masksToBounds = YES;
|
||||||
|
label.hidden = YES;
|
||||||
|
label.userInteractionEnabled = NO;
|
||||||
|
_backspaceClearLabel = label;
|
||||||
|
}
|
||||||
|
return _backspaceClearLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - Clear
|
||||||
|
|
||||||
|
- (void)kb_clearAllInput {
|
||||||
|
UIResponder *start = (UIResponder *)([self kb_hostView] ?: self.backspaceButton);
|
||||||
|
UIInputViewController *ivc = KBFindInputViewController(start);
|
||||||
|
if (ivc) {
|
||||||
|
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
||||||
|
[[KBInputBufferManager shared] refreshFromProxyIfPossible:proxy];
|
||||||
|
}
|
||||||
|
self.pendingClearBefore = nil;
|
||||||
|
self.pendingClearAfter = nil;
|
||||||
|
[[KBInputBufferManager shared] clearPendingClearSnapshot];
|
||||||
|
self.backspaceClearToken += 1;
|
||||||
|
self.backspaceClearPhase = KBClearPhaseSkipWhitespace;
|
||||||
|
NSUInteger token = self.backspaceClearToken;
|
||||||
|
[self kb_clearAllInputStepForToken:token guard:0 emptyRounds:0];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)kb_clearAllInputStepForToken:(NSUInteger)token
|
||||||
|
guard:(NSInteger)guard
|
||||||
|
emptyRounds:(NSInteger)emptyRounds {
|
||||||
|
if (token != self.backspaceClearToken) { return; }
|
||||||
|
UIResponder *start = (UIResponder *)([self kb_hostView] ?: self.backspaceButton);
|
||||||
|
UIInputViewController *ivc = KBFindInputViewController(start);
|
||||||
|
if (!ivc) { return; }
|
||||||
|
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
||||||
|
NSInteger nextEmptyRounds = emptyRounds;
|
||||||
|
static NSCharacterSet *stopBoundarySet = nil;
|
||||||
|
static NSCharacterSet *trailingBoundarySet = nil;
|
||||||
|
static NSCharacterSet *trailingWhitespaceSet = nil;
|
||||||
|
static dispatch_once_t onceToken;
|
||||||
|
dispatch_once(&onceToken, ^{
|
||||||
|
// stopBoundary: 遇到这些符号就停(不删除它)
|
||||||
|
// - 句末符号:. ! ? 。!?
|
||||||
|
// - 省略号:…(中文里“……”常用作句/段落的停顿)
|
||||||
|
// - 换行:\n \r(段落边界,避免一次“清空”跨段把全文删完)
|
||||||
|
stopBoundarySet = [NSCharacterSet characterSetWithCharactersInString:@".!?。!?…\n\r\u2028\u2029"];
|
||||||
|
|
||||||
|
// trailingBoundary: 允许作为“尾部句末符号”先删掉,再继续删上一句(更接近微信体验)
|
||||||
|
// 注意:不要把换行/省略号放进来,否则可能跨段/跨停顿继续删。
|
||||||
|
trailingBoundarySet = [NSCharacterSet characterSetWithCharactersInString:@".!?。!?"];
|
||||||
|
|
||||||
|
// trailingWhitespace: 只跳过空格/Tab(不包含换行,换行由 stopBoundarySet 处理)
|
||||||
|
trailingWhitespaceSet = [NSCharacterSet whitespaceCharacterSet];
|
||||||
|
});
|
||||||
|
KBClearPhase phase = self.backspaceClearPhase;
|
||||||
|
|
||||||
|
NSInteger deletedThisTick = 0;
|
||||||
|
BOOL shouldStop = NO;
|
||||||
|
NSString *lastBefore = nil;
|
||||||
|
for (NSInteger i = 0; i < kKBBackspaceClearDeletesPerTick; i++) {
|
||||||
|
NSString *before = proxy.documentContextBeforeInput ?: @"";
|
||||||
|
if (before.length == 0) {
|
||||||
|
nextEmptyRounds += 1;
|
||||||
|
// 宿主(微信/QQ 等)可能在长文本场景下返回空 context,即使还有很多内容。
|
||||||
|
// 为了避免一次“清空”误删全文:一旦拿不到 before,就立刻停止本次清空。
|
||||||
|
shouldStop = YES;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
nextEmptyRounds = 0;
|
||||||
|
|
||||||
|
if (lastBefore && [before isEqualToString:lastBefore] && deletedThisTick > 0) {
|
||||||
|
// 宿主未及时刷新 context,留到下一 tick 再继续,避免越界/重复记录
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
lastBefore = before;
|
||||||
|
|
||||||
|
// 取最后一个组合字符
|
||||||
|
__block NSString *lastChar = @"";
|
||||||
|
[before enumerateSubstringsInRange:NSMakeRange(0, before.length)
|
||||||
|
options:NSStringEnumerationByComposedCharacterSequences | NSStringEnumerationReverse
|
||||||
|
usingBlock:^(NSString *substring, __unused NSRange substringRange, __unused NSRange enclosingRange, BOOL *stop) {
|
||||||
|
lastChar = substring ?: @"";
|
||||||
|
*stop = YES;
|
||||||
|
}];
|
||||||
|
if (lastChar.length == 0) { break; }
|
||||||
|
|
||||||
|
BOOL isWhitespace = ([lastChar rangeOfCharacterFromSet:trailingWhitespaceSet].location != NSNotFound);
|
||||||
|
BOOL isStopBoundary = ([lastChar rangeOfCharacterFromSet:stopBoundarySet].location != NSNotFound);
|
||||||
|
BOOL isTrailingBoundary = ([lastChar rangeOfCharacterFromSet:trailingBoundarySet].location != NSNotFound);
|
||||||
|
|
||||||
|
if (phase == KBClearPhaseSkipWhitespace) {
|
||||||
|
if (isWhitespace) {
|
||||||
|
[[KBBackspaceUndoManager shared] captureAndDeleteBackwardFromProxy:proxy count:1];
|
||||||
|
[[KBInputBufferManager shared] applyClearDeleteCount:1];
|
||||||
|
deletedThisTick += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
phase = KBClearPhaseSkipTrailingBoundary;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (phase == KBClearPhaseSkipTrailingBoundary) {
|
||||||
|
if (isTrailingBoundary) {
|
||||||
|
[[KBBackspaceUndoManager shared] captureAndDeleteBackwardFromProxy:proxy count:1];
|
||||||
|
[[KBInputBufferManager shared] applyClearDeleteCount:1];
|
||||||
|
deletedThisTick += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
phase = KBClearPhaseDeleteUntilBoundary;
|
||||||
|
}
|
||||||
|
|
||||||
|
// phase == DeleteUntilBoundary
|
||||||
|
if (isStopBoundary) {
|
||||||
|
shouldStop = YES; // 保留该句末符号
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
[[KBBackspaceUndoManager shared] captureAndDeleteBackwardFromProxy:proxy count:1];
|
||||||
|
[[KBInputBufferManager shared] applyClearDeleteCount:1];
|
||||||
|
deletedThisTick += 1;
|
||||||
|
if (guard + deletedThisTick >= kKBBackspaceClearMaxDeletes) { break; }
|
||||||
|
if (deletedThisTick >= kKBBackspaceClearMaxStep) { break; }
|
||||||
|
}
|
||||||
|
|
||||||
|
self.backspaceClearPhase = phase;
|
||||||
|
NSInteger nextGuard = guard + deletedThisTick;
|
||||||
|
if (nextGuard >= kKBBackspaceClearMaxDeletes ||
|
||||||
|
nextEmptyRounds > kKBBackspaceClearEmptyContextMaxRounds ||
|
||||||
|
shouldStop) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
__weak typeof(self) weakSelf = self;
|
||||||
|
dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
|
||||||
|
(int64_t)(kKBBackspaceClearBatchInterval * NSEC_PER_SEC)),
|
||||||
|
dispatch_get_main_queue(), ^{
|
||||||
|
__strong typeof(weakSelf) selfStrong = weakSelf;
|
||||||
|
[selfStrong kb_clearAllInputStepForToken:token
|
||||||
|
guard:nextGuard
|
||||||
|
emptyRounds:nextEmptyRounds];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - Helpers
|
||||||
|
|
||||||
|
- (UIView *)kb_hostView {
|
||||||
|
if (self.containerView) { return self.containerView; }
|
||||||
|
return self.backspaceButton.superview;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)kb_captureDeletionSnapshotIfNeeded {
|
||||||
|
if ([KBBackspaceUndoManager shared].hasUndo) { return; }
|
||||||
|
UIResponder *start = (UIResponder *)([self kb_hostView] ?: self.backspaceButton);
|
||||||
|
UIInputViewController *ivc = KBFindInputViewController(start);
|
||||||
|
if (!ivc) { return; }
|
||||||
|
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
||||||
|
[[KBBackspaceUndoManager shared] recordDeletionSnapshotBefore:proxy.documentContextBeforeInput
|
||||||
|
after:proxy.documentContextAfterInput];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)kb_capturePendingClearSnapshotIfNeeded {
|
||||||
|
if (self.pendingClearBefore.length > 0 || self.pendingClearAfter.length > 0) { return; }
|
||||||
|
UIResponder *start = (UIResponder *)([self kb_hostView] ?: self.backspaceButton);
|
||||||
|
UIInputViewController *ivc = KBFindInputViewController(start);
|
||||||
|
if (!ivc) { return; }
|
||||||
|
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
||||||
|
self.pendingClearBefore = proxy.documentContextBeforeInput ?: @"";
|
||||||
|
self.pendingClearAfter = proxy.documentContextAfterInput ?: @"";
|
||||||
|
#if DEBUG
|
||||||
|
NSLog(@"[kb_capturePendingClearSnapshotIfNeeded/before] len=%lu text=%@", (unsigned long)self.pendingClearBefore.length, self.pendingClearBefore);
|
||||||
|
NSLog(@"[kb_capturePendingClearSnapshotIfNeeded/after] len=%lu text=%@", (unsigned long)self.pendingClearAfter.length, self.pendingClearAfter);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
35
CustomKeyboard/Utils/KBBackspaceUndoManager.h
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
//
|
||||||
|
// KBBackspaceUndoManager.h
|
||||||
|
// CustomKeyboard
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <UIKit/UIKit.h>
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
extern NSNotificationName const KBBackspaceUndoStateDidChangeNotification;
|
||||||
|
|
||||||
|
@interface KBBackspaceUndoManager : NSObject
|
||||||
|
|
||||||
|
@property (nonatomic, readonly) BOOL hasUndo;
|
||||||
|
|
||||||
|
+ (instancetype)shared;
|
||||||
|
|
||||||
|
/// 记录一次删除前的快照(不改变撤销按钮显示)。
|
||||||
|
- (void)recordDeletionSnapshotBefore:(NSString *)before after:(NSString *)after;
|
||||||
|
|
||||||
|
/// 记录一次“立刻清空”删除的内容(基于 documentContextBeforeInput/AfterInput)。
|
||||||
|
- (void)recordClearWithContextBefore:(NSString *)before after:(NSString *)after;
|
||||||
|
|
||||||
|
/// 记录本次将被 deleteBackward 的内容,并执行 deleteBackward(支持多次累计,撤销时一次性插回)。
|
||||||
|
- (void)captureAndDeleteBackwardFromProxy:(id<UITextDocumentProxy>)proxy count:(NSUInteger)count;
|
||||||
|
|
||||||
|
/// 在指定 responder 处执行撤销(向光标处插回删除的内容)
|
||||||
|
- (void)performUndoFromResponder:(UIResponder *)responder;
|
||||||
|
|
||||||
|
/// 非删除行为触发时,清理撤销状态
|
||||||
|
- (void)registerNonClearAction;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
304
CustomKeyboard/Utils/KBBackspaceUndoManager.m
Normal file
@@ -0,0 +1,304 @@
|
|||||||
|
//
|
||||||
|
// KBBackspaceUndoManager.m
|
||||||
|
// CustomKeyboard
|
||||||
|
//
|
||||||
|
|
||||||
|
#import "KBBackspaceUndoManager.h"
|
||||||
|
#import "KBResponderUtils.h"
|
||||||
|
#import "KBInputBufferManager.h"
|
||||||
|
|
||||||
|
NSNotificationName const KBBackspaceUndoStateDidChangeNotification = @"KBBackspaceUndoStateDidChangeNotification";
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
static NSString *KBLogString(NSString *tag, NSString *text) {
|
||||||
|
NSString *safeTag = tag ?: @"";
|
||||||
|
NSString *safeText = text ?: @"";
|
||||||
|
if (safeText.length <= 2000) {
|
||||||
|
return [NSString stringWithFormat:@"[%@] len=%lu text=%@", safeTag, (unsigned long)safeText.length, safeText];
|
||||||
|
}
|
||||||
|
NSString *head = [safeText substringToIndex:800];
|
||||||
|
NSString *tail = [safeText substringFromIndex:safeText.length - 800];
|
||||||
|
return [NSString stringWithFormat:@"[%@] len=%lu head=%@ ... tail=%@", safeTag, (unsigned long)safeText.length, head, tail];
|
||||||
|
}
|
||||||
|
#define KB_UNDO_LOG(tag, text) NSLog(@"%@", KBLogString((tag), (text)))
|
||||||
|
#else
|
||||||
|
#define KB_UNDO_LOG(tag, text) do {} while(0)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
typedef NS_ENUM(NSInteger, KBUndoSnapshotSource) {
|
||||||
|
KBUndoSnapshotSourceNone = 0,
|
||||||
|
KBUndoSnapshotSourceDeletionSnapshot,
|
||||||
|
KBUndoSnapshotSourceClear
|
||||||
|
};
|
||||||
|
|
||||||
|
@interface KBBackspaceUndoManager ()
|
||||||
|
@property (nonatomic, copy) NSString *undoText;
|
||||||
|
@property (nonatomic, assign) NSInteger undoAfterLength;
|
||||||
|
@property (nonatomic, assign) BOOL hasUndo;
|
||||||
|
@property (nonatomic, assign) KBUndoSnapshotSource snapshotSource;
|
||||||
|
@property (nonatomic, strong) NSMutableArray<NSString *> *undoDeletedPieces;
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation KBBackspaceUndoManager
|
||||||
|
|
||||||
|
+ (instancetype)shared {
|
||||||
|
static KBBackspaceUndoManager *mgr = nil;
|
||||||
|
static dispatch_once_t onceToken;
|
||||||
|
dispatch_once(&onceToken, ^{
|
||||||
|
mgr = [[KBBackspaceUndoManager alloc] init];
|
||||||
|
});
|
||||||
|
return mgr;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (instancetype)init {
|
||||||
|
if (self = [super init]) {
|
||||||
|
_undoText = @"";
|
||||||
|
_undoAfterLength = 0;
|
||||||
|
_snapshotSource = KBUndoSnapshotSourceNone;
|
||||||
|
_undoDeletedPieces = [NSMutableArray array];
|
||||||
|
}
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)captureAndDeleteBackwardFromProxy:(id<UITextDocumentProxy>)proxy count:(NSUInteger)count {
|
||||||
|
if (!proxy || count == 0) { return; }
|
||||||
|
|
||||||
|
NSString *selected = proxy.selectedText ?: @"";
|
||||||
|
NSString *ctxBefore = proxy.documentContextBeforeInput ?: @"";
|
||||||
|
NSString *ctxAfter = proxy.documentContextAfterInput ?: @"";
|
||||||
|
NSUInteger ctxLen = ctxBefore.length + ctxAfter.length;
|
||||||
|
BOOL isSelectAllLike = (selected.length > 0 &&
|
||||||
|
(ctxLen == 0 || selected.length >= MAX((NSUInteger)40, ctxLen * 2)));
|
||||||
|
if (isSelectAllLike) {
|
||||||
|
// “全选删除”在微信/QQ中通常拿不到可靠的全文,因此禁用撤销,避免插回错误/不完整内容。
|
||||||
|
if (self.hasUndo) {
|
||||||
|
[self registerNonClearAction];
|
||||||
|
}
|
||||||
|
#if DEBUG
|
||||||
|
KB_UNDO_LOG(@"captureAndDelete/selectAllDisableUndo", selected);
|
||||||
|
#endif
|
||||||
|
[proxy deleteBackward];
|
||||||
|
[[KBInputBufferManager shared] resetWithText:@""];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!self.hasUndo) {
|
||||||
|
[self.undoDeletedPieces removeAllObjects];
|
||||||
|
self.undoText = @"";
|
||||||
|
self.undoAfterLength = 0;
|
||||||
|
self.snapshotSource = KBUndoSnapshotSourceDeletionSnapshot;
|
||||||
|
[self kb_updateHasUndo:YES];
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOL didAppend = NO;
|
||||||
|
NSString *lastObservedBefore = nil;
|
||||||
|
for (NSUInteger i = 0; i < count; i++) {
|
||||||
|
NSString *before = proxy.documentContextBeforeInput ?: @"";
|
||||||
|
if (before.length > 0) {
|
||||||
|
// 若宿主在同一 runloop 内不更新 context,则跳过记录,避免把同一个字符重复记录成“多句”。
|
||||||
|
if (lastObservedBefore && [before isEqualToString:lastObservedBefore]) {
|
||||||
|
// still delete, but don't record
|
||||||
|
} else {
|
||||||
|
NSString *piece = [self kb_lastComposedCharacterFromString:before];
|
||||||
|
if (piece.length > 0) {
|
||||||
|
[self.undoDeletedPieces addObject:piece];
|
||||||
|
didAppend = YES;
|
||||||
|
}
|
||||||
|
lastObservedBefore = before;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
[proxy deleteBackward];
|
||||||
|
}
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
if (didAppend) {
|
||||||
|
NSUInteger piecesCount = self.undoDeletedPieces.count;
|
||||||
|
if (piecesCount <= 20) {
|
||||||
|
KB_UNDO_LOG(@"captureAndDelete/undoInsertTextNow", [self kb_buildUndoInsertTextFromPieces]);
|
||||||
|
} else if (piecesCount % 50 == 0) {
|
||||||
|
NSString *lastPiece = self.undoDeletedPieces.lastObject ?: @"";
|
||||||
|
NSLog(@"[captureAndDelete/undoPieces] pieces=%lu lastPiece=%@",
|
||||||
|
(unsigned long)piecesCount,
|
||||||
|
lastPiece);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)recordDeletionSnapshotBefore:(NSString *)before after:(NSString *)after {
|
||||||
|
if (self.hasUndo) { return; }
|
||||||
|
NSString *pending = [KBInputBufferManager shared].pendingClearSnapshot;
|
||||||
|
NSString *manual = [KBInputBufferManager shared].manualSnapshot;
|
||||||
|
NSString *fallbackText = (pending.length > 0) ? pending : ((manual.length > 0) ? manual : [KBInputBufferManager shared].liveText);
|
||||||
|
if (fallbackText.length > 0) {
|
||||||
|
self.undoText = fallbackText;
|
||||||
|
self.undoAfterLength = 0;
|
||||||
|
self.snapshotSource = KBUndoSnapshotSourceDeletionSnapshot;
|
||||||
|
KB_UNDO_LOG(@"recordDeletionSnapshot/fallback", self.undoText);
|
||||||
|
[self kb_updateHasUndo:YES];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
NSString *safeBefore = before ?: @"";
|
||||||
|
NSString *safeAfter = after ?: @"";
|
||||||
|
NSString *full = [safeBefore stringByAppendingString:safeAfter];
|
||||||
|
if (full.length == 0) { return; }
|
||||||
|
self.undoText = full;
|
||||||
|
self.undoAfterLength = (NSInteger)safeAfter.length;
|
||||||
|
self.snapshotSource = KBUndoSnapshotSourceDeletionSnapshot;
|
||||||
|
KB_UNDO_LOG(@"recordDeletionSnapshot/context", self.undoText);
|
||||||
|
[self kb_updateHasUndo:YES];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)recordClearWithContextBefore:(NSString *)before after:(NSString *)after {
|
||||||
|
NSString *pending = [KBInputBufferManager shared].pendingClearSnapshot;
|
||||||
|
NSString *manual = [KBInputBufferManager shared].manualSnapshot;
|
||||||
|
NSString *fallbackText = (pending.length > 0) ? pending : ((manual.length > 0) ? manual : [KBInputBufferManager shared].liveText);
|
||||||
|
|
||||||
|
NSString *safeBefore = before ?: @"";
|
||||||
|
NSString *safeAfter = after ?: @"";
|
||||||
|
NSString *contextText = [[safeBefore stringByAppendingString:safeAfter] copy];
|
||||||
|
|
||||||
|
NSString *candidate = (fallbackText.length > 0) ? fallbackText : contextText;
|
||||||
|
NSInteger candidateAfterLen = (fallbackText.length > 0) ? 0 : (NSInteger)safeAfter.length;
|
||||||
|
|
||||||
|
if (candidate.length == 0) { return; }
|
||||||
|
|
||||||
|
KB_UNDO_LOG(@"recordClear/candidate", candidate);
|
||||||
|
|
||||||
|
if (self.undoText.length > 0) {
|
||||||
|
if (self.snapshotSource == KBUndoSnapshotSourceClear) {
|
||||||
|
KB_UNDO_LOG(@"recordClear/ignored(alreadyClear)", self.undoText);
|
||||||
|
[self kb_updateHasUndo:YES];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (self.snapshotSource == KBUndoSnapshotSourceDeletionSnapshot) {
|
||||||
|
if (candidate.length > self.undoText.length) {
|
||||||
|
self.undoText = candidate;
|
||||||
|
self.undoAfterLength = candidateAfterLen;
|
||||||
|
KB_UNDO_LOG(@"recordClear/upgradedFromDeletion", self.undoText);
|
||||||
|
} else {
|
||||||
|
KB_UNDO_LOG(@"recordClear/keepDeletionSnapshot", self.undoText);
|
||||||
|
}
|
||||||
|
self.snapshotSource = KBUndoSnapshotSourceClear;
|
||||||
|
[self kb_updateHasUndo:YES];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.undoText = candidate;
|
||||||
|
self.undoAfterLength = candidateAfterLen;
|
||||||
|
self.snapshotSource = KBUndoSnapshotSourceClear;
|
||||||
|
KB_UNDO_LOG(@"recordClear/set", self.undoText);
|
||||||
|
[self kb_updateHasUndo:YES];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)performUndoFromResponder:(UIResponder *)responder {
|
||||||
|
if (!self.hasUndo) { return; }
|
||||||
|
UIInputViewController *ivc = KBFindInputViewController(responder);
|
||||||
|
if (!ivc) { return; }
|
||||||
|
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
||||||
|
NSString *curBefore = proxy.documentContextBeforeInput ?: @"";
|
||||||
|
NSString *curAfter = proxy.documentContextAfterInput ?: @"";
|
||||||
|
KB_UNDO_LOG(@"performUndo/currentBefore", curBefore);
|
||||||
|
KB_UNDO_LOG(@"performUndo/currentAfter", curAfter);
|
||||||
|
NSString *insertText = [self kb_buildUndoInsertTextFromPieces];
|
||||||
|
if (insertText.length > 0) {
|
||||||
|
KB_UNDO_LOG(@"performUndo/insertDeletedText", insertText);
|
||||||
|
[proxy insertText:insertText];
|
||||||
|
[[KBInputBufferManager shared] appendText:insertText];
|
||||||
|
} else if (self.undoText.length > 0) {
|
||||||
|
KB_UNDO_LOG(@"performUndo/fallbackUndoText", self.undoText);
|
||||||
|
[self kb_clearAllTextForProxy:proxy];
|
||||||
|
[proxy insertText:self.undoText];
|
||||||
|
if (self.undoAfterLength > 0 &&
|
||||||
|
[proxy respondsToSelector:@selector(adjustTextPositionByCharacterOffset:)]) {
|
||||||
|
[proxy adjustTextPositionByCharacterOffset:-self.undoAfterLength];
|
||||||
|
}
|
||||||
|
[[KBInputBufferManager shared] resetWithText:self.undoText];
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.undoText = @"";
|
||||||
|
self.undoAfterLength = 0;
|
||||||
|
self.snapshotSource = KBUndoSnapshotSourceNone;
|
||||||
|
[self.undoDeletedPieces removeAllObjects];
|
||||||
|
[self kb_updateHasUndo:NO];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)registerNonClearAction {
|
||||||
|
if (!self.hasUndo) { return; }
|
||||||
|
if (self.undoText.length > 0) {
|
||||||
|
KB_UNDO_LOG(@"registerNonClearAction/clearUndoText", self.undoText);
|
||||||
|
}
|
||||||
|
if (self.undoDeletedPieces.count > 0) {
|
||||||
|
KB_UNDO_LOG(@"registerNonClearAction/clearDeletedPieces", [self kb_buildUndoInsertTextFromPieces]);
|
||||||
|
}
|
||||||
|
self.undoText = @"";
|
||||||
|
self.undoAfterLength = 0;
|
||||||
|
self.snapshotSource = KBUndoSnapshotSourceNone;
|
||||||
|
[self.undoDeletedPieces removeAllObjects];
|
||||||
|
[self kb_updateHasUndo:NO];
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - Helpers
|
||||||
|
|
||||||
|
- (void)kb_updateHasUndo:(BOOL)hasUndo {
|
||||||
|
if (self.hasUndo == hasUndo) { return; }
|
||||||
|
self.hasUndo = hasUndo;
|
||||||
|
[[NSNotificationCenter defaultCenter] postNotificationName:KBBackspaceUndoStateDidChangeNotification object:self];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSString *)kb_lastComposedCharacterFromString:(NSString *)text {
|
||||||
|
if (text.length == 0) { return @""; }
|
||||||
|
__block NSString *last = @"";
|
||||||
|
[text enumerateSubstringsInRange:NSMakeRange(0, text.length)
|
||||||
|
options:NSStringEnumerationByComposedCharacterSequences | NSStringEnumerationReverse
|
||||||
|
usingBlock:^(NSString *substring, __unused NSRange substringRange, __unused NSRange enclosingRange, BOOL *stop) {
|
||||||
|
last = substring ?: @"";
|
||||||
|
*stop = YES;
|
||||||
|
}];
|
||||||
|
return last ?: @"";
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSString *)kb_buildUndoInsertTextFromPieces {
|
||||||
|
if (self.undoDeletedPieces.count == 0) { return @""; }
|
||||||
|
NSMutableString *result = [NSMutableString string];
|
||||||
|
for (NSInteger i = (NSInteger)self.undoDeletedPieces.count - 1; i >= 0; i--) {
|
||||||
|
NSString *piece = self.undoDeletedPieces[(NSUInteger)i] ?: @"";
|
||||||
|
if (piece.length == 0) { continue; }
|
||||||
|
[result appendString:piece];
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const NSInteger kKBUndoClearMaxRounds = 200;
|
||||||
|
|
||||||
|
- (void)kb_clearAllTextForProxy:(id<UITextDocumentProxy>)proxy {
|
||||||
|
if (!proxy) { return; }
|
||||||
|
|
||||||
|
if ([proxy respondsToSelector:@selector(adjustTextPositionByCharacterOffset:)]) {
|
||||||
|
NSInteger guard = 0;
|
||||||
|
NSString *contextAfter = proxy.documentContextAfterInput ?: @"";
|
||||||
|
while (contextAfter.length > 0 && guard < kKBUndoClearMaxRounds) {
|
||||||
|
NSInteger offset = (NSInteger)contextAfter.length;
|
||||||
|
[proxy adjustTextPositionByCharacterOffset:offset];
|
||||||
|
for (NSUInteger i = 0; i < contextAfter.length; i++) {
|
||||||
|
[proxy deleteBackward];
|
||||||
|
}
|
||||||
|
guard += 1;
|
||||||
|
contextAfter = proxy.documentContextAfterInput ?: @"";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NSInteger guard = 0;
|
||||||
|
NSString *contextBefore = proxy.documentContextBeforeInput ?: @"";
|
||||||
|
while (contextBefore.length > 0 && guard < kKBUndoClearMaxRounds) {
|
||||||
|
for (NSUInteger i = 0; i < contextBefore.length; i++) {
|
||||||
|
[proxy deleteBackward];
|
||||||
|
}
|
||||||
|
guard += 1;
|
||||||
|
contextBefore = proxy.documentContextBeforeInput ?: @"";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||