Compare commits
508 Commits
efb04d134e
...
dev_st_重构A
| Author | SHA1 | Date | |
|---|---|---|---|
| 61095a379f | |||
| 822a814f85 | |||
| 0bd0392191 | |||
| b9663037f5 | |||
| a0923c8572 | |||
| d482cfcb7d | |||
| 9e6d2906f8 | |||
| 6f7bb4f960 | |||
| fa9af5ff1b | |||
| 08628bcd1d | |||
| 19cb29616f | |||
| 6e50cdcd2a | |||
| f1b52151be | |||
| 993ec623af | |||
| 0416a64235 | |||
| 2b75ad90fb | |||
| 0ac9030f80 | |||
| ea9c40f64f | |||
| 48c90fa0be | |||
| fe59a0cb45 | |||
| 81bc50ce17 | |||
| 6ae504823b | |||
| d2f582b7f8 | |||
| cc82396195 | |||
| 2ff8a7a4af | |||
| 3c0b7e754c | |||
| 3705db4aab | |||
| 36774a8a2c | |||
| 36135313d8 | |||
| 23c0d14128 | |||
| d0c5cada35 | |||
| b556e6841d | |||
| 26096abbcc | |||
| 766c62f3c0 | |||
| 07a77149fc | |||
| 32ebc6fb65 | |||
| 25fbe9b64e | |||
| 4392296616 | |||
| ef52cd4872 | |||
| 70a8466d9f | |||
| 66d85f78a0 | |||
| 93a20cd92a | |||
| 9a54a2ae6c | |||
| 1b9ce1622d | |||
| b4db79eba8 | |||
| 22f77d56ea | |||
| d8d5bdc3ae | |||
| 7d583ceb1d | |||
| 51b744ecd7 | |||
| 3fd7d2af2e | |||
| db869552e4 | |||
| b34de116a3 | |||
| e67bc37571 | |||
| 2b749cd2b0 | |||
| ce889e1ed0 | |||
| e8b4b2c58a | |||
| 3a5a6395af | |||
| a22599feda | |||
| 6a177ceebc | |||
| f9d7579536 | |||
| 0fa31418f6 | |||
| 77fd46aa34 | |||
| 6ad9783bcb | |||
| edc25c159d | |||
| 06a572c08a | |||
| 36c0b0b210 | |||
| d1d47336c2 | |||
| 063ceae10f | |||
| 552387293c | |||
| 93489b09d9 | |||
| 663cb8493b | |||
| ac0d9584d8 | |||
| 7fa124d45f | |||
| 3dfb8f31e2 | |||
| 619c02f236 | |||
| 28852a8d4b | |||
| b021fd308f | |||
| 169a1929d7 | |||
| b5da9f35a5 | |||
| 8f4deaac4e | |||
| d479d1903b | |||
| 32c4138ae0 | |||
| da62d4f411 | |||
| 85dcd72a5d | |||
| 21fcbe3665 | |||
| 1b6724f043 | |||
| ef332ecaa1 | |||
| 3d6d673c0b | |||
| 674f09d5b6 | |||
| 11d8f78b1b | |||
| bbacef4ff7 | |||
| 8e692647d3 | |||
| 6f80f969a4 | |||
| bdf2a9af80 | |||
| e858d35722 | |||
| f2d5210313 | |||
| 1b0af3e2d6 | |||
| 0965cd3c7e | |||
| c3909d63da | |||
| 1096f24c57 | |||
| 7ed84fd445 | |||
| 4e2d7d2908 | |||
| 34089ddeea | |||
| 6ec98468de | |||
| 2d5919016f | |||
| c0fa51bb2e | |||
| 6713f36387 | |||
| f24750458a | |||
| 510a2f4d66 | |||
| ae37730da6 | |||
| 203f104ece | |||
| 8e934dd83a | |||
| 1676916a5c | |||
| 1af5a0e849 | |||
| 5b6e0a8fbf | |||
| 9968883bab | |||
| af5f637d31 | |||
| 0a725e845e | |||
| 6a539dc3c5 | |||
| 73d6ec933a | |||
| 000d603241 | |||
| fbf9fe9f2a | |||
| 8e4d7e1ee8 | |||
| 262eb57b36 | |||
| 2e1c261775 | |||
| 6ad2079351 | |||
| a477592f5d | |||
| 6f336e8368 | |||
| 17e038beb1 | |||
| 4e6fd90668 | |||
| 5cfc76e6c5 | |||
| 9e33c93763 | |||
| 1c9ae7bc06 | |||
| 472e9ad341 | |||
| 19c69f4f6f | |||
| 8788cbb105 | |||
| ea77e9a5f8 | |||
| eaaf0e1ed6 | |||
| 8a344b293d | |||
| 9cdd024ce2 | |||
| d612346db5 | |||
| 2cacaab974 | |||
| 200b1ab9f8 | |||
| 8d0939cd78 | |||
| df51620ca9 | |||
| 70520fb7d9 | |||
| 7587fe6714 | |||
| 1c8834caf6 | |||
| 68306aa07f | |||
| 9544ad75ff | |||
| d90e080981 | |||
| fc65052583 | |||
| e0d5ae0257 | |||
| 639ce7eafd | |||
| e0379d3717 | |||
| c108077178 | |||
| 182e5b9da1 | |||
| ea4ecc05b4 | |||
| ae05127292 | |||
| 38a3d2879e | |||
| 10ba4cd80f | |||
| 857822c49c | |||
| 85012eab78 | |||
| 8aa43d723a | |||
| 1ecb7d60e5 | |||
| 904a6c932a | |||
| 886de394d0 | |||
| 8bad475288 | |||
| 4a26419e67 | |||
| 1e04e7c39a | |||
| dde8716262 | |||
| c6b4444589 | |||
| 5bd20a911f | |||
| b43567748c | |||
| 59297eac77 | |||
| 9f7d805a52 | |||
| 6800864866 | |||
| e8a980ff5b | |||
| dfbd5efe69 | |||
| 4a474e9b44 | |||
| 30f2e4f24f | |||
| c898d16688 | |||
| f10ddd9a31 | |||
| 1651258eec | |||
| fd0ddfd45a | |||
| 444877fb73 | |||
| 1436464eca | |||
| 05b9a0b823 | |||
| 053001170a | |||
| 9cafb0f70e | |||
| a399af53b5 | |||
| a7574cd286 | |||
| 06dee39566 | |||
| d5b4ef2b59 | |||
| 2620bd6845 | |||
| 1f0bdb1bd4 | |||
| b21a2a8193 | |||
| 1f07120289 | |||
| 64e0218add | |||
| 7972188ac3 | |||
| 6963c8016f | |||
| 633e6a9123 | |||
| 437f796a08 | |||
| 2b08dd44ee | |||
| 1eeeef266b | |||
| 3813974eae | |||
| 6a5bda44e6 | |||
| 704553cd4e | |||
| 35597f89ca | |||
| 577b749198 | |||
| cccced6afa | |||
| 14637a21ad | |||
| 111fe42782 | |||
| 58da905ade | |||
| f338a54e41 | |||
| 526ac1a7df | |||
| 7f90240731 | |||
| e4442afe72 | |||
| be1d1ad70d | |||
| 4fd0a52a36 | |||
| 04c7d19c37 | |||
| 94269209e0 | |||
| 2b4123741a | |||
| 45695364e9 | |||
| d348b35984 | |||
| e39104c431 | |||
| 7b86b739eb | |||
| ade23e7a20 | |||
| 1b2b0c1143 | |||
| 0400d2020b | |||
| 2cc93e0b48 | |||
| fd8c08316b | |||
| 0a1c30f669 | |||
| ca3ea0630e | |||
| a26b6b58a9 | |||
| 6fd4a86a7e | |||
| fa999f502f | |||
| ad18a47d21 | |||
| d2258883df | |||
| f7d11c5f8b | |||
| 40d9b5aad4 | |||
| 6ac6514f89 | |||
| eb7ad1a9f1 | |||
| 17ce91d40a | |||
| 515487e748 | |||
| 7a25a6a5fa | |||
| 64887054e0 | |||
| 8f63741d8c | |||
| f593ef0b4a | |||
| 231f7f8c13 | |||
| c9863cd353 | |||
| ce4c4f0531 | |||
| af2bcc42fd | |||
| 1596aac717 | |||
| d13bb734b1 | |||
| 2665b5ad1f | |||
| cffd77eeb5 | |||
| b8f8d2e6b0 | |||
| 279255a14c | |||
| b216ddaa61 | |||
| f770f8055e | |||
| 819d74cc8d | |||
| 9651ae7ad7 | |||
| eca168957d | |||
| f026b9f9fd | |||
| b368ba0159 | |||
| 43e8b85656 | |||
| 716f91bdd0 | |||
| b00283cd96 | |||
| 25edf2d817 | |||
| 1d6371c37e | |||
| 82123fc232 | |||
| 6f02bc7cf5 | |||
| a5ff2ce51b | |||
| 4deccb76dc | |||
| 93556ddb9c | |||
| 681fced59d | |||
| 91e2b047eb | |||
| c1c4c85bd2 | |||
| d06e0499bc | |||
| 49f730b609 | |||
| 04a392e7c7 | |||
| 22e393e588 | |||
| 6556689c8f | |||
| a50d18b486 | |||
| 599a5de3bc | |||
| c1eb6a3458 | |||
| b87998549c | |||
| 27aa723e7d | |||
| 6be90ebb10 | |||
| 2f55e7bfa1 | |||
| c56655c728 | |||
| 5e4c16c577 | |||
| 8245e7b3d1 | |||
| cafde48f4a | |||
| c897111855 | |||
| a2bb61408b | |||
| 9268a21eb8 | |||
| d4c553f072 | |||
| 73802b6e80 | |||
| 1a0a444a99 | |||
| c37038f163 | |||
| 3144315de5 | |||
| 8f16250cbe | |||
| 95d5e2b972 | |||
| 2760a070a3 | |||
| 2435d760e8 | |||
| 80e4db86e4 | |||
| 4ab8a61a3c | |||
| 73c83153f9 | |||
| 1b67998f6a | |||
| b8cc38aa61 | |||
| c4398a689b | |||
| b660eb19f4 | |||
| 1eb73f5257 | |||
| 71423df1c0 | |||
| 709e0f4453 | |||
| 18df76a2b4 | |||
| 15e37841bb | |||
| 8e93f8f86f | |||
| fd35c5c993 | |||
| fd7b3a7f75 | |||
| af8fff5b13 | |||
| fc87c545a0 | |||
| 0f4ca89060 | |||
| c371c7224e | |||
| 31bb72c8f4 | |||
| faa05e2a10 | |||
| 6bdd111a3a | |||
| 8296ac12b6 | |||
| b2994adc1c | |||
| 75d2e4072a | |||
| b23927968f | |||
| c27fd099f6 | |||
| bc1264e28f | |||
| 799b0f3989 | |||
| b3ce856ad4 | |||
| f51fe1fac9 | |||
| 8dbaa9dcf6 | |||
| 0196128008 | |||
| 4108aed4e0 | |||
| cc55bb107a | |||
| 7518a29d2f | |||
| 37e131eb09 | |||
| 3dcc4932c3 | |||
| 254e65906a | |||
| ced0b88ca4 | |||
| b2021dcb3c | |||
| 0ef7b7d1d8 | |||
| 7254e2dbd9 | |||
| 26ef29ac4e | |||
| 005e3c7581 | |||
| ee433db4ad | |||
| ea4b8168b7 | |||
| f366a4aa6c | |||
| d849b201ca | |||
| dc813fcabc | |||
| 1d215ffdb3 | |||
| d9bfc30c88 | |||
| 9305acb69b | |||
| f9a8955384 | |||
| 1f9dbba39d | |||
| dace0a9309 | |||
| 4f2e80e482 | |||
| b27b9f9ee1 | |||
| 66a1ddef66 | |||
| eacac8425c | |||
| d164514fcf | |||
| ae79d1b1ba | |||
| 50163d02a7 | |||
| 5ec950cc61 | |||
| a61b5fa2fd | |||
| f406416698 | |||
| debbe2777b | |||
| bc261661ae | |||
| 0aead49816 | |||
| 66b7a9218e | |||
| 2f4205ad1a | |||
| fea22aecab | |||
| 62f3ddae4a | |||
| c317afc0fe | |||
| 1dbe04cdf9 | |||
| afc44cb471 | |||
| 39d8b3d547 | |||
| f387b95d0d | |||
| 1d064c1f31 | |||
| 3440cc4773 | |||
| 20b13bcffa | |||
| 105e2ddf9b | |||
| a1a38d821c | |||
| 83987db5ac | |||
| d10114572e | |||
| e34288ae56 | |||
| 17b8bf2bfd | |||
| 57bd4ba109 | |||
| e4ba237a00 | |||
| 9059a24637 | |||
| dc0c55c495 | |||
| a007a77db9 | |||
| 3eb3a86376 | |||
| 1dc9560a1f | |||
| 8069b08fab | |||
| 2c8142c0d2 | |||
| 9f4110b24a | |||
| 1cdc17b710 | |||
| 97316c7989 | |||
| fac5e7657c | |||
| 50dcb78417 | |||
| 998fa7aa67 | |||
| 5e1a1f540e | |||
| 2415e97c97 | |||
| aa71cc3c4f | |||
| e5ddcc4308 | |||
| 883b222254 | |||
| dc9ee10023 | |||
| 2c4a4329ff | |||
| 553238de0c | |||
| 80b6102673 | |||
| 705b0f374e | |||
| 5bdc7ddec0 | |||
| 5d2a3de2f4 | |||
| 675a9f6d64 | |||
| 41b14ceea4 | |||
| a729396401 | |||
| 3b0beb52da | |||
| faeb930fe3 | |||
| 9a39c29e88 | |||
| b23c9a678b | |||
| 96cd32ed99 | |||
| 50dd53b0c0 | |||
| 48a12f0919 | |||
| 91d754b389 | |||
| 450798c8bd | |||
| c3acc11f6a | |||
| d592c9f12e | |||
| 26e39ce416 | |||
| 32521208a0 | |||
| 6e969648c6 | |||
| 074596ebcb | |||
| f0542c11c8 | |||
| 0fa3d10284 | |||
| 0d13192723 | |||
| a72aae84ef | |||
| 6ba1339c0b | |||
| a75afbe4c1 | |||
| 1f45564539 | |||
| 41aec6b89e | |||
| a1db745b6c | |||
| 15fc9621cd | |||
| d7874829d9 | |||
| abf32e8457 | |||
| efdcf60ed1 | |||
| 7a1b17d060 | |||
| f43f94b94d | |||
| 3e2dc4bcb6 | |||
| 6fb9e56720 | |||
| e2cff76d13 | |||
| 6e57f1c853 | |||
| 4fe18c77dc | |||
| cb5819e330 | |||
| 6f20e6aeb1 | |||
| 5af2612ff7 | |||
| cac2f13b88 | |||
| edf88721da | |||
| 915b329805 | |||
| 1673a2f4be | |||
| e4cebeac85 | |||
| c7021e382e | |||
| ffea9d2022 | |||
| 90c1e7ff6c | |||
| 59d04bb33c | |||
| eb0d3aaa71 | |||
| 10dfe9b1d6 | |||
| 6993bfd682 | |||
| 247a87891e | |||
| 9af91cc4bc | |||
| 4f23118ec0 | |||
| 482756f6f0 | |||
| 85a3694e35 | |||
| f58bf61500 | |||
| 783d088f22 | |||
| 74476cd592 | |||
| 9b43274e93 | |||
| 8ce1d95c8c | |||
| e8c88a6148 | |||
| e218c1bf3d | |||
| c5326a3079 | |||
| 9101ffaab0 | |||
| e594711fa3 | |||
| 7fd084e529 | |||
| 11b25241bf | |||
| 8fcfce7376 | |||
| 23317c9fd4 | |||
| 045d5eaff8 | |||
| 72b6dbb157 | |||
| e78b56e2cb | |||
| 6c05026402 | |||
| 13facba33a | |||
| 935284388c | |||
| 4c7fd9049f | |||
| 0031b7a5f6 | |||
| 02dd204744 | |||
| f28f7de49d | |||
| c2859f888a | |||
| a2b51189aa | |||
| 2f2f20cfc2 | |||
| 377e88b6db | |||
| 1deca2ae5b |
@@ -0,0 +1,17 @@
|
||||
Dec 18 13:42:00 macbookpro com.apple.dt.xcodebuild[42901] <Error>: Unable to deliver request ({
|
||||
"developer_dir" = "/Applications/Xcode.app/Contents/Developer";
|
||||
request = "set_developer_dir";
|
||||
}) because we are not connected to CoreSimulatorService.
|
||||
Dec 18 13:42:00 macbookpro com.apple.dt.xcodebuild[42901] <Warning>: Unable to discover any Simulator runtimes. Developer Directory is /Applications/Xcode.app/Contents/Developer.
|
||||
Dec 18 13:42:00 macbookpro com.apple.dt.xcodebuild[42901] <Error>: Could not kickstart simdiskimaged; SimDiskImageManager services will not be available: Error Domain=NSPOSIXErrorDomain Code=53 "Software caused connection abort" UserInfo={NSLocalizedDescription=Error returned in reply from CoreSimulatorService: Connection invalid}
|
||||
Dec 18 13:42:00 macbookpro com.apple.dt.xcodebuild[42901] <Error>: simdiskimaged returned error (invalid), marking disconnected.
|
||||
Dec 18 13:42:00 macbookpro com.apple.dt.xcodebuild[42901] <Error>: simdiskimaged returned error (invalid), marking disconnected.
|
||||
Dec 18 13:42:00 macbookpro com.apple.dt.xcodebuild[42901] <Error>: Could not get list of trusted mount directories: Error Domain=com.apple.CoreSimulator.SimError Code=410 "The service used to manage runtime disk images (simdiskimaged) crashed or is not responding" UserInfo={NSLocalizedDescription=The service used to manage runtime disk images (simdiskimaged) crashed or is not responding}
|
||||
Dec 18 13:42:00 macbookpro com.apple.dt.xcodebuild[42901] <Error>: Unable to deliver request ({
|
||||
request = "notification_subscription";
|
||||
"set_path" = "/Users/mac/Library/Developer/CoreSimulator/Devices";
|
||||
}) because we are not connected to CoreSimulatorService.
|
||||
Dec 18 13:42:00 macbookpro com.apple.dt.xcodebuild[42901] <Error>: Unable to deliver request ({
|
||||
request = "notification_subscription";
|
||||
"set_path" = "/Users/mac/Library/Developer/CoreSimulator/Devices";
|
||||
}) because we are not connected to CoreSimulatorService.
|
||||
18
CustomKeyboard/CustomKeyboard.entitlements
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.developer.associated-domains</key>
|
||||
<array>
|
||||
<string>applinks:app.tknb.net</string>
|
||||
</array>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.com.loveKey.nyx</string>
|
||||
</array>
|
||||
<key>keychain-access-groups</key>
|
||||
<array>
|
||||
<string>$(AppIdentifierPrefix)com.loveKey.nyx.shared</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -2,6 +2,12 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>LSApplicationQueriesSchemes</key>
|
||||
<array>
|
||||
<string>kbkeyboardAppExtension</string>
|
||||
</array>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>需要使用麦克风进行语音输入</string>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionAttributes</key>
|
||||
|
||||
BIN
CustomKeyboard/KeyboardAssets.xcassets/App_icon.imageset/App_icon.png
vendored
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
CustomKeyboard/KeyboardAssets.xcassets/App_icon.imageset/App_icon@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 9.2 KiB |
BIN
CustomKeyboard/KeyboardAssets.xcassets/App_icon.imageset/App_icon@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 19 KiB |
23
CustomKeyboard/KeyboardAssets.xcassets/App_icon.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "App_icon.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "App_icon@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "App_icon@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
6
CustomKeyboard/KeyboardAssets.xcassets/Contents.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
22
CustomKeyboard/KeyboardAssets.xcassets/ai_key_icon.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "切图 271@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "切图 271@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
CustomKeyboard/KeyboardAssets.xcassets/ai_key_icon.imageset/切图 271@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
CustomKeyboard/KeyboardAssets.xcassets/ai_key_icon.imageset/切图 271@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
22
CustomKeyboard/KeyboardAssets.xcassets/back_keybord_icon.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "back_keybord_icon@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "back_keybord_icon@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
CustomKeyboard/KeyboardAssets.xcassets/back_keybord_icon.imageset/back_keybord_icon@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
CustomKeyboard/KeyboardAssets.xcassets/back_keybord_icon.imageset/back_keybord_icon@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
22
CustomKeyboard/KeyboardAssets.xcassets/buy_sel_icon.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "buy_sel_icon@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "buy_sel_icon@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
CustomKeyboard/KeyboardAssets.xcassets/buy_sel_icon.imageset/buy_sel_icon@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
CustomKeyboard/KeyboardAssets.xcassets/buy_sel_icon.imageset/buy_sel_icon@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
22
CustomKeyboard/KeyboardAssets.xcassets/close_icon.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "close_icon@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "close_icon@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
CustomKeyboard/KeyboardAssets.xcassets/close_icon.imageset/close_icon@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
CustomKeyboard/KeyboardAssets.xcassets/close_icon.imageset/close_icon@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
22
CustomKeyboard/KeyboardAssets.xcassets/home_ai_icon.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "home_ai_icon@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "home_ai_icon@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
CustomKeyboard/KeyboardAssets.xcassets/home_ai_icon.imageset/home_ai_icon@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 9.4 KiB |
BIN
CustomKeyboard/KeyboardAssets.xcassets/home_ai_icon.imageset/home_ai_icon@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 18 KiB |
22
CustomKeyboard/KeyboardAssets.xcassets/home_chat_icon.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "home_chat_icon@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "home_chat_icon@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
CustomKeyboard/KeyboardAssets.xcassets/home_chat_icon.imageset/home_chat_icon@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 8.2 KiB |
BIN
CustomKeyboard/KeyboardAssets.xcassets/home_chat_icon.imageset/home_chat_icon@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 16 KiB |
22
CustomKeyboard/KeyboardAssets.xcassets/home_emotion_icon.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "home_emotion_icon@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "home_emotion_icon@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
CustomKeyboard/KeyboardAssets.xcassets/home_emotion_icon.imageset/home_emotion_icon@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 9.3 KiB |
BIN
CustomKeyboard/KeyboardAssets.xcassets/home_emotion_icon.imageset/home_emotion_icon@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 18 KiB |
22
CustomKeyboard/KeyboardAssets.xcassets/home_keyboard_icon.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "home_keyboard_icon@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "home_keyboard_icon@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
CustomKeyboard/KeyboardAssets.xcassets/home_keyboard_icon.imageset/home_keyboard_icon@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
CustomKeyboard/KeyboardAssets.xcassets/home_keyboard_icon.imageset/home_keyboard_icon@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 25 KiB |
86
CustomKeyboard/KeyboardAssets.xcassets/kb_del_icon.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,86 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "light"
|
||||
}
|
||||
],
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "kb_del_icon@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "light"
|
||||
}
|
||||
],
|
||||
"filename" : "kb_del_icon@2x 1.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "切图 256@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "kb_del_icon@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "light"
|
||||
}
|
||||
],
|
||||
"filename" : "kb_del_icon@3x 1.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "切图 256@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
CustomKeyboard/KeyboardAssets.xcassets/kb_del_icon.imageset/kb_del_icon@2x 1.png
vendored
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
CustomKeyboard/KeyboardAssets.xcassets/kb_del_icon.imageset/kb_del_icon@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
CustomKeyboard/KeyboardAssets.xcassets/kb_del_icon.imageset/kb_del_icon@3x 1.png
vendored
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
CustomKeyboard/KeyboardAssets.xcassets/kb_del_icon.imageset/kb_del_icon@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
CustomKeyboard/KeyboardAssets.xcassets/kb_del_icon.imageset/切图 256@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 1008 B |
BIN
CustomKeyboard/KeyboardAssets.xcassets/kb_del_icon.imageset/切图 256@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
22
CustomKeyboard/KeyboardAssets.xcassets/kb_zt_icon.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "kb_zt_icon@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "kb_zt_icon@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
CustomKeyboard/KeyboardAssets.xcassets/kb_zt_icon.imageset/kb_zt_icon@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 1014 B |
BIN
CustomKeyboard/KeyboardAssets.xcassets/kb_zt_icon.imageset/kb_zt_icon@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
22
CustomKeyboard/KeyboardAssets.xcassets/key_revoke.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "key_revoke@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "key_revoke@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
CustomKeyboard/KeyboardAssets.xcassets/key_revoke.imageset/key_revoke@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
CustomKeyboard/KeyboardAssets.xcassets/key_revoke.imageset/key_revoke@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
22
CustomKeyboard/KeyboardAssets.xcassets/keybord_bg_icon.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "keybord_bg_icon@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "keybord_bg_icon@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
CustomKeyboard/KeyboardAssets.xcassets/keybord_bg_icon.imageset/keybord_bg_icon@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 486 KiB |
BIN
CustomKeyboard/KeyboardAssets.xcassets/keybord_bg_icon.imageset/keybord_bg_icon@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
22
CustomKeyboard/KeyboardAssets.xcassets/upgrad_vip_icon.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "upgrad_vip_icon@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "upgrad_vip_icon@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
CustomKeyboard/KeyboardAssets.xcassets/upgrad_vip_icon.imageset/upgrad_vip_icon@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
CustomKeyboard/KeyboardAssets.xcassets/upgrad_vip_icon.imageset/upgrad_vip_icon@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 39 KiB |
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
|
||||
|
||||
49
CustomKeyboard/Model/KBChatMessage.h
Normal file
@@ -0,0 +1,49 @@
|
||||
//
|
||||
// KBChatMessage.h
|
||||
// CustomKeyboard
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface KBChatMessage : NSObject
|
||||
|
||||
@property (nonatomic, copy) NSString *text;
|
||||
@property (nonatomic, assign) BOOL outgoing;
|
||||
@property (nonatomic, copy, nullable) NSString *audioFilePath;
|
||||
@property (nonatomic, copy, nullable) NSString *avatarURL;
|
||||
@property (nonatomic, copy, nullable) NSString *displayName;
|
||||
@property (nonatomic, strong, nullable) UIImage *avatarImage;
|
||||
|
||||
/// 是否处于加载状态
|
||||
@property (nonatomic, assign) BOOL isLoading;
|
||||
/// 是否完成(用于打字机效果)
|
||||
@property (nonatomic, assign) BOOL isComplete;
|
||||
/// 是否需要打字机效果
|
||||
@property (nonatomic, assign) BOOL needsTypewriterEffect;
|
||||
/// 音频 ID(用于异步加载音频)
|
||||
@property (nonatomic, copy, nullable) NSString *audioId;
|
||||
/// 音频数据(缓存)
|
||||
@property (nonatomic, strong, nullable) NSData *audioData;
|
||||
/// 音频时长(秒)
|
||||
@property (nonatomic, assign) NSTimeInterval audioDuration;
|
||||
|
||||
+ (instancetype)messageWithText:(NSString *)text
|
||||
outgoing:(BOOL)outgoing
|
||||
audioFilePath:(nullable NSString *)audioFilePath;
|
||||
|
||||
/// 创建用户消息
|
||||
+ (instancetype)userMessageWithText:(NSString *)text;
|
||||
|
||||
/// 创建 AI 消息(带 audioId)
|
||||
+ (instancetype)assistantMessageWithText:(NSString *)text
|
||||
audioId:(nullable NSString *)audioId;
|
||||
|
||||
/// 创建加载中的 AI 消息
|
||||
+ (instancetype)loadingAssistantMessage;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
55
CustomKeyboard/Model/KBChatMessage.m
Normal file
@@ -0,0 +1,55 @@
|
||||
//
|
||||
// KBChatMessage.m
|
||||
// CustomKeyboard
|
||||
//
|
||||
|
||||
#import "KBChatMessage.h"
|
||||
|
||||
@implementation KBChatMessage
|
||||
|
||||
+ (instancetype)messageWithText:(NSString *)text
|
||||
outgoing:(BOOL)outgoing
|
||||
audioFilePath:(NSString *)audioFilePath {
|
||||
KBChatMessage *msg = [[KBChatMessage alloc] init];
|
||||
msg.text = text ?: @"";
|
||||
msg.outgoing = outgoing;
|
||||
msg.audioFilePath = audioFilePath;
|
||||
msg.isComplete = YES;
|
||||
msg.isLoading = NO;
|
||||
msg.needsTypewriterEffect = NO;
|
||||
return msg;
|
||||
}
|
||||
|
||||
+ (instancetype)userMessageWithText:(NSString *)text {
|
||||
KBChatMessage *msg = [[KBChatMessage alloc] init];
|
||||
msg.text = text ?: @"";
|
||||
msg.outgoing = YES;
|
||||
msg.isComplete = YES;
|
||||
msg.isLoading = NO;
|
||||
msg.needsTypewriterEffect = NO;
|
||||
return msg;
|
||||
}
|
||||
|
||||
+ (instancetype)assistantMessageWithText:(NSString *)text
|
||||
audioId:(NSString *)audioId {
|
||||
KBChatMessage *msg = [[KBChatMessage alloc] init];
|
||||
msg.text = text ?: @"";
|
||||
msg.outgoing = NO;
|
||||
msg.audioId = audioId;
|
||||
msg.isComplete = NO;
|
||||
msg.isLoading = NO;
|
||||
msg.needsTypewriterEffect = YES;
|
||||
return msg;
|
||||
}
|
||||
|
||||
+ (instancetype)loadingAssistantMessage {
|
||||
KBChatMessage *msg = [[KBChatMessage alloc] init];
|
||||
msg.text = @"";
|
||||
msg.outgoing = NO;
|
||||
msg.isComplete = NO;
|
||||
msg.isLoading = YES;
|
||||
msg.needsTypewriterEffect = NO;
|
||||
return msg;
|
||||
}
|
||||
|
||||
@end
|
||||
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
|
||||
96
CustomKeyboard/Model/KBKeyboardLayoutConfig.h
Normal file
@@ -0,0 +1,96 @@
|
||||
//
|
||||
// KBKeyboardLayoutConfig.h
|
||||
// CustomKeyboard
|
||||
//
|
||||
// 键盘布局配置模型(由 JSON 驱动)
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface KBKeyboardLayoutMetrics : NSObject
|
||||
@property (nonatomic, strong, nullable) NSNumber *rowSpacing;
|
||||
@property (nonatomic, strong, nullable) NSNumber *topInset;
|
||||
@property (nonatomic, strong, nullable) NSNumber *bottomInset;
|
||||
@property (nonatomic, strong, nullable) NSNumber *keyHeight;
|
||||
@property (nonatomic, strong, nullable) NSNumber *edgeInset;
|
||||
@property (nonatomic, strong, nullable) NSNumber *gap;
|
||||
@property (nonatomic, strong, nullable) NSNumber *letterWidth;
|
||||
@property (nonatomic, strong, nullable) NSNumber *controlWidth;
|
||||
@property (nonatomic, strong, nullable) NSNumber *sendWidth;
|
||||
@property (nonatomic, strong, nullable) NSNumber *symbolsWideWidth;
|
||||
@property (nonatomic, strong, nullable) NSNumber *symbolsSideWidth;
|
||||
@end
|
||||
|
||||
@interface KBKeyboardLayoutFonts : NSObject
|
||||
@property (nonatomic, strong, nullable) NSNumber *letter;
|
||||
@property (nonatomic, strong, nullable) NSNumber *digit;
|
||||
@property (nonatomic, strong, nullable) NSNumber *symbol;
|
||||
@property (nonatomic, strong, nullable) NSNumber *mode;
|
||||
@property (nonatomic, strong, nullable) NSNumber *space;
|
||||
@property (nonatomic, strong, nullable) NSNumber *send;
|
||||
@end
|
||||
|
||||
@interface KBKeyboardKeyDef : NSObject
|
||||
@property (nonatomic, copy, nullable) NSString *type;
|
||||
@property (nonatomic, copy, nullable) NSString *title;
|
||||
@property (nonatomic, copy, nullable) NSString *selectedTitle;
|
||||
@property (nonatomic, copy, nullable) NSString *symbolName;
|
||||
@property (nonatomic, copy, nullable) NSString *selectedSymbolName;
|
||||
@property (nonatomic, copy, nullable) NSString *font;
|
||||
@property (nonatomic, copy, nullable) NSString *width;
|
||||
@property (nonatomic, strong, nullable) NSNumber *widthValue;
|
||||
@property (nonatomic, copy, nullable) NSString *backgroundColor;
|
||||
@end
|
||||
|
||||
@interface KBKeyboardRowItem : NSObject
|
||||
@property (nonatomic, copy, nullable) NSString *itemId;
|
||||
@property (nonatomic, copy, nullable) NSString *width;
|
||||
@property (nonatomic, strong, nullable) NSNumber *widthValue;
|
||||
+ (NSArray<KBKeyboardRowItem *> *)itemsFromRawArray:(NSArray *)raw;
|
||||
@end
|
||||
|
||||
@interface KBKeyboardRowSegments : NSObject
|
||||
@property (nonatomic, strong, nullable) NSArray *left;
|
||||
@property (nonatomic, strong, nullable) NSArray *center;
|
||||
@property (nonatomic, strong, nullable) NSArray *right;
|
||||
- (NSArray<KBKeyboardRowItem *> *)leftItems;
|
||||
- (NSArray<KBKeyboardRowItem *> *)centerItems;
|
||||
- (NSArray<KBKeyboardRowItem *> *)rightItems;
|
||||
@end
|
||||
|
||||
@interface KBKeyboardRowConfig : NSObject
|
||||
@property (nonatomic, strong, nullable) NSNumber *height;
|
||||
@property (nonatomic, strong, nullable) NSNumber *insetLeft;
|
||||
@property (nonatomic, strong, nullable) NSNumber *insetRight;
|
||||
@property (nonatomic, strong, nullable) NSNumber *gap;
|
||||
@property (nonatomic, copy, nullable) NSString *align;
|
||||
@property (nonatomic, strong, nullable) NSArray *items;
|
||||
@property (nonatomic, strong, nullable) KBKeyboardRowSegments *segments;
|
||||
- (NSArray<KBKeyboardRowItem *> *)resolvedItems;
|
||||
@end
|
||||
|
||||
@interface KBKeyboardLayout : NSObject
|
||||
@property (nonatomic, strong, nullable) NSArray<KBKeyboardRowConfig *> *rows;
|
||||
@end
|
||||
|
||||
@interface KBKeyboardLayoutConfig : NSObject
|
||||
@property (nonatomic, assign) CGFloat designWidth;
|
||||
@property (nonatomic, strong, nullable) KBKeyboardLayoutMetrics *metrics;
|
||||
@property (nonatomic, strong, nullable) KBKeyboardLayoutFonts *fonts;
|
||||
@property (nonatomic, copy, nullable) NSString *defaultKeyBackground;
|
||||
@property (nonatomic, strong, nullable) NSDictionary<NSString *, KBKeyboardKeyDef *> *keyDefs;
|
||||
@property (nonatomic, strong, nullable) NSDictionary<NSString *, KBKeyboardLayout *> *layouts;
|
||||
|
||||
+ (nullable instancetype)sharedConfig;
|
||||
+ (nullable instancetype)configFromJSONData:(NSData *)data;
|
||||
- (CGFloat)scaledValue:(CGFloat)designValue;
|
||||
- (CGFloat)keyboardAreaDesignHeight;
|
||||
- (CGFloat)keyboardAreaScaledHeight;
|
||||
- (nullable KBKeyboardLayout *)layoutForName:(NSString *)name;
|
||||
- (nullable KBKeyboardKeyDef *)keyDefForIdentifier:(NSString *)identifier;
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
187
CustomKeyboard/Model/KBKeyboardLayoutConfig.m
Normal file
@@ -0,0 +1,187 @@
|
||||
//
|
||||
// KBKeyboardLayoutConfig.m
|
||||
// CustomKeyboard
|
||||
//
|
||||
|
||||
#import "KBKeyboardLayoutConfig.h"
|
||||
#import <MJExtension/MJExtension.h>
|
||||
#import "KBConfig.h"
|
||||
|
||||
static NSString * const kKBKeyboardLayoutConfigFileName = @"kb_keyboard_layout_config";
|
||||
|
||||
@implementation KBKeyboardLayoutMetrics
|
||||
@end
|
||||
|
||||
@implementation KBKeyboardLayoutFonts
|
||||
@end
|
||||
|
||||
@implementation KBKeyboardKeyDef
|
||||
@end
|
||||
|
||||
@implementation KBKeyboardRowItem
|
||||
|
||||
+ (NSDictionary *)mj_replacedKeyFromPropertyName {
|
||||
return @{ @"itemId": @"id" };
|
||||
}
|
||||
|
||||
+ (NSArray<KBKeyboardRowItem *> *)itemsFromRawArray:(NSArray *)raw {
|
||||
if (![raw isKindOfClass:[NSArray class]] || raw.count == 0) {
|
||||
return @[];
|
||||
}
|
||||
NSMutableArray<KBKeyboardRowItem *> *items = [NSMutableArray arrayWithCapacity:raw.count];
|
||||
for (id obj in raw) {
|
||||
if ([obj isKindOfClass:[NSString class]]) {
|
||||
KBKeyboardRowItem *item = [KBKeyboardRowItem new];
|
||||
item.itemId = (NSString *)obj;
|
||||
[items addObject:item];
|
||||
continue;
|
||||
}
|
||||
if ([obj isKindOfClass:[NSDictionary class]]) {
|
||||
KBKeyboardRowItem *item = [KBKeyboardRowItem mj_objectWithKeyValues:obj];
|
||||
if (item.itemId.length == 0) {
|
||||
NSString *fallback = ((NSDictionary *)obj)[@"id"];
|
||||
if ([fallback isKindOfClass:[NSString class]]) {
|
||||
item.itemId = fallback;
|
||||
}
|
||||
}
|
||||
if (item.itemId.length > 0) {
|
||||
[items addObject:item];
|
||||
}
|
||||
}
|
||||
}
|
||||
return items.copy;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation KBKeyboardRowSegments
|
||||
|
||||
- (NSArray<KBKeyboardRowItem *> *)leftItems {
|
||||
return [KBKeyboardRowItem itemsFromRawArray:self.left ?: @[]];
|
||||
}
|
||||
|
||||
- (NSArray<KBKeyboardRowItem *> *)centerItems {
|
||||
return [KBKeyboardRowItem itemsFromRawArray:self.center ?: @[]];
|
||||
}
|
||||
|
||||
- (NSArray<KBKeyboardRowItem *> *)rightItems {
|
||||
return [KBKeyboardRowItem itemsFromRawArray:self.right ?: @[]];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation KBKeyboardRowConfig
|
||||
|
||||
- (NSArray<KBKeyboardRowItem *> *)resolvedItems {
|
||||
return [KBKeyboardRowItem itemsFromRawArray:self.items ?: @[]];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation KBKeyboardLayout
|
||||
|
||||
+ (NSDictionary *)mj_objectClassInArray {
|
||||
return @{ @"rows": [KBKeyboardRowConfig class] };
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation KBKeyboardLayoutConfig
|
||||
|
||||
+ (instancetype)sharedConfig {
|
||||
static KBKeyboardLayoutConfig *config = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
NSString *path = [[NSBundle mainBundle] pathForResource:kKBKeyboardLayoutConfigFileName ofType:@"json"];
|
||||
NSData *data = path.length ? [NSData dataWithContentsOfFile:path] : nil;
|
||||
config = data ? [KBKeyboardLayoutConfig configFromJSONData:data] : nil;
|
||||
});
|
||||
return config;
|
||||
}
|
||||
|
||||
+ (instancetype)configFromJSONData:(NSData *)data {
|
||||
if (data.length == 0) { return nil; }
|
||||
NSError *error = nil;
|
||||
id json = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
|
||||
if (error || ![json isKindOfClass:[NSDictionary class]]) {
|
||||
return nil;
|
||||
}
|
||||
NSDictionary *dict = (NSDictionary *)json;
|
||||
KBKeyboardLayoutConfig *config = [KBKeyboardLayoutConfig mj_objectWithKeyValues:dict];
|
||||
|
||||
NSDictionary *keyDefsRaw = dict[@"keyDefs"];
|
||||
if ([keyDefsRaw isKindOfClass:[NSDictionary class]]) {
|
||||
NSMutableDictionary<NSString *, KBKeyboardKeyDef *> *defs = [NSMutableDictionary dictionaryWithCapacity:keyDefsRaw.count];
|
||||
[keyDefsRaw enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
|
||||
if (![key isKindOfClass:[NSString class]] || ![obj isKindOfClass:[NSDictionary class]]) {
|
||||
return;
|
||||
}
|
||||
KBKeyboardKeyDef *def = [KBKeyboardKeyDef mj_objectWithKeyValues:obj];
|
||||
if (def) {
|
||||
defs[key] = def;
|
||||
}
|
||||
}];
|
||||
config.keyDefs = defs.copy;
|
||||
}
|
||||
|
||||
NSDictionary *layoutsRaw = dict[@"layouts"];
|
||||
if ([layoutsRaw isKindOfClass:[NSDictionary class]]) {
|
||||
NSMutableDictionary<NSString *, KBKeyboardLayout *> *layouts = [NSMutableDictionary dictionaryWithCapacity:layoutsRaw.count];
|
||||
[layoutsRaw enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
|
||||
if (![key isKindOfClass:[NSString class]] || ![obj isKindOfClass:[NSDictionary class]]) {
|
||||
return;
|
||||
}
|
||||
KBKeyboardLayout *layout = [KBKeyboardLayout mj_objectWithKeyValues:obj];
|
||||
if (layout) {
|
||||
layouts[key] = layout;
|
||||
}
|
||||
}];
|
||||
config.layouts = layouts.copy;
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
- (CGFloat)scaledValue:(CGFloat)designValue {
|
||||
CGFloat baseWidth = (self.designWidth > 0.0) ? self.designWidth : KB_DESIGN_WIDTH;
|
||||
CGFloat scale = KBScreenWidth() / baseWidth;
|
||||
return designValue * scale;
|
||||
}
|
||||
|
||||
- (CGFloat)keyboardAreaDesignHeight {
|
||||
KBKeyboardLayout *layout = [self layoutForName:@"letters"] ?: self.layouts.allValues.firstObject;
|
||||
NSUInteger rowCount = layout.rows.count;
|
||||
if (rowCount == 0) { return 0.0; }
|
||||
|
||||
CGFloat rowSpacing = self.metrics.rowSpacing.doubleValue;
|
||||
CGFloat topInset = self.metrics.topInset.doubleValue;
|
||||
CGFloat bottomInset = self.metrics.bottomInset.doubleValue;
|
||||
|
||||
CGFloat total = topInset + bottomInset + rowSpacing * (rowCount - 1);
|
||||
for (KBKeyboardRowConfig *row in layout.rows) {
|
||||
CGFloat height = row.height.doubleValue;
|
||||
if (height <= 0.0) {
|
||||
height = self.metrics.keyHeight.doubleValue;
|
||||
}
|
||||
if (height <= 0.0) { height = 40.0; }
|
||||
total += height;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
- (CGFloat)keyboardAreaScaledHeight {
|
||||
CGFloat designHeight = [self keyboardAreaDesignHeight];
|
||||
return designHeight > 0.0 ? [self scaledValue:designHeight] : 0.0;
|
||||
}
|
||||
|
||||
- (KBKeyboardLayout *)layoutForName:(NSString *)name {
|
||||
if (name.length == 0) { return nil; }
|
||||
return self.layouts[name];
|
||||
}
|
||||
|
||||
- (KBKeyboardKeyDef *)keyDefForIdentifier:(NSString *)identifier {
|
||||
if (identifier.length == 0) { return nil; }
|
||||
return self.keyDefs[identifier];
|
||||
}
|
||||
|
||||
@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
|
||||
78
CustomKeyboard/Network/KBNetworkManager.h
Normal file
@@ -0,0 +1,78 @@
|
||||
//
|
||||
// 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;
|
||||
|
||||
/// POST multipart 上传文件(常用于语音/图片等文件)
|
||||
- (nullable NSURLSessionDataTask *)uploadFile:(NSString *)path
|
||||
fileURL:(NSURL *)fileURL
|
||||
name:(NSString *)name
|
||||
mimeType:(NSString *)mimeType
|
||||
parameters:(nullable NSDictionary *)parameters
|
||||
headers:(nullable NSDictionary<NSString *, NSString *> *)headers
|
||||
completion:(KBNetworkCompletion)completion;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
384
CustomKeyboard/Network/KBNetworkManager.m
Normal file
@@ -0,0 +1,384 @@
|
||||
//
|
||||
// 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 *)uploadFile:(NSString *)path
|
||||
fileURL:(NSURL *)fileURL
|
||||
name:(NSString *)name
|
||||
mimeType:(NSString *)mimeType
|
||||
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; }
|
||||
if (!fileURL) {
|
||||
if (completion) completion(nil, nil, [NSError errorWithDomain:KBNetworkErrorDomain code:KBNetworkErrorInvalidResponse userInfo:@{NSLocalizedDescriptionKey: KBLocalized(@"Invalid file")}]);
|
||||
return nil;
|
||||
}
|
||||
AFHTTPRequestSerializer *serializer = [AFHTTPRequestSerializer serializer];
|
||||
serializer.timeoutInterval = self.timeout;
|
||||
NSError *error = nil;
|
||||
NSMutableURLRequest *req = [serializer multipartFormRequestWithMethod:@"POST"
|
||||
URLString:urlString
|
||||
parameters:parameters
|
||||
constructingBodyWithBlock:^(id<AFMultipartFormData> formData) {
|
||||
NSString *safeName = (name.length > 0) ? name : @"file";
|
||||
NSString *fileName = fileURL.lastPathComponent ?: @"upload.bin";
|
||||
NSString *type = (mimeType.length > 0) ? mimeType : @"application/octet-stream";
|
||||
[formData appendPartWithFileURL:fileURL name:safeName fileName:fileName mimeType:type error:nil];
|
||||
} error:&error];
|
||||
if (error || !req) {
|
||||
if (completion) completion(nil, nil, error ?: [NSError errorWithDomain:KBNetworkErrorDomain code:KBNetworkErrorInvalidURL userInfo:@{NSLocalizedDescriptionKey: KBLocalized(@"Invalid URL")}]);
|
||||
return nil;
|
||||
}
|
||||
[self applyHeaders:headers toMutableRequest:req contentType:nil];
|
||||
self.manager.responseSerializer = [AFHTTPResponseSerializer serializer];
|
||||
NSURLSessionUploadTask *task = [self.manager uploadTaskWithStreamedRequest:req progress: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;
|
||||
}
|
||||
NSString *ct = nil;
|
||||
if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
|
||||
ct = ((NSHTTPURLResponse *)response).allHeaderFields[@"Content-Type"];
|
||||
}
|
||||
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 *)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
|
||||