Compare commits
1008 Commits
1.1.1
...
feature/reflex
| Author | SHA1 | Date | |
|---|---|---|---|
| 0c0e936143 | |||
| 5ab4f8d2a6 | |||
| ade5f81bc6 | |||
| 1cd3a90809 | |||
| 29f3c3d3cd | |||
| a4d49c0ca9 | |||
| f4bcfe708c | |||
| 1523bf0e4d | |||
| ee18360f89 | |||
| 19df6d0f0a | |||
| e6e38dfba5 | |||
| d2a7ba388e | |||
| 33873531d2 | |||
| fb2a33876e | |||
| a42efe17a1 | |||
| 4bc2d1c7a9 | |||
| c7ebecfcb3 | |||
| 208f0a31e4 | |||
| 2aeb34a67e | |||
| 39f16bd039 | |||
| eea681d6c5 | |||
| a8768da4b9 | |||
| 4f6fd29d38 | |||
| ba9cb43b6a | |||
| 3af6da8554 | |||
| 6c21395ac8 | |||
| 456fda31cd | |||
| 9fc8735925 | |||
| 9683acbe4f | |||
| 86a1cc9324 | |||
| 88b14b3a92 | |||
| b9cd2682a7 | |||
| 64d1534fae | |||
| 8318902826 | |||
| a7f41fe5fc | |||
| 2983649cdb | |||
| d2e0db8773 | |||
| fd98070166 | |||
| 4a9fd00eda | |||
| 97205fc808 | |||
| 0cffe72a8f | |||
| d5ab2ee916 | |||
| 90c9a48694 | |||
| 0222c682a0 | |||
| 1e6f6ee110 | |||
| 00d6b348f0 | |||
| 5404f37d0f | |||
| 1e539c7129 | |||
| ca1b202949 | |||
| 62220a9a65 | |||
| 02730c6c86 | |||
| 68789fbe1d | |||
| b4261f8647 | |||
| 94bf4eac2a | |||
| bed52392e5 | |||
| c4891840bd | |||
| 9720227ac7 | |||
| 74622aaf10 | |||
| 0cb5ad8453 | |||
| def68eae48 | |||
| ffa658c49b | |||
| 6066de480f | |||
| 0fd7dfa002 | |||
| 60403e614d | |||
| fa7db997bd | |||
| d15e72c681 | |||
| 4f1ff7784d | |||
| 8129a034e3 | |||
| 838db6954b | |||
| 5b3d3af99c | |||
| 2f9f266493 | |||
| 9d94979d08 | |||
| 19b83f4404 | |||
| 2b13378d98 | |||
| 4ae9d41104 | |||
| 1490170eb4 | |||
| cd695ed106 | |||
| 69c1719159 | |||
| 4019518bf5 | |||
| 3fd8e7c77d | |||
| 99c3bcb8c5 | |||
| b587e96e70 | |||
| ef8f0a303e | |||
| cfb1e4caab | |||
| 2411c331cd | |||
| fd4b38f46d | |||
| 269e31894c | |||
| 2f2da50aed | |||
| d87779212c | |||
| 5db6a12c6e | |||
| 6d0f776102 | |||
| 6c83ddc2c7 | |||
| b510d24e13 | |||
| 6cdf2e61dc | |||
| 9b0ed83ff5 | |||
| dbe1b93f48 | |||
| 06444f1576 | |||
| 9bbf1d0d48 | |||
| 0562f15cd0 | |||
| eb63c91481 | |||
| f916174070 | |||
| 60e23e126e | |||
| afeff1b562 | |||
| 5acb33005b | |||
| 3446eff353 | |||
| ad1f1f579e | |||
| e03b5f7e5d | |||
| d010c82dd0 | |||
| 652d03c39a | |||
| 1d39669a52 | |||
| d558ca6852 | |||
| ad32ca0f05 | |||
| 67097982ea | |||
| 1342d3029c | |||
| 62dcef4644 | |||
| 7ee296143e | |||
| c6bac54597 | |||
| 5f7bce64ed | |||
| d2dde55bb1 | |||
| f1a0a5c5e5 | |||
| 0db073459e | |||
| 7c7ed9286f | |||
| aac88dd6c8 | |||
| d7376b75cd | |||
| 5b39b3ed03 | |||
| bbaa85bdbf | |||
| 34e27bc5d9 | |||
| 714307273e | |||
| 5242d3c5a1 | |||
| cf2e94a1d2 | |||
| 800acb4cad | |||
| 368ce64121 | |||
| 05f03090a9 | |||
| a8803781e8 | |||
| 170f74b297 | |||
| 0d0f2a3073 | |||
| d6bddf5199 | |||
| 258ec8697b | |||
| 8f9a6e88ec | |||
| c37270e6ac | |||
| ce10d45c29 | |||
| b2716c4b2e | |||
| ab135ba94e | |||
| bcc04f4113 | |||
| 470b3fa3b3 | |||
| e84dfeae5c | |||
| 44e86e59b7 | |||
| ac6d9cfa3f | |||
| 1a59711760 | |||
| b60ce7a057 | |||
| b4ac210bef | |||
| 9360c58975 | |||
| 43d9a460ce | |||
| 15fee5a8a5 | |||
| fad038392b | |||
| 558d65a0b0 | |||
| 077fca36c0 | |||
| 947769f6f9 | |||
| 7c480c5faf | |||
| fa6c72cb08 | |||
| 9af2926ec1 | |||
| 366a8266bd | |||
| 713fbac54a | |||
| 8198fba689 | |||
| 20e14a36c9 | |||
| 176e98518d | |||
| 31440056c1 | |||
| 6c7b39ed03 | |||
| 96989f7e0c | |||
| 22a23b3b12 | |||
| 90a855a289 | |||
| fd67373995 | |||
| 3b5e095f74 | |||
| c7850df186 | |||
| 6bbcc55cf6 | |||
| e63ea4bbff | |||
| 5a760fb1ac | |||
| e63f2ee3ad | |||
| 46c6dcb7e6 | |||
| bf42bbe27b | |||
| e89fec4b2d | |||
| 715bb92929 | |||
| 109074f98e | |||
| 45fbdb7914 | |||
| cb2e0789d8 | |||
| de1ca783b6 | |||
| 3a9c24b784 | |||
| b57a333fc9 | |||
| 288bf1343e | |||
| a0b1caed54 | |||
| 9282c61183 | |||
| ee6677ee08 | |||
| 3276eb3516 | |||
| a3fa7bbadc | |||
| 637074b354 | |||
| 547bfbaec0 | |||
| 4e1fcf4682 | |||
| 4d50fd2020 | |||
| 2c510c8ca1 | |||
| 8283e2a8e7 | |||
| 3f82631a95 | |||
| d77864494d | |||
| c5ed6d4ece | |||
| 9412f6eccf | |||
| d9aa102e77 | |||
| 6f80476135 | |||
| e18a854a9f | |||
| 0d676e2504 | |||
| fcca09e74a | |||
| 107e44a399 | |||
| e79a3db255 | |||
| bec9ffd981 | |||
| d4070ed9b7 | |||
| 01e4af47e2 | |||
| 220af5c350 | |||
| 15fd16d395 | |||
| af87ea14e0 | |||
| 5c8b334a84 | |||
| d3c3c61ab1 | |||
| c28952f6e9 | |||
| aaf7ffec85 | |||
| 3ef55eac62 | |||
| 515806e194 | |||
| d0d1632ca1 | |||
| 79e22cf828 | |||
| 563cb514a1 | |||
| 84131d8533 | |||
| 55cb7ebf8f | |||
| b9e2c1ebd9 | |||
| 7377399673 | |||
| 21199b2a56 | |||
| e72c6aa349 | |||
| 13c583d32b | |||
| 129c291469 | |||
| 67609b28a4 | |||
| 4dc206eaa6 | |||
| 28e91507db | |||
| 5f74fb0d43 | |||
| 09f5859feb | |||
| cee416889a | |||
| e2a334384a | |||
| a840e909a1 | |||
| 2f952c380f | |||
| 5d919eb329 | |||
| 1d5d825135 | |||
| 2a8cdbdb84 | |||
| 5e2081b8f9 | |||
| fbaeda1956 | |||
| c761865b9b | |||
| 700c50af5d | |||
| b38cca06b1 | |||
| 6429573918 | |||
| f77f5ccdc9 | |||
| 7aeddcdb2c | |||
| a25ef87a51 | |||
| fbeb1beca0 | |||
| 059bde9711 | |||
| 2ca563f570 | |||
| 88c7ca9373 | |||
| 83486641aa | |||
| 6bd0c87881 | |||
| 1a64da70c9 | |||
| 87ea2bb147 | |||
| d9e9be53d8 | |||
| 142f037497 | |||
| 6cdb626d78 | |||
| 6e81029b8b | |||
| 1c7048e710 | |||
| 23c7cfbe6e | |||
| b45750eb1b | |||
| 550c9c1120 | |||
| d5dfb23cf2 | |||
| 90ee4f8f38 | |||
| c037e703a6 | |||
| 6bf746d4aa | |||
| ca005fb4d0 | |||
| 5f2bac9c2f | |||
| dc0e7dc7d3 | |||
| 740daab55b | |||
| 1953089653 | |||
| 25e0af042e | |||
| 0265334976 | |||
| 6aa9ec9ec1 | |||
| 459aa9b6f5 | |||
| 06655dde6a | |||
| 5c153b0c89 | |||
| 29dd970ea7 | |||
| f6701f8ec9 | |||
| 7f119ba0cc | |||
| e7e5115fc0 | |||
| 0e0bd5a890 | |||
| 9cc2470901 | |||
| b07da3e11d | |||
| 36e0e9fb1e | |||
| 0e85162c11 | |||
| 7329fd9272 | |||
| 30fb8f077a | |||
| f2f66489d1 | |||
| 3cb1366966 | |||
| 907b315601 | |||
| 877a1db87b | |||
| 5b6b50bf6a | |||
| 6c8fbbeaa8 | |||
| da67902cf5 | |||
| 8533689ca7 | |||
| 8f85b22866 | |||
| fa8a4d61ea | |||
| 89010395de | |||
| 5b969b6438 | |||
| 2dee6901f7 | |||
| fced419509 | |||
| 09ff0482df | |||
| d9986d879b | |||
| 15d7d07809 | |||
| a556ece626 | |||
| 35ce037288 | |||
| a32b4074f8 | |||
| c3da7e10f7 | |||
| 135c8d05c1 | |||
| 8afd1a1975 | |||
| dcdd638719 | |||
| c661d491a5 | |||
| 82f104d682 | |||
| f9e42aed74 | |||
| c8343500af | |||
| 5a96ed6af5 | |||
| 84cdc6a8e4 | |||
| 2b6ccb23e4 | |||
| be02b89d9b | |||
| b4f07a0f92 | |||
| 2093913b17 | |||
| 7705dac42b | |||
| d23f01dd87 | |||
| ec2b73ceda | |||
| b8f226ce45 | |||
| b232ddb075 | |||
| 87a821903d | |||
| 9645811baa | |||
| 2e2a550b1f | |||
| 98dff514e8 | |||
| 9332a87d98 | |||
| d031dab174 | |||
| 7ddb46ff9f | |||
| 4a4a08df16 | |||
| 4830daeab3 | |||
| dd05a6652c | |||
| e78947260d | |||
| 5dd856070e | |||
| 2e868eba39 | |||
| 9ba80a53cf | |||
| 1780968f50 | |||
| 9abd9cc933 | |||
| 18f20fbf3b | |||
| 3e6d18dd8c | |||
| 99d2ddd001 | |||
| 1647f7ab9f | |||
| 5ae64e17e6 | |||
| c5d4e959cd | |||
| c056a29375 | |||
| 02409d8051 | |||
| 805434239a | |||
| 179c968443 | |||
| cfc68017e0 | |||
| 2300d68321 | |||
| 873e79d2c0 | |||
| 17e74a7b02 | |||
| 795cff68fd | |||
| 162bf48b5e | |||
| 239afdbd7c | |||
| e53e083507 | |||
| 1acfa52c64 | |||
| fa9eec08c1 | |||
| 2160fb3c46 | |||
| 43e7938281 | |||
| b7baa219a4 | |||
| 5d27613b38 | |||
| d8ff46853d | |||
| 804fd376b9 | |||
| 8ef3a78386 | |||
| 05bd084174 | |||
| 920727e375 | |||
| 3647c8deee | |||
| 43c15356d9 | |||
| 7c28466ec8 | |||
| edf5426e80 | |||
| 3fdaead2f7 | |||
| 1b2181d1f7 | |||
| ee2f5c6415 | |||
| 503ce499d1 | |||
| 1ac04cd52c | |||
| c3801f2366 | |||
| 7703757dab | |||
| 756b8210b8 | |||
| 30ce841030 | |||
| 3036676a93 | |||
| bf649ff1f6 | |||
| 9f68011207 | |||
| b5a95bacba | |||
| 6ec780b679 | |||
| fab67cb5cb | |||
| f24990d10f | |||
| 43a3f859e2 | |||
| f3129aaa90 | |||
| 3199063914 | |||
| 7d6cc33c2a | |||
| 0b914bb7c6 | |||
| 5cff9ce3e8 | |||
| 73bc5a4443 | |||
| 65e5bee4e3 | |||
| c02192cd71 | |||
| 37757d76e1 | |||
| 3226cbc8c4 | |||
| ab720971de | |||
| 613a886604 | |||
| 3581267072 | |||
| 7481f7e098 | |||
| c44b251413 | |||
| 2541c89b99 | |||
| bfacfcfe55 | |||
| b221da7021 | |||
| 8d381ea020 | |||
| 3bde42fd21 | |||
| 9790c7b0bc | |||
| b10807cd84 | |||
| 799e3b2a88 | |||
| a95a31cf74 | |||
| f43b438be0 | |||
| 6d4c7b5e0d | |||
| bbc64efb12 | |||
| c0cb5f6dcb | |||
| fc9874ece6 | |||
| c098bb2c5e | |||
| 605d35b364 | |||
| 10fcdfd886 | |||
| 44aac90d41 | |||
| f5cc3fd347 | |||
| b4e8574a2f | |||
| 191680a6b9 | |||
| 0f1134e25e | |||
| d506fee663 | |||
| 11544b77b6 | |||
| aa5b9d4e7f | |||
| c04e0b606d | |||
| eb1345dcb3 | |||
| 7186b58e80 | |||
| 5859017040 | |||
| d31e1aeb4e | |||
| 3d05e4fb6a | |||
| ba1de91f85 | |||
| 1e9379dc02 | |||
| 965419bd58 | |||
| b21fbabd67 | |||
| 31446c01be | |||
| 81a3336053 | |||
| 8367342b25 | |||
| 2df073a792 | |||
| 8236fc97cc | |||
| 0364de36bd | |||
| 12195eb879 | |||
| acdc46c43f | |||
| 52eed1b6f9 | |||
| a91d1de9ad | |||
| 492d2e49fe | |||
| 49bc439000 | |||
| 8c919cc26c | |||
| 8e86ffccd6 | |||
| 228de102e7 | |||
| 37b5d1be2a | |||
| ef8f866330 | |||
| e862b81734 | |||
| a3f66b3f87 | |||
| 3e12ad9887 | |||
| fe36b59b4c | |||
| b735a69c1b | |||
| 7b1b6f9e24 | |||
| c047fbc581 | |||
| 40239524d1 | |||
| 2f93050e2e | |||
| dc8ac6c195 | |||
| 8edf6b4ad6 | |||
| 4d019046bc | |||
| 67359023f4 | |||
| a6cbfbd3fd | |||
| b7cac1fe48 | |||
| 226e0cd803 | |||
| 53538bfead | |||
| a78bf1b22f | |||
| 0803b46f9d | |||
| f64e6ec3c9 | |||
| 46652ac73d | |||
| e30b1854fc | |||
| 8a5e57c1d2 | |||
| f582c9ae0d | |||
| 8a762a66ae | |||
| d537d3c79e | |||
| 07f0b07a91 | |||
| 555b57941d | |||
| 4be2b119d1 | |||
| 8255c7fe79 | |||
| a21e5ea158 | |||
| ac4c50b62c | |||
| ca919a4188 | |||
| b9c9af5509 | |||
| 6cbfa63d48 | |||
| c3066a7847 | |||
| 16fbab783e | |||
| a58314a825 | |||
| 89b9ece45d | |||
| b5d5867bc1 | |||
| 5a54f5808d | |||
| 664a39e0f1 | |||
| 9935860efb | |||
| 25a05eec1a | |||
| 32c0983bb7 | |||
| 3650da6c12 | |||
| 40b52120d0 | |||
| 74eb6e180b | |||
| 4c2e921f9e | |||
| bba5b8b72c | |||
| e7290bc84f | |||
| f7619cdbf2 | |||
| bff9f1dd89 | |||
| 81b7ccea22 | |||
| f2c8ede0e0 | |||
| adf2fc56e8 | |||
| 78a34a8437 | |||
| aa6bbfb7e7 | |||
| c907a98099 | |||
| d3ae20bebe | |||
| b69560e62e | |||
| b01309678c | |||
| eb4160636f | |||
| 6d489e72c5 | |||
| 156eb8cbe1 | |||
| e98abb1e23 | |||
| e88c286f9e | |||
| 0de3a9d65c | |||
| f98e0622b5 | |||
| 85cc51bfd3 | |||
| c3bb4ff0d3 | |||
| 55b579a34e | |||
| 3a5a242346 | |||
| e3ac0d6ecc | |||
| 211e2956ca | |||
| efed18c386 | |||
| 3dd9a5705d | |||
| 462b38a473 | |||
| 7979fcd896 | |||
| 642b1810c5 | |||
| e0b6eec03c | |||
| 9c4ff2ddd8 | |||
| 0ddd202852 | |||
| 81be8e1316 | |||
| 4e2e05b451 | |||
| 4262074948 | |||
| 0f98a35643 | |||
| 4c1fceac54 | |||
| 45996df0c2 | |||
| 1669b205da | |||
| 9f1d988651 | |||
| 5541e3c683 | |||
| 6632703b70 | |||
| eebfffe4a6 | |||
| 70264c1cd5 | |||
| 0a124a2424 | |||
| 2a34f21667 | |||
| 62b26036ed | |||
| 611b861678 | |||
| c7bc875b0e | |||
| 841f3f9775 | |||
| 8b225d2046 | |||
| cf9bd2335b | |||
| e07bfa8d5f | |||
| 17e194b69d | |||
| f86cb8a81f | |||
| 739c28cf81 | |||
| fcdb33fce2 | |||
| 1eb8e4f430 | |||
| 1d937777c0 | |||
| 308afda5c2 | |||
| f7b00e02ee | |||
| 0ff29e1f90 | |||
| 3fe31e8628 | |||
| 33263bfcfa | |||
| d6caab29dc | |||
| 5d181adcb8 | |||
| 4ba2fdc289 | |||
| 140dc32775 | |||
| 78568cd5be | |||
| b010cdb072 | |||
| 37e299733b | |||
| f3a1587cf1 | |||
| 821ca1683b | |||
| 7e13ca2757 | |||
| 129c91c876 | |||
| d2f6ff0b40 | |||
| 69414e4174 | |||
| 0654fb4b5f | |||
| d7d40e6d27 | |||
| bec7e0c229 | |||
| 82a19e41e7 | |||
| 867ae614e5 | |||
| 22b7c6ccc7 | |||
| 9e9704580a | |||
| 1ef608cf8a | |||
| b64cd37ec6 | |||
| 44e9d55fb8 | |||
| ab9515caaf | |||
| 0dd0fc9418 | |||
| 24d5f3e9b2 | |||
| 693f57eef7 | |||
| 7c17ce0787 | |||
| 400a3ccd1c | |||
| a8cdac1872 | |||
| dedac1f98d | |||
| efa317f0d1 | |||
| 9b55bb10de | |||
| 122fb41fa8 | |||
| d6b5e8c77d | |||
| a6ad98dd53 | |||
| cc35f2086a | |||
| 7038aae6db | |||
| f5433153d0 | |||
| 8b7c59d949 | |||
| faef524b6c | |||
| a2bdc03684 | |||
| bd5f9740b7 | |||
| 505bb2ca41 | |||
| 009711ab3f | |||
| 7ad7653cdf | |||
| e5f51e4dfa | |||
| 92029d2b43 | |||
| 7da059791e | |||
| af57527961 | |||
| 9a8f45663e | |||
| 8528c8a1f6 | |||
| d682fd0ace | |||
| 31af87a81e | |||
| 386d6ae06a | |||
| df79ae7971 | |||
| d30c642707 | |||
| f463e2b43e | |||
| ed49a4fc89 | |||
| b897250fde | |||
| 06709a5afe | |||
| 6eee9e6080 | |||
| c50e6e51c5 | |||
| d1e9248695 | |||
| a535f10d0c | |||
| 009aa5e3f9 | |||
| 675f03fc71 | |||
| 99eccdf4c3 | |||
| 29afa5e80f | |||
| bf26bc6539 | |||
| f7b40646e2 | |||
| 84c1fb159b | |||
| 62ef95ff93 | |||
| 731b729db7 | |||
| a1c464d1a7 | |||
| eb2ecbf9b3 | |||
| 16fab66f7b | |||
| b3e70ac491 | |||
| d5177bb049 | |||
| bb0faeb3cf | |||
| 761feef3c0 | |||
| 352bae03ea | |||
| b0085cae7d | |||
| b2f93f1752 | |||
| 354510f2c4 | |||
| b8c6175193 | |||
| d409b110f5 | |||
| 5c73220158 | |||
| 397721e7ea | |||
| 5714275bcd | |||
| 833c584e41 | |||
| 49b24487c5 | |||
| 6d4eb01a07 | |||
| a752203ff9 | |||
| c69427613d | |||
| 5d75a83568 | |||
| 7642a0632d | |||
| 841054a713 | |||
| 49f368fd63 | |||
| 1dc99250c8 | |||
| 00edccf326 | |||
| 7f3af90645 | |||
| 1a030f06cd | |||
| 0477858bed | |||
| 000e061d00 | |||
| 224978b31b | |||
| 7fd133f13b | |||
| e40054ba1a | |||
| 1761734447 | |||
| f23ee3cd95 | |||
| e455ac0c7d | |||
| 94f68c6dfe | |||
| a22f022014 | |||
| 22e7edb698 | |||
| 52fcda53c5 | |||
| be6c5d0e43 | |||
| 6ed0037f50 | |||
| 81e3a5ff47 | |||
| 2a6e28c9d0 | |||
| 9e928b0b09 | |||
| d5deaad628 | |||
| 232ae8a6fd | |||
| c766e5d94a | |||
| 5f27a2304b | |||
| 0cb0f44f18 | |||
| d1c1aa0a26 | |||
| 832957f621 | |||
| 24985ac984 | |||
| 0b652c2f2a | |||
| 98d83bb438 | |||
| e13717b056 | |||
| 552e687b9c | |||
| fb29421644 | |||
| f5d930bd58 | |||
| 26af4ef476 | |||
| ac8940da26 | |||
| f597152a62 | |||
| 58f94f108c | |||
| a5e0bbd50e | |||
| ac273fbfc9 | |||
| 548fd03bd5 | |||
| b564c25d2a | |||
| bd821dc553 | |||
| e46a33417b | |||
| a3419a841f | |||
| cafc1ba0bd | |||
| 0112c097d9 | |||
| 507d03fd90 | |||
| b6453ac360 | |||
| b693ceb20e | |||
| 31e81a616d | |||
| 928f60b56f | |||
| 12a1900d75 | |||
| c72b6f7e5b | |||
| 3cf9f72dcb | |||
| 9b1318e975 | |||
| d3d0f04c23 | |||
| b606d04944 | |||
| 7afd50d241 | |||
| 94f06d5ff8 | |||
| 85424fd15e | |||
| cff391f78c | |||
| 3e420bb747 | |||
| 8aece0a266 | |||
| 81b27b6918 | |||
| 727943c4b3 | |||
| 9f2c032157 | |||
| d6a5b1af8d | |||
| dda9dd5beb | |||
| 888887f09a | |||
| b70a1a2f48 | |||
| 54730c368c | |||
| 21672e6f8d | |||
| 4ffc992872 | |||
| 8eea2ec652 | |||
| 3df01ee7bb | |||
| d0ad6e4319 | |||
| 37aec6dacc | |||
| cdc5aae4b7 | |||
| fd2b89fd24 | |||
| f1683e54c3 | |||
| c66dd2e7d3 | |||
| 5a5b921bbf | |||
| 30cc65bd9d | |||
| 29a45aa02d | |||
| 08b25ea8d3 | |||
| 7ffcb83563 | |||
| b0b64c1ba9 | |||
| bc5dfa02ec | |||
| c69f5c220a | |||
| 7f28d430d0 | |||
| 30dc024903 | |||
| 6403053989 | |||
| ba352c15e8 | |||
| e9e084e6f1 | |||
| 26e92c2bd6 | |||
| 41d761f822 | |||
| 832a03bf27 | |||
| ebf5254629 | |||
| e199b529c8 | |||
| 06edea64ae | |||
| 6d3afe36d1 | |||
| b22d2b57a2 | |||
| b453086936 | |||
| 103489c566 | |||
| 3dd27557ea | |||
| a64188dd5e | |||
| 44e428655a | |||
| 96d8b425d5 | |||
| 7df172afac | |||
| 9bb44925c8 | |||
| 320aeb815b | |||
| ab4b678498 | |||
| 52bf2071a5 | |||
| f0bb931a64 | |||
| 30f1fecc54 | |||
| 85a424a824 | |||
| 6405bf40e3 | |||
| b79fd26ca4 | |||
| caadcce7f1 | |||
| c250200d03 | |||
| 51c05087e7 | |||
| 7a2e65f292 | |||
| 0489c09ba3 | |||
| 70038d244d | |||
| a9e0dedd31 | |||
| f6ad51219d | |||
| a42af79040 | |||
| 792634527a | |||
| 5627219c56 | |||
| 4e81d4b476 | |||
| 001d58cd89 | |||
| 03c96d8fdb | |||
| b5423192fb | |||
| 1efb40a07e | |||
| 6c6023dc84 | |||
| a6dc4b010c | |||
| dd87da4134 | |||
| 627ff6cbe2 | |||
| 9cc8435cae | |||
| 3d977450ca | |||
| f7c482ceed | |||
| c38b90ee60 | |||
| 70491431fa | |||
| 6bc055911e | |||
| 9b1e13b963 | |||
| 74a73893d4 | |||
| 4a3ab17851 | |||
| 5b8efe71a7 | |||
| b7f2d9bcbe | |||
| f4efc6dbbf | |||
| efab760253 | |||
| 48826e2160 | |||
| 0b4e231814 | |||
| 6f2d811338 | |||
| 9c9ce5e2e1 | |||
| 08b4559b26 | |||
| 913ad5e2c6 | |||
| 7649dc616c | |||
| efabb29a52 | |||
| e3612e31d7 | |||
| 2575d2eaee | |||
| f041002e73 | |||
| 0da49c1eb6 | |||
| a3a84b0cd7 | |||
| 33be034e2b | |||
| f590263d9f | |||
| a1c378a9d5 | |||
| 617db9c48a | |||
| b05f78c388 | |||
| d9ecb2359b | |||
| 5f9a61c755 | |||
| 3dd178c029 | |||
| ed938aa333 | |||
| 717ed18077 | |||
| 1f43569a4d | |||
| 991fd6559d | |||
| 9d98993838 | |||
| 3eae2ce457 | |||
| f24edcdba9 | |||
| a2c7110b07 | |||
| 300b68ae83 | |||
| ea53ac9bfb | |||
| 6f9c0917eb | |||
| b66857f0da | |||
| 52a51b6fd4 | |||
| 06ff0152bb | |||
| ea1ecbc73a | |||
| 33d2667c99 | |||
| b333f83470 | |||
| fd54e4cae0 | |||
| b1d6ac4d52 | |||
| bd9a062329 | |||
| 56fffab825 | |||
| e73eafccdb | |||
| f7d23bc8a2 | |||
| e531190616 | |||
| a88b5f5019 | |||
| 538fe5e17b | |||
| 62a350059b | |||
| 02657d7766 | |||
| f13e39450c | |||
| 1544f2bf4b | |||
| cd41de37e9 | |||
| b4c9cbd6a6 | |||
| c5668f42b2 | |||
| 99e8f112b2 | |||
| 57f4519c3f | |||
| 671f2eeb77 | |||
| ab31f8202c | |||
| ffd532b630 | |||
| f911a91000 | |||
| fed8c35f2f | |||
| aafc23a5ec | |||
| eee45744d0 | |||
| 51493322c3 | |||
| baeb0c9a14 | |||
| 141cdf121d | |||
| cdb9235a83 | |||
| cae0e0ef98 | |||
| ec98b0cba6 | |||
| d468254b21 | |||
| 462e0ed548 | |||
| cbfc49bd0c | |||
| 176652010c | |||
| 2e516615bd | |||
| 6d1db9535f | |||
| 09bc0377d8 | |||
| 197dcccb4e | |||
| e082fdeddd | |||
| e4e22b8cd8 | |||
| 11bb3574a8 | |||
| 9ed5e08089 | |||
| 01abdf05ed | |||
| 65e6e86378 | |||
| 2b3677e503 | |||
| 84dcbb4847 | |||
| cca6415c2a | |||
| 73f3d52bb3 | |||
| 4e167f02fb | |||
| ec76385bcf | |||
| 387a72d93e | |||
| b8d8f29c97 | |||
| 8e7dfe9adb | |||
| 6471a96b19 | |||
| 3b66d9b53e | |||
| 3385493b6a | |||
| 993b61ecb6 | |||
| ea5cea6d77 | |||
| 47db763ea6 | |||
| 054cfd196a | |||
| 4d9271c7ed | |||
| 719c641692 | |||
| 95eaab3486 | |||
| b4d03fc9e9 | |||
| 3b5ed964b2 | |||
| 67a91b5283 | |||
| 580a4d8a1c | |||
| ad0b130dc4 | |||
| 5ecdb953c7 | |||
| c7132f5890 | |||
| 6d680e4df7 | |||
| 18e2edb15f | |||
| f4106e4be5 | |||
| a53f744035 | |||
| 61bcf5354a | |||
| dd686e80da | |||
| 794d00d82f | |||
| e9801242f5 | |||
| 11411a20b7 | |||
| db089f4fb9 | |||
| 34721971c5 | |||
| 26f80349f6 | |||
| 190006dcc0 | |||
| 1d7d41a350 | |||
| d74bc56487 | |||
| fa4eeea424 | |||
| 3c8ac29ebe | |||
| 18487eeac5 | |||
| b040530219 | |||
| 7fb22825ae | |||
| 61fe88fd7f | |||
| 60226cc62b | |||
| fad33ddf7a | |||
| 630eec59ee | |||
| 4e096a2cd7 | |||
| 28241518bf | |||
| e1c2c9c6fc | |||
| ef96471937 | |||
| 4b9385bee0 | |||
| 9d68ff5d15 | |||
| a644f89bbd | |||
| ed9b721cd5 | |||
| 2b1f28c2fd | |||
| 6d563856a6 | |||
| 491ba78729 | |||
| d978d574ad | |||
| a2cd7d81ed | |||
| e0366afcc2 | |||
| 1653d86552 | |||
| 05399839a3 | |||
| 49afafc50a | |||
| 063ffe97c1 | |||
| 35938ecbee | |||
| 9999f6b1d3 | |||
| 93d759e743 | |||
| d989001073 | |||
| ad231a5bb7 | |||
| b67d18f569 | |||
| f65263d027 | |||
| 3870f7c7cf | |||
| 88c893cc44 | |||
| 53a95845f4 | |||
| 8f6b8b2dae | |||
| fefcd125b5 | |||
| 5fb6684b31 | |||
| 7a93e9b4dd | |||
| a04cdf789a | |||
| 9d1489adbe | |||
| 10c7b7b420 | |||
| 8bf66eb664 | |||
| 43eedfafc5 | |||
| c6b2e97835 | |||
| c6b0a74873 |
@@ -0,0 +1,47 @@
|
||||
---
|
||||
BasedOnStyle: WebKit
|
||||
AccessModifierOffset: -2
|
||||
AlignEscapedNewlinesLeft: false
|
||||
AlignTrailingComments: true
|
||||
AllowAllParametersOfDeclarationOnNextLine: false
|
||||
AllowShortIfStatementsOnASingleLine: false
|
||||
AllowShortLoopsOnASingleLine: false
|
||||
AlwaysBreakBeforeMultilineStrings: false
|
||||
AlwaysBreakTemplateDeclarations: false
|
||||
BinPackParameters: true
|
||||
BreakBeforeBinaryOperators: false
|
||||
BreakBeforeBraces: Linux
|
||||
BreakBeforeTernaryOperators: true
|
||||
BreakConstructorInitializersBeforeComma: false
|
||||
ColumnLimit: 0
|
||||
ConstructorInitializerAllOnOneLineOrOnePerLine: true
|
||||
ConstructorInitializerIndentWidth: 4
|
||||
ContinuationIndentWidth: 4
|
||||
Cpp11BracedListStyle: true
|
||||
DerivePointerBinding: false
|
||||
ExperimentalAutoDetectBinPacking: false
|
||||
IndentCaseLabels: true
|
||||
IndentFunctionDeclarationAfterType: false
|
||||
IndentWidth: 4
|
||||
MaxEmptyLinesToKeep: 3
|
||||
NamespaceIndentation: None
|
||||
ObjCSpaceBeforeProtocolList: true
|
||||
PenaltyBreakBeforeFirstCallParameter: 19
|
||||
PenaltyBreakComment: 60
|
||||
PenaltyBreakFirstLessLess: 120
|
||||
PenaltyBreakString: 1000
|
||||
PenaltyExcessCharacter: 1000000
|
||||
PenaltyReturnTypeOnItsOwnLine: 60
|
||||
PointerBindsToType: false
|
||||
SpaceAfterControlStatementKeyword: true
|
||||
SpaceBeforeAssignmentOperators: true
|
||||
SpaceInEmptyParentheses: false
|
||||
SpacesBeforeTrailingComments: 1
|
||||
SpacesInAngles: false
|
||||
SpacesInCStyleCastParentheses: false
|
||||
SpacesInParentheses: false
|
||||
Standard: Auto
|
||||
TabWidth: 4
|
||||
UseTab: Never
|
||||
|
||||
...
|
||||
@@ -0,0 +1,3 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: [NSExceptional]
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Report a bug in FLEX
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
### Environment
|
||||
- Platform+version: **iOS 14** <!--- Change to match your platform and version -->
|
||||
- FLEX version: **9.9.9** <!--- Change to the version of FLEX you're using -->
|
||||
<!--- FLEXing / libFLEX users: please include FLEXing and libFLEX versions separately -->
|
||||
|
||||
### Bug Report
|
||||
|
||||
Here, you can provide a description of the bug. Some tips:
|
||||
|
||||
- Please do not paste an entire crash log. Upload the crash log to something like [ghostbin.co](https://ghostbin.co/) or another paste service. Alternatively, you can cut out the relevant stack trace and paste that inside a ` ```code block``` `
|
||||
- If the bug is more complex than "this button is broken" or a crash, consider including a sample project. For example, if your app's requests aren't showing up in the network history page.
|
||||
- Providing steps to reproduce is always helpful!
|
||||
- If you want to include a screenshot or GIF, consider modifying the default markdown for uploaded images to use this code to make the image smaller on desktop:
|
||||
```
|
||||
<img width="50%" src=your-image-url >
|
||||
```
|
||||
|
||||
This template is a suggestion. You may format your issue however you want, but generally you should at least include your iOS version and FLEX version.
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest a new feature for FLEX
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -17,3 +17,7 @@ DerivedData
|
||||
*.ipa
|
||||
*.xcuserstate
|
||||
.DS_Store
|
||||
/Example/Pods
|
||||
Podfile.lock
|
||||
IDEWorkspaceChecks.plist
|
||||
*.xcworkspace
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
language: objective-c
|
||||
xcode_workspace: FLEX.xcworkspace
|
||||
xcode_sdk: iphonesimulator
|
||||
before_install:
|
||||
- gem install xcpretty
|
||||
matrix:
|
||||
include:
|
||||
- xcode_scheme: UICatalog
|
||||
- xcode_scheme: FLEX
|
||||
script:
|
||||
- set -o pipefail
|
||||
- xcodebuild -workspace $TRAVIS_XCODE_WORKSPACE -scheme $TRAVIS_XCODE_SCHEME -sdk $TRAVIS_XCODE_SDK build | xcpretty
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"search.exclude": {
|
||||
"Classes/Headers": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# Contributing to FLEX #
|
||||
|
||||
We welcome contributions! Please open a pull request with your changes.
|
||||
@@ -0,0 +1,89 @@
|
||||
//
|
||||
// FLEXFilteringTableViewController.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 3/9/20.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXTableViewController.h"
|
||||
|
||||
#pragma mark - FLEXTableViewFiltering
|
||||
@protocol FLEXTableViewFiltering <FLEXSearchResultsUpdating>
|
||||
|
||||
/// An array of visible, "filtered" sections. For example,
|
||||
/// if you have 3 sections in \c allSections and the user searches
|
||||
/// for something that matches rows in only one section, then
|
||||
/// this property would only contain that on matching section.
|
||||
@property (nonatomic, copy) NSArray<FLEXTableViewSection *> *sections;
|
||||
/// An array of all possible sections. Empty sections are to be removed
|
||||
/// and the resulting array stored in the \c section property. Setting
|
||||
/// this property should immediately set \c sections to \c nonemptySections
|
||||
///
|
||||
/// Do not manually initialize this property, it will be
|
||||
/// initialized for you using the result of \c makeSections.
|
||||
@property (nonatomic, copy) NSArray<FLEXTableViewSection *> *allSections;
|
||||
|
||||
/// This computed property should filter \c allSections for assignment to \c sections
|
||||
@property (nonatomic, readonly, copy) NSArray<FLEXTableViewSection *> *nonemptySections;
|
||||
|
||||
/// This should be able to re-initialize \c allSections
|
||||
- (NSArray<FLEXTableViewSection *> *)makeSections;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
#pragma mark - FLEXFilteringTableViewController
|
||||
/// A table view which implements \c UITableView* methods using arrays of
|
||||
/// \c FLEXTableViewSection objects provied by a special delegate.
|
||||
@interface FLEXFilteringTableViewController : FLEXTableViewController <FLEXTableViewFiltering>
|
||||
|
||||
/// Stores the current search query.
|
||||
@property (nonatomic, copy) NSString *filterText;
|
||||
|
||||
/// This property is set to \c self by default.
|
||||
///
|
||||
/// This property is used to power almost all of the table view's data source
|
||||
/// and delegate methods automatically, including row and section filtering
|
||||
/// when the user searches, 3D Touch context menus, row selection, etc.
|
||||
///
|
||||
/// Setting this property will also set \c searchDelegate to that object.
|
||||
@property (nonatomic, weak) id<FLEXTableViewFiltering> filterDelegate;
|
||||
|
||||
/// Defaults to \c NO. If enabled, all filtering will be done by calling
|
||||
/// \c onBackgroundQueue:thenOnMainQueue: with the UI updated on the main queue.
|
||||
@property (nonatomic) BOOL filterInBackground;
|
||||
|
||||
/// Defaults to \c NO. If enabled, one • will be supplied as an index title for each section.
|
||||
@property (nonatomic) BOOL wantsSectionIndexTitles;
|
||||
|
||||
/// Recalculates the non-empty sections and reloads the table view.
|
||||
///
|
||||
/// Subclasses may override to perform additional reloading logic,
|
||||
/// such as calling \c -reloadSections if needed. Be sure to call
|
||||
/// \c super after any logic that would affect the appearance of
|
||||
/// the table view, since the table view is reloaded last.
|
||||
///
|
||||
/// Called at the end of this class's implementation of \c updateSearchResults:
|
||||
- (void)reloadData;
|
||||
|
||||
/// Invoke this method to call \c -reloadData on each section
|
||||
/// in \c self.filterDelegate.allSections.
|
||||
- (void)reloadSections;
|
||||
|
||||
#pragma mark FLEXTableViewFiltering
|
||||
|
||||
@property (nonatomic, copy) NSArray<FLEXTableViewSection *> *sections;
|
||||
@property (nonatomic, copy) NSArray<FLEXTableViewSection *> *allSections;
|
||||
|
||||
/// Subclasses can override to hide specific sections under certain conditions
|
||||
/// if using \c self as the \c filterDelegate, as is the default.
|
||||
///
|
||||
/// For example, the object explorer hides the description section when searching.
|
||||
@property (nonatomic, readonly, copy) NSArray<FLEXTableViewSection *> *nonemptySections;
|
||||
|
||||
/// If using \c self as the \c filterDelegate, as is the default,
|
||||
/// subclasses should override to provide the sections for the table view.
|
||||
- (NSArray<FLEXTableViewSection *> *)makeSections;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,209 @@
|
||||
//
|
||||
// FLEXFilteringTableViewController.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 3/9/20.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXFilteringTableViewController.h"
|
||||
#import "FLEXTableViewSection.h"
|
||||
#import "NSArray+FLEX.h"
|
||||
#import "FLEXMacros.h"
|
||||
|
||||
@interface FLEXFilteringTableViewController ()
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXFilteringTableViewController
|
||||
@synthesize allSections = _allSections;
|
||||
|
||||
#pragma mark - View controller lifecycle
|
||||
|
||||
- (void)loadView {
|
||||
[super loadView];
|
||||
|
||||
if (!self.filterDelegate) {
|
||||
self.filterDelegate = self;
|
||||
} else {
|
||||
[self _registerCellsForReuse];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)_registerCellsForReuse {
|
||||
for (FLEXTableViewSection *section in self.filterDelegate.allSections) {
|
||||
if (section.cellRegistrationMapping) {
|
||||
[self.tableView registerCells:section.cellRegistrationMapping];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Public
|
||||
|
||||
- (void)setFilterDelegate:(id<FLEXTableViewFiltering>)filterDelegate {
|
||||
_filterDelegate = filterDelegate;
|
||||
filterDelegate.allSections = [filterDelegate makeSections];
|
||||
|
||||
if (self.isViewLoaded) {
|
||||
[self _registerCellsForReuse];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)reloadData {
|
||||
[self reloadData:self.nonemptySections];
|
||||
}
|
||||
|
||||
- (void)reloadData:(NSArray *)nonemptySections {
|
||||
// Recalculate displayed sections
|
||||
self.filterDelegate.sections = nonemptySections;
|
||||
|
||||
// Refresh table view
|
||||
if (self.isViewLoaded) {
|
||||
[self.tableView reloadData];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)reloadSections {
|
||||
for (FLEXTableViewSection *section in self.filterDelegate.allSections) {
|
||||
[section reloadData];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Search
|
||||
|
||||
- (void)updateSearchResults:(NSString *)newText {
|
||||
NSArray *(^filter)(void) = ^NSArray *{
|
||||
self.filterText = newText;
|
||||
|
||||
// Sections will adjust data based on this property
|
||||
for (FLEXTableViewSection *section in self.filterDelegate.allSections) {
|
||||
section.filterText = newText;
|
||||
}
|
||||
|
||||
return nil;
|
||||
};
|
||||
|
||||
if (self.filterInBackground) {
|
||||
[self onBackgroundQueue:filter thenOnMainQueue:^(NSArray *unused) {
|
||||
if ([self.searchText isEqualToString:newText]) {
|
||||
[self reloadData];
|
||||
}
|
||||
}];
|
||||
} else {
|
||||
filter();
|
||||
[self reloadData];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark Filtering
|
||||
|
||||
- (NSArray<FLEXTableViewSection *> *)nonemptySections {
|
||||
return [self.filterDelegate.allSections flex_filtered:^BOOL(FLEXTableViewSection *section, NSUInteger idx) {
|
||||
return section.numberOfRows > 0;
|
||||
}];
|
||||
}
|
||||
|
||||
- (NSArray<FLEXTableViewSection *> *)makeSections {
|
||||
return @[];
|
||||
}
|
||||
|
||||
- (void)setAllSections:(NSArray<FLEXTableViewSection *> *)allSections {
|
||||
_allSections = allSections.copy;
|
||||
// Only display nonempty sections
|
||||
self.sections = self.nonemptySections;
|
||||
}
|
||||
|
||||
- (void)setSections:(NSArray<FLEXTableViewSection *> *)sections {
|
||||
// Allow sections to reload a portion of the table view at will
|
||||
[sections enumerateObjectsUsingBlock:^(FLEXTableViewSection *s, NSUInteger idx, BOOL *stop) {
|
||||
[s setTable:self.tableView section:idx];
|
||||
}];
|
||||
_sections = sections.copy;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - UITableViewDataSource
|
||||
|
||||
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
|
||||
return self.filterDelegate.sections.count;
|
||||
}
|
||||
|
||||
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
|
||||
return self.filterDelegate.sections[section].numberOfRows;
|
||||
}
|
||||
|
||||
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
|
||||
return self.filterDelegate.sections[section].title;
|
||||
}
|
||||
|
||||
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
NSString *reuse = [self.filterDelegate.sections[indexPath.section] reuseIdentifierForRow:indexPath.row];
|
||||
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:reuse forIndexPath:indexPath];
|
||||
[self.filterDelegate.sections[indexPath.section] configureCell:cell forRow:indexPath.row];
|
||||
return cell;
|
||||
}
|
||||
|
||||
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
return UITableViewAutomaticDimension;
|
||||
}
|
||||
|
||||
- (NSArray<NSString *> *)sectionIndexTitlesForTableView:(UITableView *)tableView {
|
||||
if (self.wantsSectionIndexTitles) {
|
||||
return [NSArray flex_forEachUpTo:self.filterDelegate.sections.count map:^id(NSUInteger i) {
|
||||
return @"⦁";
|
||||
}];
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - UITableViewDelegate
|
||||
|
||||
- (BOOL)tableView:(UITableView *)tableView shouldHighlightRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
return [self.filterDelegate.sections[indexPath.section] canSelectRow:indexPath.row];
|
||||
}
|
||||
|
||||
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
FLEXTableViewSection *section = self.filterDelegate.sections[indexPath.section];
|
||||
|
||||
void (^action)(UIViewController *) = [section didSelectRowAction:indexPath.row];
|
||||
UIViewController *details = [section viewControllerToPushForRow:indexPath.row];
|
||||
|
||||
if (action) {
|
||||
action(self);
|
||||
[tableView deselectRowAtIndexPath:indexPath animated:YES];
|
||||
} else if (details) {
|
||||
[self.navigationController pushViewController:details animated:YES];
|
||||
} else {
|
||||
[NSException raise:NSInternalInconsistencyException
|
||||
format:@"Row is selectable but has no action or view controller"];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)tableView:(UITableView *)tableView accessoryButtonTappedForRowWithIndexPath:(NSIndexPath *)indexPath {
|
||||
[self.filterDelegate.sections[indexPath.section] didPressInfoButtonAction:indexPath.row](self);
|
||||
}
|
||||
|
||||
- (UIContextMenuConfiguration *)tableView:(UITableView *)tableView contextMenuConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath point:(CGPoint)point __IOS_AVAILABLE(13.0) {
|
||||
FLEXTableViewSection *section = self.filterDelegate.sections[indexPath.section];
|
||||
NSString *title = [section menuTitleForRow:indexPath.row];
|
||||
NSArray<UIMenuElement *> *menuItems = [section menuItemsForRow:indexPath.row sender:self];
|
||||
|
||||
if (menuItems.count) {
|
||||
return [UIContextMenuConfiguration
|
||||
configurationWithIdentifier:nil
|
||||
previewProvider:nil
|
||||
actionProvider:^UIMenu *(NSArray<UIMenuElement *> *suggestedActions) {
|
||||
return [UIMenu menuWithTitle:title children:menuItems];
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,19 @@
|
||||
//
|
||||
// FLEXNavigationController.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 1/30/20.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface FLEXNavigationController : UINavigationController
|
||||
|
||||
+ (instancetype)withRootViewController:(UIViewController *)rootVC;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,196 @@
|
||||
//
|
||||
// FLEXNavigationController.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 1/30/20.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXNavigationController.h"
|
||||
#import "FLEXExplorerViewController.h"
|
||||
#import "FLEXTabList.h"
|
||||
|
||||
@interface UINavigationController (Private) <UIGestureRecognizerDelegate>
|
||||
- (void)_gestureRecognizedInteractiveHide:(UIGestureRecognizer *)sender;
|
||||
@end
|
||||
@interface UIPanGestureRecognizer (Private)
|
||||
- (void)_setDelegate:(id)delegate;
|
||||
@end
|
||||
|
||||
@interface FLEXNavigationController ()
|
||||
@property (nonatomic, readonly) BOOL toolbarWasHidden;
|
||||
@property (nonatomic) BOOL waitingToAddTab;
|
||||
@property (nonatomic, readonly) BOOL canShowToolbar;
|
||||
@property (nonatomic) BOOL didSetupPendingDismissButtons;
|
||||
@property (nonatomic) UISwipeGestureRecognizer *navigationBarSwipeGesture;
|
||||
@end
|
||||
|
||||
@implementation FLEXNavigationController
|
||||
|
||||
+ (instancetype)withRootViewController:(UIViewController *)rootVC {
|
||||
return [[self alloc] initWithRootViewController:rootVC];
|
||||
}
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
|
||||
self.waitingToAddTab = YES;
|
||||
|
||||
// Add gesture to reveal toolbar if hidden
|
||||
UITapGestureRecognizer *navbarTapGesture = [[UITapGestureRecognizer alloc]
|
||||
initWithTarget:self action:@selector(handleNavigationBarTap:)
|
||||
];
|
||||
|
||||
// Don't cancel touches to work around bug on versions of iOS prior to 13
|
||||
navbarTapGesture.cancelsTouchesInView = NO;
|
||||
[self.navigationBar addGestureRecognizer:navbarTapGesture];
|
||||
|
||||
// Add gesture to dismiss if not presented with a sheet style
|
||||
if (@available(iOS 13, *)) {
|
||||
switch (self.modalPresentationStyle) {
|
||||
case UIModalPresentationAutomatic:
|
||||
case UIModalPresentationPageSheet:
|
||||
case UIModalPresentationFormSheet:
|
||||
break;
|
||||
|
||||
default:
|
||||
[self addNavigationBarSwipeGesture];
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
[self addNavigationBarSwipeGesture];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)viewWillAppear:(BOOL)animated {
|
||||
[super viewWillAppear:animated];
|
||||
|
||||
if (self.beingPresented && !self.didSetupPendingDismissButtons) {
|
||||
for (UIViewController *vc in self.viewControllers) {
|
||||
[self addNavigationBarItemsToViewController:vc.navigationItem];
|
||||
}
|
||||
|
||||
self.didSetupPendingDismissButtons = YES;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)viewDidAppear:(BOOL)animated {
|
||||
[super viewDidAppear:animated];
|
||||
|
||||
if (self.waitingToAddTab) {
|
||||
// Only add new tab if we're presented properly
|
||||
if ([self.presentingViewController isKindOfClass:[FLEXExplorerViewController class]]) {
|
||||
// New navigation controllers always add themselves as new tabs,
|
||||
// tabs are closed by FLEXExplorerViewController
|
||||
[FLEXTabList.sharedList addTab:self];
|
||||
self.waitingToAddTab = NO;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated {
|
||||
[super pushViewController:viewController animated:animated];
|
||||
[self addNavigationBarItemsToViewController:viewController.navigationItem];
|
||||
}
|
||||
|
||||
- (void)dismissAnimated {
|
||||
// Tabs are only closed if the done button is pressed; this
|
||||
// allows you to leave a tab open by dragging down to dismiss
|
||||
if ([self.presentingViewController isKindOfClass:[FLEXExplorerViewController class]]) {
|
||||
[FLEXTabList.sharedList closeTab:self];
|
||||
}
|
||||
|
||||
[self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
|
||||
}
|
||||
|
||||
- (BOOL)canShowToolbar {
|
||||
return self.topViewController.toolbarItems.count > 0;
|
||||
}
|
||||
|
||||
- (void)addNavigationBarItemsToViewController:(UINavigationItem *)navigationItem {
|
||||
if (!self.presentingViewController) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if a done item already exists
|
||||
for (UIBarButtonItem *item in navigationItem.rightBarButtonItems) {
|
||||
if (item.style == UIBarButtonItemStyleDone) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Give root view controllers a Done button if it does not already have one
|
||||
UIBarButtonItem *done = [[UIBarButtonItem alloc]
|
||||
initWithBarButtonSystemItem:UIBarButtonSystemItemDone
|
||||
target:self
|
||||
action:@selector(dismissAnimated)
|
||||
];
|
||||
|
||||
// Prepend the button if other buttons exist already
|
||||
NSArray *existingItems = navigationItem.rightBarButtonItems;
|
||||
if (existingItems.count) {
|
||||
navigationItem.rightBarButtonItems = [@[done] arrayByAddingObjectsFromArray:existingItems];
|
||||
} else {
|
||||
navigationItem.rightBarButtonItem = done;
|
||||
}
|
||||
|
||||
// Keeps us from calling this method again on
|
||||
// the same view controllers in -viewWillAppear:
|
||||
self.didSetupPendingDismissButtons = YES;
|
||||
}
|
||||
|
||||
- (void)addNavigationBarSwipeGesture {
|
||||
UISwipeGestureRecognizer *swipe = [[UISwipeGestureRecognizer alloc]
|
||||
initWithTarget:self action:@selector(handleNavigationBarSwipe:)
|
||||
];
|
||||
swipe.direction = UISwipeGestureRecognizerDirectionDown;
|
||||
swipe.delegate = self;
|
||||
self.navigationBarSwipeGesture = swipe;
|
||||
[self.navigationBar addGestureRecognizer:swipe];
|
||||
}
|
||||
|
||||
- (void)handleNavigationBarSwipe:(UISwipeGestureRecognizer *)sender {
|
||||
if (sender.state == UIGestureRecognizerStateRecognized) {
|
||||
[self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)handleNavigationBarTap:(UIGestureRecognizer *)sender {
|
||||
// Don't reveal the toolbar if we were just tapping a button
|
||||
CGPoint location = [sender locationInView:self.navigationBar];
|
||||
UIView *hitView = [self.navigationBar hitTest:location withEvent:nil];
|
||||
if ([hitView isKindOfClass:[UIControl class]]) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (sender.state == UIGestureRecognizerStateRecognized) {
|
||||
if (self.toolbarHidden && self.canShowToolbar) {
|
||||
[self setToolbarHidden:NO animated:YES];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)g1 shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)g2 {
|
||||
if (g1 == self.navigationBarSwipeGesture && g2 == self.barHideOnSwipeGestureRecognizer) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (void)_gestureRecognizedInteractiveHide:(UIPanGestureRecognizer *)sender {
|
||||
if (sender.state == UIGestureRecognizerStateRecognized) {
|
||||
BOOL show = self.canShowToolbar;
|
||||
CGFloat yTranslation = [sender translationInView:self.view].y;
|
||||
CGFloat yVelocity = [sender velocityInView:self.view].y;
|
||||
if (yVelocity > 2000) {
|
||||
[self setToolbarHidden:YES animated:YES];
|
||||
} else if (show && yTranslation > 20 && yVelocity > 250) {
|
||||
[self setToolbarHidden:NO animated:YES];
|
||||
} else if (yTranslation < -20) {
|
||||
[self setToolbarHidden:YES animated:YES];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,153 @@
|
||||
//
|
||||
// FLEXTableViewController.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 7/5/19.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "FLEXTableView.h"
|
||||
@class FLEXScopeCarousel, FLEXWindow, FLEXTableViewSection;
|
||||
|
||||
typedef CGFloat FLEXDebounceInterval;
|
||||
/// No delay, all events delivered
|
||||
extern CGFloat const kFLEXDebounceInstant;
|
||||
/// Small delay which makes UI seem smoother by avoiding rapid events
|
||||
extern CGFloat const kFLEXDebounceFast;
|
||||
/// Slower than Fast, faster than ExpensiveIO
|
||||
extern CGFloat const kFLEXDebounceForAsyncSearch;
|
||||
/// The least frequent, at just over once per second; for I/O or other expensive operations
|
||||
extern CGFloat const kFLEXDebounceForExpensiveIO;
|
||||
|
||||
@protocol FLEXSearchResultsUpdating <NSObject>
|
||||
/// A method to handle search query update events.
|
||||
///
|
||||
/// \c searchBarDebounceInterval is used to reduce the frequency at which this
|
||||
/// method is called. This method is also called when the search bar becomes
|
||||
/// the first responder, and when the selected search bar scope index changes.
|
||||
- (void)updateSearchResults:(NSString *)newText;
|
||||
@end
|
||||
|
||||
@interface FLEXTableViewController : UITableViewController <
|
||||
UISearchResultsUpdating, UISearchControllerDelegate, UISearchBarDelegate
|
||||
>
|
||||
|
||||
/// A grouped table view. Inset on iOS 13.
|
||||
///
|
||||
/// Simply calls into \c initWithStyle:
|
||||
- (id)init;
|
||||
|
||||
/// Subclasses may override to configure the controller before \c viewDidLoad:
|
||||
- (id)initWithStyle:(UITableViewStyle)style;
|
||||
|
||||
@property (nonatomic) FLEXTableView *tableView;
|
||||
|
||||
/// If your subclass conforms to \c FLEXSearchResultsUpdating
|
||||
/// then this property is assigned to \c self automatically.
|
||||
///
|
||||
/// Setting \c filterDelegate will also set this property to that object.
|
||||
@property (nonatomic, weak) id<FLEXSearchResultsUpdating> searchDelegate;
|
||||
|
||||
/// Defaults to NO.
|
||||
///
|
||||
/// Setting this to YES will initialize the carousel and the view.
|
||||
@property (nonatomic) BOOL showsCarousel;
|
||||
/// A horizontally scrolling list with functionality similar to
|
||||
/// that of a search bar's scope bar. You'd want to use this when
|
||||
/// you have potentially more than 4 scope options.
|
||||
@property (nonatomic) FLEXScopeCarousel *carousel;
|
||||
|
||||
/// Defaults to NO.
|
||||
///
|
||||
/// Setting this to YES will initialize searchController and the view.
|
||||
@property (nonatomic) BOOL showsSearchBar;
|
||||
/// Defaults to NO.
|
||||
///
|
||||
/// Setting this to YES will make the search bar appear whenever the view appears.
|
||||
/// Otherwise, iOS will only show the search bar when you scroll up.
|
||||
@property (nonatomic) BOOL showSearchBarInitially;
|
||||
/// Defaults to NO.
|
||||
///
|
||||
/// Setting this to YES will make the search bar activate whenever the view appears.
|
||||
@property (nonatomic) BOOL activatesSearchBarAutomatically;
|
||||
|
||||
/// nil unless showsSearchBar is set to YES.
|
||||
///
|
||||
/// self is used as the default search results updater and delegate.
|
||||
/// The search bar will not dim the background or hide the navigation bar by default.
|
||||
/// On iOS 11 and up, the search bar will appear in the navigation bar below the title.
|
||||
@property (nonatomic) UISearchController *searchController;
|
||||
/// Used to initialize the search controller. Defaults to nil.
|
||||
@property (nonatomic) UIViewController *searchResultsController;
|
||||
/// Defaults to "Fast"
|
||||
///
|
||||
/// Determines how often search bar results will be "debounced."
|
||||
/// Empty query events are always sent instantly. Query events will
|
||||
/// be sent when the user has not changed the query for this interval.
|
||||
@property (nonatomic) FLEXDebounceInterval searchBarDebounceInterval;
|
||||
/// Whether the search bar stays at the top of the view while scrolling.
|
||||
///
|
||||
/// Calls into self.navigationItem.hidesSearchBarWhenScrolling.
|
||||
/// Do not change self.navigationItem.hidesSearchBarWhenScrolling directly,
|
||||
/// or it will not be respsected. Use this instead.
|
||||
/// Defaults to NO.
|
||||
@property (nonatomic) BOOL pinSearchBar;
|
||||
/// By default, we will show the search bar's cancel button when
|
||||
/// search becomes active and hide it when search is dismissed.
|
||||
///
|
||||
/// Do not set the showsCancelButton property on the searchController's
|
||||
/// searchBar manually. Set this property after turning on showsSearchBar.
|
||||
///
|
||||
/// Does nothing pre-iOS 13, safe to call on any version.
|
||||
@property (nonatomic) BOOL automaticallyShowsSearchBarCancelButton;
|
||||
|
||||
/// If using the scope bar, self.searchController.searchBar.selectedScopeButtonIndex.
|
||||
/// Otherwise, this is the selected index of the carousel, or NSNotFound if using neither.
|
||||
@property (nonatomic) NSInteger selectedScope;
|
||||
/// self.searchController.searchBar.text
|
||||
@property (nonatomic, readonly, copy) NSString *searchText;
|
||||
|
||||
/// A totally optional delegate to forward search results updater calls to.
|
||||
/// If a delegate is set, updateSearchResults: is not called on this view controller.
|
||||
@property (nonatomic, weak) id<FLEXSearchResultsUpdating> searchResultsUpdater;
|
||||
|
||||
/// self.view.window as a \c FLEXWindow
|
||||
@property (nonatomic, readonly) FLEXWindow *window;
|
||||
|
||||
/// Convenient for doing some async processor-intensive searching
|
||||
/// in the background before updating the UI back on the main queue.
|
||||
- (void)onBackgroundQueue:(NSArray *(^)(void))backgroundBlock thenOnMainQueue:(void(^)(NSArray *))mainBlock;
|
||||
|
||||
/// Adds up to 3 additional items to the toolbar in right-to-left order.
|
||||
///
|
||||
/// That is, the first item in the given array will be the rightmost item behind
|
||||
/// any existing toolbar items. By default, buttons for bookmarks and tabs are shown.
|
||||
///
|
||||
/// If you wish to have more control over how the buttons are arranged or which
|
||||
/// buttons are displayed, you can access the properties for the pre-existing
|
||||
/// toolbar items directly and manually set \c self.toolbarItems by overriding
|
||||
/// the \c setupToolbarItems method below.
|
||||
- (void)addToolbarItems:(NSArray<UIBarButtonItem *> *)items;
|
||||
|
||||
/// Subclasses may override. You should not need to call this method directly.
|
||||
- (void)setupToolbarItems;
|
||||
|
||||
@property (nonatomic, readonly) UIBarButtonItem *shareToolbarItem;
|
||||
@property (nonatomic, readonly) UIBarButtonItem *bookmarksToolbarItem;
|
||||
@property (nonatomic, readonly) UIBarButtonItem *openTabsToolbarItem;
|
||||
|
||||
/// Whether or not to display the "share" icon in the middle of the toolbar. NO by default.
|
||||
///
|
||||
/// Turning this on after you have added custom toolbar items will
|
||||
/// push off the leftmost toolbar item and shift the others leftward.
|
||||
@property (nonatomic) BOOL showsShareToolbarItem;
|
||||
/// Called when the share button is pressed.
|
||||
/// Default implementation does nothign. Subclasses may override.
|
||||
- (void)shareButtonPressed:(UIBarButtonItem *)sender;
|
||||
|
||||
/// Subclasses may call this to opt-out of all toolbar related behavior.
|
||||
/// This is necessary if you want to disable the gesture which reveals the toolbar.
|
||||
- (void)disableToolbar;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,618 @@
|
||||
//
|
||||
// FLEXTableViewController.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 7/5/19.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXTableViewController.h"
|
||||
#import "FLEXExplorerViewController.h"
|
||||
#import "FLEXBookmarksViewController.h"
|
||||
#import "FLEXTabsViewController.h"
|
||||
#import "FLEXScopeCarousel.h"
|
||||
#import "FLEXTableView.h"
|
||||
#import "FLEXUtility.h"
|
||||
#import "FLEXResources.h"
|
||||
#import "UIBarButtonItem+FLEX.h"
|
||||
#import <objc/runtime.h>
|
||||
|
||||
@interface Block : NSObject
|
||||
- (void)invoke;
|
||||
@end
|
||||
|
||||
CGFloat const kFLEXDebounceInstant = 0.f;
|
||||
CGFloat const kFLEXDebounceFast = 0.05;
|
||||
CGFloat const kFLEXDebounceForAsyncSearch = 0.15;
|
||||
CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
|
||||
|
||||
@interface FLEXTableViewController ()
|
||||
@property (nonatomic) NSTimer *debounceTimer;
|
||||
@property (nonatomic) BOOL didInitiallyRevealSearchBar;
|
||||
@property (nonatomic) UITableViewStyle style;
|
||||
|
||||
@property (nonatomic) BOOL hasAppeared;
|
||||
@property (nonatomic, readonly) UIView *tableHeaderViewContainer;
|
||||
|
||||
@property (nonatomic, readonly) BOOL manuallyDeactivateSearchOnDisappear;
|
||||
|
||||
@property (nonatomic) UIBarButtonItem *middleToolbarItem;
|
||||
@property (nonatomic) UIBarButtonItem *middleLeftToolbarItem;
|
||||
@property (nonatomic) UIBarButtonItem *leftmostToolbarItem;
|
||||
@end
|
||||
|
||||
@implementation FLEXTableViewController
|
||||
@dynamic tableView;
|
||||
@synthesize showsShareToolbarItem = _showsShareToolbarItem;
|
||||
@synthesize tableHeaderViewContainer = _tableHeaderViewContainer;
|
||||
@synthesize automaticallyShowsSearchBarCancelButton = _automaticallyShowsSearchBarCancelButton;
|
||||
|
||||
#pragma mark - Initialization
|
||||
|
||||
- (id)init {
|
||||
if (@available(iOS 13.0, *)) {
|
||||
self = [self initWithStyle:UITableViewStyleInsetGrouped];
|
||||
} else {
|
||||
self = [self initWithStyle:UITableViewStyleGrouped];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (id)initWithStyle:(UITableViewStyle)style {
|
||||
self = [super initWithStyle:style];
|
||||
|
||||
if (self) {
|
||||
_searchBarDebounceInterval = kFLEXDebounceFast;
|
||||
_showSearchBarInitially = YES;
|
||||
_style = style;
|
||||
_manuallyDeactivateSearchOnDisappear = (
|
||||
NSProcessInfo.processInfo.operatingSystemVersion.majorVersion < 11
|
||||
);
|
||||
|
||||
// We will be our own search delegate if we implement this method
|
||||
if ([self respondsToSelector:@selector(updateSearchResults:)]) {
|
||||
self.searchDelegate = (id)self;
|
||||
}
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Public
|
||||
|
||||
- (FLEXWindow *)window {
|
||||
return (id)self.view.window;
|
||||
}
|
||||
|
||||
- (void)setShowsSearchBar:(BOOL)showsSearchBar {
|
||||
if (_showsSearchBar == showsSearchBar) return;
|
||||
_showsSearchBar = showsSearchBar;
|
||||
|
||||
if (showsSearchBar) {
|
||||
UIViewController *results = self.searchResultsController;
|
||||
self.searchController = [[UISearchController alloc] initWithSearchResultsController:results];
|
||||
self.searchController.searchBar.placeholder = @"Filter";
|
||||
self.searchController.searchResultsUpdater = (id)self;
|
||||
self.searchController.delegate = (id)self;
|
||||
self.searchController.dimsBackgroundDuringPresentation = NO;
|
||||
self.searchController.hidesNavigationBarDuringPresentation = NO;
|
||||
/// Not necessary in iOS 13; remove this when iOS 13 is the minimum deployment target
|
||||
self.searchController.searchBar.delegate = self;
|
||||
|
||||
self.automaticallyShowsSearchBarCancelButton = YES;
|
||||
|
||||
if (@available(iOS 13, *)) {
|
||||
self.searchController.automaticallyShowsScopeBar = NO;
|
||||
}
|
||||
|
||||
[self addSearchController:self.searchController];
|
||||
} else {
|
||||
// Search already shown and just set to NO, so remove it
|
||||
[self removeSearchController:self.searchController];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setShowsCarousel:(BOOL)showsCarousel {
|
||||
if (_showsCarousel == showsCarousel) return;
|
||||
_showsCarousel = showsCarousel;
|
||||
|
||||
if (showsCarousel) {
|
||||
_carousel = ({ weakify(self)
|
||||
|
||||
FLEXScopeCarousel *carousel = [FLEXScopeCarousel new];
|
||||
carousel.selectedIndexChangedAction = ^(NSInteger idx) { strongify(self);
|
||||
[self.searchDelegate updateSearchResults:self.searchText];
|
||||
};
|
||||
|
||||
// UITableView won't update the header size unless you reset the header view
|
||||
[carousel registerBlockForDynamicTypeChanges:^(FLEXScopeCarousel *_) { strongify(self);
|
||||
[self layoutTableHeaderIfNeeded];
|
||||
}];
|
||||
|
||||
carousel;
|
||||
});
|
||||
[self addCarousel:_carousel];
|
||||
} else {
|
||||
// Carousel already shown and just set to NO, so remove it
|
||||
[self removeCarousel:_carousel];
|
||||
}
|
||||
}
|
||||
|
||||
- (NSInteger)selectedScope {
|
||||
if (self.searchController.searchBar.showsScopeBar) {
|
||||
return self.searchController.searchBar.selectedScopeButtonIndex;
|
||||
} else if (self.showsCarousel) {
|
||||
return self.carousel.selectedIndex;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setSelectedScope:(NSInteger)selectedScope {
|
||||
if (self.searchController.searchBar.showsScopeBar) {
|
||||
self.searchController.searchBar.selectedScopeButtonIndex = selectedScope;
|
||||
} else if (self.showsCarousel) {
|
||||
self.carousel.selectedIndex = selectedScope;
|
||||
}
|
||||
|
||||
[self.searchDelegate updateSearchResults:self.searchText];
|
||||
}
|
||||
|
||||
- (NSString *)searchText {
|
||||
return self.searchController.searchBar.text;
|
||||
}
|
||||
|
||||
- (BOOL)automaticallyShowsSearchBarCancelButton {
|
||||
if (@available(iOS 13, *)) {
|
||||
return self.searchController.automaticallyShowsCancelButton;
|
||||
}
|
||||
|
||||
return _automaticallyShowsSearchBarCancelButton;
|
||||
}
|
||||
|
||||
- (void)setAutomaticallyShowsSearchBarCancelButton:(BOOL)value {
|
||||
if (@available(iOS 13, *)) {
|
||||
self.searchController.automaticallyShowsCancelButton = value;
|
||||
}
|
||||
|
||||
_automaticallyShowsSearchBarCancelButton = value;
|
||||
}
|
||||
|
||||
- (void)onBackgroundQueue:(NSArray *(^)(void))backgroundBlock thenOnMainQueue:(void(^)(NSArray *))mainBlock {
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
NSArray *items = backgroundBlock();
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
mainBlock(items);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
- (void)setsShowsShareToolbarItem:(BOOL)showsShareToolbarItem {
|
||||
_showsShareToolbarItem = showsShareToolbarItem;
|
||||
if (self.isViewLoaded) {
|
||||
[self setupToolbarItems];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)disableToolbar {
|
||||
self.navigationController.toolbarHidden = YES;
|
||||
self.navigationController.hidesBarsOnSwipe = NO;
|
||||
self.toolbarItems = nil;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - View Controller Lifecycle
|
||||
|
||||
- (void)loadView {
|
||||
self.view = [FLEXTableView style:self.style];
|
||||
self.tableView.dataSource = self;
|
||||
self.tableView.delegate = self;
|
||||
|
||||
_shareToolbarItem = FLEXBarButtonItemSystem(Action, self, @selector(shareButtonPressed:));
|
||||
_bookmarksToolbarItem = [UIBarButtonItem
|
||||
flex_itemWithImage:FLEXResources.bookmarksIcon target:self action:@selector(showBookmarks)
|
||||
];
|
||||
_openTabsToolbarItem = [UIBarButtonItem
|
||||
flex_itemWithImage:FLEXResources.openTabsIcon target:self action:@selector(showTabSwitcher)
|
||||
];
|
||||
|
||||
self.leftmostToolbarItem = UIBarButtonItem.flex_fixedSpace;
|
||||
self.middleLeftToolbarItem = UIBarButtonItem.flex_fixedSpace;
|
||||
self.middleToolbarItem = UIBarButtonItem.flex_fixedSpace;
|
||||
}
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
|
||||
self.tableView.keyboardDismissMode = UIScrollViewKeyboardDismissModeOnDrag;
|
||||
|
||||
// Toolbar
|
||||
self.navigationController.toolbarHidden = self.toolbarItems.count > 0;
|
||||
self.navigationController.hidesBarsOnSwipe = YES;
|
||||
|
||||
// On iOS 13, the root view controller shows it's search bar no matter what.
|
||||
// Turning this off avoids some weird flash the navigation bar does when we
|
||||
// toggle navigationItem.hidesSearchBarWhenScrolling on and off. The flash
|
||||
// will still happen on subsequent view controllers, but we can at least
|
||||
// avoid it for the root view controller
|
||||
if (@available(iOS 13, *)) {
|
||||
if (self.navigationController.viewControllers.firstObject == self) {
|
||||
_showSearchBarInitially = NO;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)viewWillAppear:(BOOL)animated {
|
||||
[super viewWillAppear:animated];
|
||||
|
||||
if (@available(iOS 11.0, *)) {
|
||||
// When going back, make the search bar reappear instead of hiding
|
||||
if ((self.pinSearchBar || self.showSearchBarInitially) && !self.didInitiallyRevealSearchBar) {
|
||||
self.navigationItem.hidesSearchBarWhenScrolling = NO;
|
||||
}
|
||||
}
|
||||
|
||||
// Make the keyboard seem to appear faster
|
||||
if (self.activatesSearchBarAutomatically) {
|
||||
[self makeKeyboardAppearNow];
|
||||
}
|
||||
|
||||
[self setupToolbarItems];
|
||||
}
|
||||
|
||||
- (void)viewDidAppear:(BOOL)animated {
|
||||
[super viewDidAppear:animated];
|
||||
|
||||
// Allow scrolling to collapse the search bar, only if we don't want it pinned
|
||||
if (@available(iOS 11.0, *)) {
|
||||
if (self.showSearchBarInitially && !self.pinSearchBar && !self.didInitiallyRevealSearchBar) {
|
||||
// All this mumbo jumbo is necessary to work around a bug in iOS 13 up to 13.2
|
||||
// wherein quickly toggling navigationItem.hidesSearchBarWhenScrolling to make
|
||||
// the search bar appear initially results in a bugged search bar that
|
||||
// becomes transparent and floats over the screen as you scroll
|
||||
[UIView animateWithDuration:0.2 animations:^{
|
||||
self.navigationItem.hidesSearchBarWhenScrolling = YES;
|
||||
[self.navigationController.view setNeedsLayout];
|
||||
[self.navigationController.view layoutIfNeeded];
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
if (self.activatesSearchBarAutomatically) {
|
||||
// Keyboard has appeared, now we call this as we soon present our search bar
|
||||
[self removeDummyTextField];
|
||||
|
||||
// Activate the search bar
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
// This doesn't work unless it's wrapped in this dispatch_async call
|
||||
[self.searchController.searchBar becomeFirstResponder];
|
||||
});
|
||||
}
|
||||
|
||||
// We only want to reveal the search bar when the view controller first appears.
|
||||
self.didInitiallyRevealSearchBar = YES;
|
||||
}
|
||||
|
||||
- (void)viewWillDisappear:(BOOL)animated {
|
||||
[super viewWillDisappear:animated];
|
||||
|
||||
if (self.manuallyDeactivateSearchOnDisappear && self.searchController.isActive) {
|
||||
self.searchController.active = NO;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)didMoveToParentViewController:(UIViewController *)parent {
|
||||
[super didMoveToParentViewController:parent];
|
||||
// Reset this since we are re-appearing under a new
|
||||
// parent view controller and need to show it again
|
||||
self.didInitiallyRevealSearchBar = NO;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Toolbar, Public
|
||||
|
||||
- (void)setupToolbarItems {
|
||||
if (!self.isViewLoaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.toolbarItems = @[
|
||||
self.leftmostToolbarItem,
|
||||
UIBarButtonItem.flex_flexibleSpace,
|
||||
self.middleLeftToolbarItem,
|
||||
UIBarButtonItem.flex_flexibleSpace,
|
||||
self.middleToolbarItem,
|
||||
UIBarButtonItem.flex_flexibleSpace,
|
||||
self.bookmarksToolbarItem,
|
||||
UIBarButtonItem.flex_flexibleSpace,
|
||||
self.openTabsToolbarItem,
|
||||
];
|
||||
|
||||
for (UIBarButtonItem *item in self.toolbarItems) {
|
||||
[item _setWidth:60];
|
||||
// This does not work for anything but fixed spaces for some reason
|
||||
// item.width = 60;
|
||||
}
|
||||
|
||||
// Disable tabs entirely when not presented by FLEXExplorerViewController
|
||||
UIViewController *presenter = self.navigationController.presentingViewController;
|
||||
if (![presenter isKindOfClass:[FLEXExplorerViewController class]]) {
|
||||
self.openTabsToolbarItem.enabled = NO;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)addToolbarItems:(NSArray<UIBarButtonItem *> *)items {
|
||||
if (self.showsShareToolbarItem) {
|
||||
// Share button is in the middle, skip middle button
|
||||
if (items.count > 0) {
|
||||
self.middleLeftToolbarItem = items[0];
|
||||
}
|
||||
if (items.count > 1) {
|
||||
self.leftmostToolbarItem = items[1];
|
||||
}
|
||||
} else {
|
||||
// Add buttons right-to-left
|
||||
if (items.count > 0) {
|
||||
self.middleToolbarItem = items[0];
|
||||
}
|
||||
if (items.count > 1) {
|
||||
self.middleLeftToolbarItem = items[1];
|
||||
}
|
||||
if (items.count > 2) {
|
||||
self.leftmostToolbarItem = items[2];
|
||||
}
|
||||
}
|
||||
|
||||
[self setupToolbarItems];
|
||||
}
|
||||
|
||||
- (void)setShowsShareToolbarItem:(BOOL)showShare {
|
||||
if (_showsShareToolbarItem != showShare) {
|
||||
_showsShareToolbarItem = showShare;
|
||||
|
||||
if (showShare) {
|
||||
// Push out leftmost item
|
||||
self.leftmostToolbarItem = self.middleLeftToolbarItem;
|
||||
self.middleLeftToolbarItem = self.middleToolbarItem;
|
||||
|
||||
// Use share for middle
|
||||
self.middleToolbarItem = self.shareToolbarItem;
|
||||
} else {
|
||||
// Remove share, shift custom items rightward
|
||||
self.middleToolbarItem = self.middleLeftToolbarItem;
|
||||
self.middleLeftToolbarItem = self.leftmostToolbarItem;
|
||||
self.leftmostToolbarItem = UIBarButtonItem.flex_fixedSpace;
|
||||
}
|
||||
}
|
||||
|
||||
[self setupToolbarItems];
|
||||
}
|
||||
|
||||
- (void)shareButtonPressed:(UIBarButtonItem *)sender {
|
||||
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Private
|
||||
|
||||
- (void)debounce:(void(^)(void))block {
|
||||
[self.debounceTimer invalidate];
|
||||
|
||||
self.debounceTimer = [NSTimer
|
||||
scheduledTimerWithTimeInterval:self.searchBarDebounceInterval
|
||||
target:block
|
||||
selector:@selector(invoke)
|
||||
userInfo:nil
|
||||
repeats:NO
|
||||
];
|
||||
}
|
||||
|
||||
- (void)layoutTableHeaderIfNeeded {
|
||||
if (self.showsCarousel) {
|
||||
self.carousel.frame = FLEXRectSetHeight(
|
||||
self.carousel.frame, self.carousel.intrinsicContentSize.height
|
||||
);
|
||||
}
|
||||
|
||||
self.tableView.tableHeaderView = self.tableView.tableHeaderView;
|
||||
}
|
||||
|
||||
- (void)addCarousel:(FLEXScopeCarousel *)carousel {
|
||||
if (@available(iOS 11.0, *)) {
|
||||
self.tableView.tableHeaderView = carousel;
|
||||
} else {
|
||||
carousel.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin;
|
||||
|
||||
CGRect frame = self.tableHeaderViewContainer.frame;
|
||||
CGRect subviewFrame = carousel.frame;
|
||||
subviewFrame.origin.y = 0;
|
||||
|
||||
// Put the carousel below the search bar if it's already there
|
||||
if (self.showsSearchBar) {
|
||||
carousel.frame = subviewFrame = FLEXRectSetY(
|
||||
subviewFrame, self.searchController.searchBar.frame.size.height
|
||||
);
|
||||
frame.size.height += carousel.intrinsicContentSize.height;
|
||||
} else {
|
||||
frame.size.height = carousel.intrinsicContentSize.height;
|
||||
}
|
||||
|
||||
self.tableHeaderViewContainer.frame = frame;
|
||||
[self.tableHeaderViewContainer addSubview:carousel];
|
||||
}
|
||||
|
||||
[self layoutTableHeaderIfNeeded];
|
||||
}
|
||||
|
||||
- (void)removeCarousel:(FLEXScopeCarousel *)carousel {
|
||||
[carousel removeFromSuperview];
|
||||
|
||||
if (@available(iOS 11.0, *)) {
|
||||
self.tableView.tableHeaderView = nil;
|
||||
} else {
|
||||
if (self.showsSearchBar) {
|
||||
[self removeSearchController:self.searchController];
|
||||
[self addSearchController:self.searchController];
|
||||
} else {
|
||||
self.tableView.tableHeaderView = nil;
|
||||
_tableHeaderViewContainer = nil;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)addSearchController:(UISearchController *)controller {
|
||||
if (@available(iOS 11.0, *)) {
|
||||
self.navigationItem.searchController = controller;
|
||||
} else {
|
||||
controller.searchBar.autoresizingMask |= UIViewAutoresizingFlexibleBottomMargin;
|
||||
[self.tableHeaderViewContainer addSubview:controller.searchBar];
|
||||
CGRect subviewFrame = controller.searchBar.frame;
|
||||
CGRect frame = self.tableHeaderViewContainer.frame;
|
||||
frame.size.width = MAX(frame.size.width, subviewFrame.size.width);
|
||||
frame.size.height = subviewFrame.size.height;
|
||||
|
||||
// Move the carousel down if it's already there
|
||||
if (self.showsCarousel) {
|
||||
self.carousel.frame = FLEXRectSetY(
|
||||
self.carousel.frame, subviewFrame.size.height
|
||||
);
|
||||
frame.size.height += self.carousel.frame.size.height;
|
||||
}
|
||||
|
||||
self.tableHeaderViewContainer.frame = frame;
|
||||
[self layoutTableHeaderIfNeeded];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)removeSearchController:(UISearchController *)controller {
|
||||
if (@available(iOS 11.0, *)) {
|
||||
self.navigationItem.searchController = nil;
|
||||
} else {
|
||||
[controller.searchBar removeFromSuperview];
|
||||
|
||||
if (self.showsCarousel) {
|
||||
// self.carousel.frame = FLEXRectRemake(CGPointZero, self.carousel.frame.size);
|
||||
[self removeCarousel:self.carousel];
|
||||
[self addCarousel:self.carousel];
|
||||
} else {
|
||||
self.tableView.tableHeaderView = nil;
|
||||
_tableHeaderViewContainer = nil;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (UIView *)tableHeaderViewContainer {
|
||||
if (!_tableHeaderViewContainer) {
|
||||
_tableHeaderViewContainer = [UIView new];
|
||||
self.tableView.tableHeaderView = self.tableHeaderViewContainer;
|
||||
}
|
||||
|
||||
return _tableHeaderViewContainer;
|
||||
}
|
||||
|
||||
- (void)showBookmarks {
|
||||
UINavigationController *nav = [[UINavigationController alloc]
|
||||
initWithRootViewController:[FLEXBookmarksViewController new]
|
||||
];
|
||||
[self presentViewController:nav animated:YES completion:nil];
|
||||
}
|
||||
|
||||
- (void)showTabSwitcher {
|
||||
UINavigationController *nav = [[UINavigationController alloc]
|
||||
initWithRootViewController:[FLEXTabsViewController new]
|
||||
];
|
||||
[self presentViewController:nav animated:YES completion:nil];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Search Bar
|
||||
|
||||
#pragma mark Faster keyboard
|
||||
|
||||
static UITextField *kDummyTextField = nil;
|
||||
|
||||
/// Make the keyboard appear instantly. We use this to make the
|
||||
/// keyboard appear faster when the search bar is set to appear initially.
|
||||
/// You must call \c -removeDummyTextField before your search bar is to appear.
|
||||
- (void)makeKeyboardAppearNow {
|
||||
if (!kDummyTextField) {
|
||||
kDummyTextField = [UITextField new];
|
||||
kDummyTextField.autocorrectionType = UITextAutocorrectionTypeNo;
|
||||
}
|
||||
|
||||
kDummyTextField.inputAccessoryView = self.searchController.searchBar.inputAccessoryView;
|
||||
[UIApplication.sharedApplication.keyWindow addSubview:kDummyTextField];
|
||||
[kDummyTextField becomeFirstResponder];
|
||||
}
|
||||
|
||||
- (void)removeDummyTextField {
|
||||
if (kDummyTextField.superview) {
|
||||
[kDummyTextField removeFromSuperview];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark UISearchResultsUpdating
|
||||
|
||||
- (void)updateSearchResultsForSearchController:(UISearchController *)searchController {
|
||||
[self.debounceTimer invalidate];
|
||||
NSString *text = searchController.searchBar.text;
|
||||
|
||||
void (^updateSearchResults)(void) = ^{
|
||||
if (self.searchResultsUpdater) {
|
||||
[self.searchResultsUpdater updateSearchResults:text];
|
||||
} else {
|
||||
[self.searchDelegate updateSearchResults:text];
|
||||
}
|
||||
};
|
||||
|
||||
// Only debounce if we want to, and if we have a non-empty string
|
||||
// Empty string events are sent instantly
|
||||
if (text.length && self.searchBarDebounceInterval > kFLEXDebounceInstant) {
|
||||
[self debounce:updateSearchResults];
|
||||
} else {
|
||||
updateSearchResults();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark UISearchControllerDelegate
|
||||
|
||||
- (void)willPresentSearchController:(UISearchController *)searchController {
|
||||
// Manually show cancel button for < iOS 13
|
||||
if (!@available(iOS 13, *) && self.automaticallyShowsSearchBarCancelButton) {
|
||||
[searchController.searchBar setShowsCancelButton:YES animated:YES];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)willDismissSearchController:(UISearchController *)searchController {
|
||||
// Manually hide cancel button for < iOS 13
|
||||
if (!@available(iOS 13, *) && self.automaticallyShowsSearchBarCancelButton) {
|
||||
[searchController.searchBar setShowsCancelButton:NO animated:YES];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark UISearchBarDelegate
|
||||
|
||||
/// Not necessary in iOS 13; remove this when iOS 13 is the deployment target
|
||||
- (void)searchBar:(UISearchBar *)searchBar selectedScopeButtonIndexDidChange:(NSInteger)selectedScope {
|
||||
[self updateSearchResultsForSearchController:self.searchController];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark Table View
|
||||
|
||||
/// Not having a title in the first section looks weird with a rounded-corner table view style
|
||||
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
|
||||
if (@available(iOS 13, *)) {
|
||||
if (self.style == UITableViewStyleInsetGrouped) {
|
||||
return @" ";
|
||||
}
|
||||
}
|
||||
|
||||
return nil; // For plain/gropued style
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,28 @@
|
||||
//
|
||||
// FLEXSingleRowSection.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner Bennett on 9/25/19.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXTableViewSection.h"
|
||||
|
||||
/// A section providing a specific single row.
|
||||
///
|
||||
/// You may optionally provide a view controller to push when the row
|
||||
/// is selected, or an action to perform when it is selected.
|
||||
/// Which one is used first is up to the table view data source.
|
||||
@interface FLEXSingleRowSection : FLEXTableViewSection
|
||||
|
||||
/// @param reuseIdentifier if nil, kFLEXDefaultCell is used.
|
||||
+ (instancetype)title:(NSString *)sectionTitle
|
||||
reuse:(NSString *)reuseIdentifier
|
||||
cell:(void(^)(__kindof UITableViewCell *cell))cellConfiguration;
|
||||
|
||||
@property (nonatomic) UIViewController *pushOnSelection;
|
||||
@property (nonatomic) void (^selectionAction)(UIViewController *host);
|
||||
/// Called to determine whether the single row should display itself or not.
|
||||
@property (nonatomic) BOOL (^filterMatcher)(NSString *filterText);
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,87 @@
|
||||
//
|
||||
// FLEXSingleRowSection.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner Bennett on 9/25/19.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXSingleRowSection.h"
|
||||
#import "FLEXTableView.h"
|
||||
|
||||
@interface FLEXSingleRowSection ()
|
||||
@property (nonatomic, readonly) NSString *reuseIdentifier;
|
||||
@property (nonatomic, readonly) void (^cellConfiguration)(__kindof UITableViewCell *cell);
|
||||
|
||||
@property (nonatomic) NSString *lastTitle;
|
||||
@property (nonatomic) NSString *lastSubitle;
|
||||
@end
|
||||
|
||||
@implementation FLEXSingleRowSection
|
||||
|
||||
#pragma mark - Public
|
||||
|
||||
+ (instancetype)title:(NSString *)title
|
||||
reuse:(NSString *)reuse
|
||||
cell:(void (^)(__kindof UITableViewCell *))config {
|
||||
return [[self alloc] initWithTitle:title reuse:reuse cell:config];
|
||||
}
|
||||
|
||||
- (id)initWithTitle:(NSString *)sectionTitle
|
||||
reuse:(NSString *)reuseIdentifier
|
||||
cell:(void (^)(__kindof UITableViewCell *))cellConfiguration {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_title = sectionTitle;
|
||||
_reuseIdentifier = reuseIdentifier ?: kFLEXDefaultCell;
|
||||
_cellConfiguration = cellConfiguration;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - Overrides
|
||||
|
||||
- (NSInteger)numberOfRows {
|
||||
if (self.filterMatcher && self.filterText.length) {
|
||||
return self.filterMatcher(self.filterText) ? 1 : 0;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
- (BOOL)canSelectRow:(NSInteger)row {
|
||||
return self.pushOnSelection || self.selectionAction;
|
||||
}
|
||||
|
||||
- (void (^)(__kindof UIViewController *))didSelectRowAction:(NSInteger)row {
|
||||
return self.selectionAction;
|
||||
}
|
||||
|
||||
- (UIViewController *)viewControllerToPushForRow:(NSInteger)row {
|
||||
return self.pushOnSelection;
|
||||
}
|
||||
|
||||
- (NSString *)reuseIdentifierForRow:(NSInteger)row {
|
||||
return self.reuseIdentifier;
|
||||
}
|
||||
|
||||
- (void)configureCell:(__kindof UITableViewCell *)cell forRow:(NSInteger)row {
|
||||
cell.textLabel.text = nil;
|
||||
cell.detailTextLabel.text = nil;
|
||||
cell.accessoryType = UITableViewCellAccessoryNone;
|
||||
|
||||
self.cellConfiguration(cell);
|
||||
self.lastTitle = cell.textLabel.text;
|
||||
self.lastSubitle = cell.detailTextLabel.text;
|
||||
}
|
||||
|
||||
- (NSString *)titleForRow:(NSInteger)row {
|
||||
return self.lastTitle;
|
||||
}
|
||||
|
||||
- (NSString *)subtitleForRow:(NSInteger)row {
|
||||
return self.lastSubitle;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,146 @@
|
||||
//
|
||||
// FLEXTableViewSection.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 1/29/20.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "NSArray+FLEX.h"
|
||||
@class FLEXTableView;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
#pragma mark FLEXTableViewSection
|
||||
|
||||
/// An abstract base class for table view sections.
|
||||
///
|
||||
/// Many properties or methods here return nil or some logical equivalent by default.
|
||||
/// Even so, most of the methods with defaults are intended to be overriden by subclasses.
|
||||
/// Some methods are not implemented at all and MUST be implemented by a subclass.
|
||||
@interface FLEXTableViewSection : NSObject {
|
||||
@protected
|
||||
/// Unused by default, use if you want
|
||||
NSString *_title;
|
||||
|
||||
@private
|
||||
__weak UITableView *_tableView;
|
||||
NSInteger _sectionIndex;
|
||||
}
|
||||
|
||||
#pragma mark - Data
|
||||
|
||||
/// A title to be displayed for the custom section.
|
||||
/// Subclasses may override or use the \c _title ivar.
|
||||
@property (nonatomic, readonly, nullable, copy) NSString *title;
|
||||
/// The number of rows in this section. Subclasses must override.
|
||||
/// This should not change until \c filterText is changed or \c reloadData is called.
|
||||
@property (nonatomic, readonly) NSInteger numberOfRows;
|
||||
/// A map of reuse identifiers to \c UITableViewCell (sub)class objects.
|
||||
/// Subclasses \e may override this as necessary, but are not required to.
|
||||
/// See \c FLEXTableView.h for more information.
|
||||
/// @return nil by default.
|
||||
@property (nonatomic, readonly, nullable) NSDictionary<NSString *, Class> *cellRegistrationMapping;
|
||||
|
||||
/// The section should filter itself based on the contents of this property
|
||||
/// as it is set. If it is set to nil or an empty string, it should not filter.
|
||||
/// Subclasses should override or observe this property and react to changes.
|
||||
///
|
||||
/// It is common practice to use two arrays for the underlying model:
|
||||
/// One to hold all rows, and one to hold unfiltered rows. When \c setFilterText:
|
||||
/// is called, call \c super to store the new value, and re-filter your model accordingly.
|
||||
@property (nonatomic, nullable) NSString *filterText;
|
||||
|
||||
/// Provides an avenue for the section to refresh data or change the number of rows.
|
||||
///
|
||||
/// This is called before reloading the table view itself. If your section pulls data
|
||||
/// from an external data source, this is a good place to refresh that data entirely.
|
||||
/// If your section does not, then it might be simpler for you to just override
|
||||
/// \c setFilterText: to call \c super and call \c reloadData.
|
||||
- (void)reloadData;
|
||||
|
||||
/// Like \c reloadData, but optionally reloads the table view section
|
||||
/// associated with this section object, if any. Do not override.
|
||||
/// Do not call outside of the main thread.
|
||||
- (void)reloadData:(BOOL)updateTable;
|
||||
|
||||
/// Provide a table view and section index to allow the section to efficiently reload
|
||||
/// its own section of the table when something changes it. The table reference is
|
||||
/// held weakly, and subclasses cannot access it or the index. Call this method again
|
||||
/// if the section numbers have changed since you last called it.
|
||||
- (void)setTable:(UITableView *)tableView section:(NSInteger)index;
|
||||
|
||||
#pragma mark - Row Selection
|
||||
|
||||
/// Whether the given row should be selectable, such as if tapping the cell
|
||||
/// should take the user to a new screen or trigger an action.
|
||||
/// Subclasses \e may override this as necessary, but are not required to.
|
||||
/// @return \c NO by default
|
||||
- (BOOL)canSelectRow:(NSInteger)row;
|
||||
|
||||
/// An action "future" to be triggered when the row is selected, if the row
|
||||
/// supports being selected as indicated by \c canSelectRow:. Subclasses
|
||||
/// must implement this in accordance with how they implement \c canSelectRow:
|
||||
/// if they do not implement \c viewControllerToPushForRow:
|
||||
/// @return This returns \c nil if no view controller is provided by
|
||||
/// \c viewControllerToPushForRow: — otherwise it pushes that view controller
|
||||
/// onto \c host.navigationController
|
||||
- (nullable void(^)(__kindof UIViewController *host))didSelectRowAction:(NSInteger)row;
|
||||
|
||||
/// A view controller to display when the row is selected, if the row
|
||||
/// supports being selected as indicated by \c canSelectRow:. Subclasses
|
||||
/// must implement this in accordance with how they implement \c canSelectRow:
|
||||
/// if they do not implement \c didSelectRowAction:
|
||||
/// @return \c nil by default
|
||||
- (nullable UIViewController *)viewControllerToPushForRow:(NSInteger)row;
|
||||
|
||||
/// Called when the accessory view's detail button is pressed.
|
||||
/// @return \c nil by default.
|
||||
- (nullable void(^)(__kindof UIViewController *host))didPressInfoButtonAction:(NSInteger)row;
|
||||
|
||||
#pragma mark - Context Menus
|
||||
|
||||
/// By default, this is the title of the row.
|
||||
/// @return The title of the context menu, if any.
|
||||
- (nullable NSString *)menuTitleForRow:(NSInteger)row API_AVAILABLE(ios(13.0));
|
||||
/// Protected, not intended for public use. \c menuTitleForRow:
|
||||
/// already includes the value returned from this method.
|
||||
///
|
||||
/// By default, this returns \c @"". Subclasses may override to
|
||||
/// provide a detailed description of the target of the context menu.
|
||||
- (NSString *)menuSubtitleForRow:(NSInteger)row API_AVAILABLE(ios(13.0));
|
||||
/// The context menu items, if any. Subclasses may override.
|
||||
/// By default, only inludes items for \c copyMenuItemsForRow:.
|
||||
- (nullable NSArray<UIMenuElement *> *)menuItemsForRow:(NSInteger)row sender:(UIViewController *)sender API_AVAILABLE(ios(13.0));
|
||||
/// Subclasses may override to return a list of copiable items.
|
||||
///
|
||||
/// Every two elements in the list compose a key-value pair, where the key
|
||||
/// should be a description of what will be copied, and the values should be
|
||||
/// the strings to copy. Return an empty string as a value to show a disabled action.
|
||||
- (nullable NSArray<NSString *> *)copyMenuItemsForRow:(NSInteger)row API_AVAILABLE(ios(13.0));
|
||||
|
||||
#pragma mark - Cell Configuration
|
||||
|
||||
/// Provide a reuse identifier for the given row. Subclasses should override.
|
||||
///
|
||||
/// Custom reuse identifiers should be specified in \c cellRegistrationMapping.
|
||||
/// You may return any of the identifiers in \c FLEXTableView.h
|
||||
/// without including them in the \c cellRegistrationMapping.
|
||||
/// @return \c kFLEXDefaultCell by default.
|
||||
- (NSString *)reuseIdentifierForRow:(NSInteger)row;
|
||||
/// Configure a cell for the given row. Subclasses must override.
|
||||
- (void)configureCell:(__kindof UITableViewCell *)cell forRow:(NSInteger)row;
|
||||
|
||||
#pragma mark - External Convenience
|
||||
|
||||
/// For use by whatever view controller uses your section. Not required.
|
||||
/// @return An optional title.
|
||||
- (nullable NSString *)titleForRow:(NSInteger)row;
|
||||
/// For use by whatever view controller uses your section. Not required.
|
||||
/// @return An optional subtitle.
|
||||
- (nullable NSString *)subtitleForRow:(NSInteger)row;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,137 @@
|
||||
//
|
||||
// FLEXTableViewSection.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 1/29/20.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXTableViewSection.h"
|
||||
#import "FLEXTableView.h"
|
||||
#import "FLEXUtility.h"
|
||||
#import "UIMenu+FLEX.h"
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wincomplete-implementation"
|
||||
|
||||
@implementation FLEXTableViewSection
|
||||
|
||||
- (NSInteger)numberOfRows {
|
||||
return 0;
|
||||
}
|
||||
|
||||
- (void)reloadData { }
|
||||
|
||||
- (void)reloadData:(BOOL)updateTable {
|
||||
[self reloadData];
|
||||
if (updateTable) {
|
||||
NSIndexSet *index = [NSIndexSet indexSetWithIndex:_sectionIndex];
|
||||
[_tableView reloadSections:index withRowAnimation:UITableViewRowAnimationNone];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setTable:(UITableView *)tableView section:(NSInteger)index {
|
||||
_tableView = tableView;
|
||||
_sectionIndex = index;
|
||||
}
|
||||
|
||||
- (NSDictionary<NSString *,Class> *)cellRegistrationMapping {
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (BOOL)canSelectRow:(NSInteger)row { return NO; }
|
||||
|
||||
- (void (^)(__kindof UIViewController *))didSelectRowAction:(NSInteger)row {
|
||||
UIViewController *toPush = [self viewControllerToPushForRow:row];
|
||||
if (toPush) {
|
||||
return ^(UIViewController *host) {
|
||||
[host.navigationController pushViewController:toPush animated:YES];
|
||||
};
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (UIViewController *)viewControllerToPushForRow:(NSInteger)row {
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (void (^)(__kindof UIViewController *))didPressInfoButtonAction:(NSInteger)row {
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (NSString *)reuseIdentifierForRow:(NSInteger)row {
|
||||
return kFLEXDefaultCell;
|
||||
}
|
||||
|
||||
- (NSString *)menuTitleForRow:(NSInteger)row {
|
||||
NSString *title = [self titleForRow:row];
|
||||
NSString *subtitle = [self menuSubtitleForRow:row];
|
||||
|
||||
if (subtitle.length) {
|
||||
return [NSString stringWithFormat:@"%@\n\n%@", title, subtitle];
|
||||
}
|
||||
|
||||
return title;
|
||||
}
|
||||
|
||||
- (NSString *)menuSubtitleForRow:(NSInteger)row {
|
||||
return @"";
|
||||
}
|
||||
|
||||
- (NSArray<UIMenuElement *> *)menuItemsForRow:(NSInteger)row sender:(UIViewController *)sender API_AVAILABLE(ios(13)) {
|
||||
NSArray<NSString *> *copyItems = [self copyMenuItemsForRow:row];
|
||||
NSAssert(copyItems.count % 2 == 0, @"copyMenuItemsForRow: should return an even list");
|
||||
|
||||
if (copyItems.count) {
|
||||
NSInteger numberOfActions = copyItems.count / 2;
|
||||
BOOL collapseMenu = numberOfActions > 4;
|
||||
UIImage *copyIcon = [UIImage systemImageNamed:@"doc.on.doc"];
|
||||
|
||||
NSMutableArray *actions = [NSMutableArray new];
|
||||
|
||||
for (NSInteger i = 0; i < copyItems.count; i += 2) {
|
||||
NSString *key = copyItems[i], *value = copyItems[i+1];
|
||||
NSString *title = collapseMenu ? key : [@"Copy " stringByAppendingString:key];
|
||||
|
||||
UIAction *copy = [UIAction
|
||||
actionWithTitle:title
|
||||
image:copyIcon
|
||||
identifier:nil
|
||||
handler:^(__kindof UIAction *action) {
|
||||
UIPasteboard.generalPasteboard.string = value;
|
||||
}
|
||||
];
|
||||
if (!value.length) {
|
||||
copy.attributes = UIMenuElementAttributesDisabled;
|
||||
}
|
||||
|
||||
[actions addObject:copy];
|
||||
}
|
||||
|
||||
UIMenu *copyMenu = [UIMenu
|
||||
flex_inlineMenuWithTitle:@"Copy…"
|
||||
image:copyIcon
|
||||
children:actions
|
||||
];
|
||||
|
||||
if (collapseMenu) {
|
||||
return @[[copyMenu flex_collapsed]];
|
||||
} else {
|
||||
return @[copyMenu];
|
||||
}
|
||||
}
|
||||
|
||||
return @[];
|
||||
}
|
||||
|
||||
- (NSArray<NSString *> *)copyMenuItemsForRow:(NSInteger)row {
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (NSString *)titleForRow:(NSInteger)row { return nil; }
|
||||
- (NSString *)subtitleForRow:(NSInteger)row { return nil; }
|
||||
|
||||
@end
|
||||
|
||||
#pragma clang diagnostic pop
|
||||
@@ -0,0 +1,15 @@
|
||||
//
|
||||
// FLEXCarouselCell.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner Bennett on 7/17/19.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface FLEXCarouselCell : UICollectionViewCell
|
||||
|
||||
@property (nonatomic, copy) NSString *title;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,93 @@
|
||||
//
|
||||
// FLEXCarouselCell.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner Bennett on 7/17/19.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXCarouselCell.h"
|
||||
#import "FLEXColor.h"
|
||||
#import "UIView+FLEX_Layout.h"
|
||||
|
||||
@interface FLEXCarouselCell ()
|
||||
@property (nonatomic, readonly) UILabel *titleLabel;
|
||||
@property (nonatomic, readonly) UIView *selectionIndicatorStripe;
|
||||
@property (nonatomic) BOOL constraintsInstalled;
|
||||
@end
|
||||
|
||||
@implementation FLEXCarouselCell
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
_titleLabel = [UILabel new];
|
||||
_selectionIndicatorStripe = [UIView new];
|
||||
|
||||
self.titleLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody];
|
||||
self.selectionIndicatorStripe.backgroundColor = self.tintColor;
|
||||
if (@available(iOS 10, *)) {
|
||||
self.titleLabel.adjustsFontForContentSizeCategory = YES;
|
||||
}
|
||||
|
||||
[self.contentView addSubview:self.titleLabel];
|
||||
[self.contentView addSubview:self.selectionIndicatorStripe];
|
||||
|
||||
[self installConstraints];
|
||||
|
||||
[self updateAppearance];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)updateAppearance {
|
||||
self.selectionIndicatorStripe.hidden = !self.selected;
|
||||
|
||||
if (self.selected) {
|
||||
self.titleLabel.textColor = self.tintColor;
|
||||
} else {
|
||||
self.titleLabel.textColor = FLEXColor.deemphasizedTextColor;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark Public
|
||||
|
||||
- (NSString *)title {
|
||||
return self.titleLabel.text;
|
||||
}
|
||||
|
||||
- (void)setTitle:(NSString *)title {
|
||||
self.titleLabel.text = title;
|
||||
[self.titleLabel sizeToFit];
|
||||
[self setNeedsLayout];
|
||||
}
|
||||
|
||||
#pragma mark Overrides
|
||||
|
||||
- (void)prepareForReuse {
|
||||
[super prepareForReuse];
|
||||
[self updateAppearance];
|
||||
}
|
||||
|
||||
- (void)installConstraints {
|
||||
CGFloat stripeHeight = 2;
|
||||
|
||||
self.titleLabel.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
self.selectionIndicatorStripe.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
|
||||
UIView *superview = self.contentView;
|
||||
[self.titleLabel flex_pinEdgesToSuperviewWithInsets:UIEdgeInsetsMake(10, 15, 8 + stripeHeight, 15)];
|
||||
|
||||
[self.selectionIndicatorStripe.leadingAnchor constraintEqualToAnchor:superview.leadingAnchor].active = YES;
|
||||
[self.selectionIndicatorStripe.bottomAnchor constraintEqualToAnchor:superview.bottomAnchor].active = YES;
|
||||
[self.selectionIndicatorStripe.trailingAnchor constraintEqualToAnchor:superview.trailingAnchor].active = YES;
|
||||
[self.selectionIndicatorStripe.heightAnchor constraintEqualToConstant:stripeHeight].active = YES;
|
||||
}
|
||||
|
||||
- (void)setSelected:(BOOL)selected {
|
||||
super.selected = selected;
|
||||
[self updateAppearance];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,20 @@
|
||||
//
|
||||
// FLEXScopeCarousel.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner Bennett on 7/17/19.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
/// Only use on iOS 10 and up. Requires iOS 10 APIs for calculating row sizes.
|
||||
@interface FLEXScopeCarousel : UIControl
|
||||
|
||||
@property (nonatomic, copy) NSArray<NSString *> *items;
|
||||
@property (nonatomic) NSInteger selectedIndex;
|
||||
@property (nonatomic) void(^selectedIndexChangedAction)(NSInteger idx);
|
||||
|
||||
- (void)registerBlockForDynamicTypeChanges:(void(^)(FLEXScopeCarousel *))handler;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,204 @@
|
||||
//
|
||||
// FLEXScopeCarousel.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner Bennett on 7/17/19.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXScopeCarousel.h"
|
||||
#import "FLEXCarouselCell.h"
|
||||
#import "FLEXColor.h"
|
||||
#import "FLEXMacros.h"
|
||||
#import "UIView+FLEX_Layout.h"
|
||||
|
||||
const CGFloat kCarouselItemSpacing = 0;
|
||||
NSString * const kCarouselCellReuseIdentifier = @"kCarouselCellReuseIdentifier";
|
||||
|
||||
@interface FLEXScopeCarousel () <UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout>
|
||||
@property (nonatomic, readonly) UICollectionView *collectionView;
|
||||
@property (nonatomic, readonly) FLEXCarouselCell *sizingCell;
|
||||
|
||||
@property (nonatomic, readonly) id dynamicTypeObserver;
|
||||
@property (nonatomic, readonly) NSMutableArray *dynamicTypeHandlers;
|
||||
|
||||
@property (nonatomic) BOOL constraintsInstalled;
|
||||
@end
|
||||
|
||||
@implementation FLEXScopeCarousel
|
||||
|
||||
- (id)initWithFrame:(CGRect)frame {
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
self.backgroundColor = FLEXColor.primaryBackgroundColor;
|
||||
self.autoresizingMask = UIViewAutoresizingFlexibleWidth;
|
||||
self.translatesAutoresizingMaskIntoConstraints = YES;
|
||||
_dynamicTypeHandlers = [NSMutableArray new];
|
||||
|
||||
CGSize itemSize = CGSizeZero;
|
||||
if (@available(iOS 10.0, *)) {
|
||||
itemSize = UICollectionViewFlowLayoutAutomaticSize;
|
||||
}
|
||||
|
||||
// Collection view layout
|
||||
UICollectionViewFlowLayout *layout = ({
|
||||
UICollectionViewFlowLayout *layout = [UICollectionViewFlowLayout new];
|
||||
layout.scrollDirection = UICollectionViewScrollDirectionHorizontal;
|
||||
layout.sectionInset = UIEdgeInsetsZero;
|
||||
layout.minimumLineSpacing = kCarouselItemSpacing;
|
||||
layout.itemSize = itemSize;
|
||||
layout.estimatedItemSize = itemSize;
|
||||
layout;
|
||||
});
|
||||
|
||||
// Collection view
|
||||
_collectionView = ({
|
||||
UICollectionView *cv = [[UICollectionView alloc]
|
||||
initWithFrame:CGRectZero
|
||||
collectionViewLayout:layout
|
||||
];
|
||||
cv.showsHorizontalScrollIndicator = NO;
|
||||
cv.backgroundColor = UIColor.clearColor;
|
||||
cv.delegate = self;
|
||||
cv.dataSource = self;
|
||||
[cv registerClass:[FLEXCarouselCell class] forCellWithReuseIdentifier:kCarouselCellReuseIdentifier];
|
||||
|
||||
[self addSubview:cv];
|
||||
cv;
|
||||
});
|
||||
|
||||
|
||||
// Sizing cell
|
||||
_sizingCell = [FLEXCarouselCell new];
|
||||
self.sizingCell.title = @"NSObject";
|
||||
|
||||
// Dynamic type
|
||||
weakify(self);
|
||||
_dynamicTypeObserver = [NSNotificationCenter.defaultCenter
|
||||
addObserverForName:UIContentSizeCategoryDidChangeNotification
|
||||
object:nil queue:nil usingBlock:^(NSNotification *note) { strongify(self)
|
||||
[self.collectionView setNeedsLayout];
|
||||
[self setNeedsUpdateConstraints];
|
||||
|
||||
// Notify observers
|
||||
for (void (^block)(FLEXScopeCarousel *) in self.dynamicTypeHandlers) {
|
||||
block(self);
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[NSNotificationCenter.defaultCenter removeObserver:self.dynamicTypeObserver];
|
||||
}
|
||||
|
||||
#pragma mark - Overrides
|
||||
|
||||
- (void)drawRect:(CGRect)rect {
|
||||
[super drawRect:rect];
|
||||
|
||||
CGFloat width = 1.f / UIScreen.mainScreen.scale;
|
||||
|
||||
// Draw hairline
|
||||
CGContextRef context = UIGraphicsGetCurrentContext();
|
||||
CGContextSetStrokeColorWithColor(context, FLEXColor.hairlineColor.CGColor);
|
||||
CGContextSetLineWidth(context, width);
|
||||
CGContextMoveToPoint(context, 0, rect.size.height - width);
|
||||
CGContextAddLineToPoint(context, rect.size.width, rect.size.height - width);
|
||||
CGContextStrokePath(context);
|
||||
}
|
||||
|
||||
+ (BOOL)requiresConstraintBasedLayout {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)updateConstraints {
|
||||
if (!self.constraintsInstalled) {
|
||||
self.collectionView.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
[self.collectionView flex_pinEdgesToSuperview];
|
||||
|
||||
self.constraintsInstalled = YES;
|
||||
}
|
||||
|
||||
[super updateConstraints];
|
||||
}
|
||||
|
||||
- (CGSize)intrinsicContentSize {
|
||||
return CGSizeMake(
|
||||
UIViewNoIntrinsicMetric,
|
||||
[self.sizingCell systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height
|
||||
);
|
||||
}
|
||||
|
||||
#pragma mark - Public
|
||||
|
||||
- (void)setItems:(NSArray<NSString *> *)items {
|
||||
NSParameterAssert(items.count);
|
||||
|
||||
_items = items.copy;
|
||||
|
||||
// Refresh list, select first item initially
|
||||
[self.collectionView reloadData];
|
||||
self.selectedIndex = 0;
|
||||
}
|
||||
|
||||
- (void)setSelectedIndex:(NSInteger)idx {
|
||||
NSParameterAssert(idx < self.items.count);
|
||||
|
||||
_selectedIndex = idx;
|
||||
NSIndexPath *path = [NSIndexPath indexPathForItem:idx inSection:0];
|
||||
[self.collectionView selectItemAtIndexPath:path
|
||||
animated:YES
|
||||
scrollPosition:UICollectionViewScrollPositionCenteredHorizontally];
|
||||
[self collectionView:self.collectionView didSelectItemAtIndexPath:path];
|
||||
}
|
||||
|
||||
- (void)registerBlockForDynamicTypeChanges:(void (^)(FLEXScopeCarousel *))handler {
|
||||
[self.dynamicTypeHandlers addObject:handler];
|
||||
}
|
||||
|
||||
#pragma mark - UICollectionView
|
||||
|
||||
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
|
||||
// if (@available(iOS 10.0, *)) {
|
||||
// return UICollectionViewFlowLayoutAutomaticSize;
|
||||
// }
|
||||
|
||||
self.sizingCell.title = self.items[indexPath.item];
|
||||
return [self.sizingCell systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
|
||||
}
|
||||
|
||||
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
|
||||
return self.items.count;
|
||||
}
|
||||
|
||||
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView
|
||||
cellForItemAtIndexPath:(NSIndexPath *)indexPath {
|
||||
FLEXCarouselCell *cell = (id)[collectionView dequeueReusableCellWithReuseIdentifier:kCarouselCellReuseIdentifier
|
||||
forIndexPath:indexPath];
|
||||
cell.title = self.items[indexPath.row];
|
||||
return cell;
|
||||
}
|
||||
|
||||
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
|
||||
_selectedIndex = indexPath.item; // In case self.selectedIndex didn't trigger this call
|
||||
|
||||
if (self.selectedIndexChangedAction) {
|
||||
self.selectedIndexChangedAction(indexPath.row);
|
||||
}
|
||||
|
||||
// TODO: dynamically choose a scroll position. Very wide items should
|
||||
// get "Left" while smaller items should not scroll at all, unless
|
||||
// they are only partially on the screen, in which case they
|
||||
// should get "HorizontallyCentered" to bring them onto the screen.
|
||||
// For now, everything goes to the left, as this has a similar effect.
|
||||
[collectionView scrollToItemAtIndexPath:indexPath
|
||||
atScrollPosition:UICollectionViewScrollPositionLeft
|
||||
animated:YES];
|
||||
[self sendActionsForControlEvents:UIControlEventValueChanged];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// FLEXCodeFontCell.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 12/27/19.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXMultilineTableViewCell.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface FLEXCodeFontCell : FLEXMultilineDetailTableViewCell
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,34 @@
|
||||
//
|
||||
// FLEXCodeFontCell.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 12/27/19.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXCodeFontCell.h"
|
||||
#import "UIFont+FLEX.h"
|
||||
|
||||
@implementation FLEXCodeFontCell
|
||||
|
||||
- (void)postInit {
|
||||
[super postInit];
|
||||
|
||||
self.titleLabel.font = UIFont.flex_codeFont;
|
||||
self.subtitleLabel.font = UIFont.flex_codeFont;
|
||||
|
||||
self.titleLabel.adjustsFontSizeToFitWidth = YES;
|
||||
self.titleLabel.minimumScaleFactor = 0.9;
|
||||
self.subtitleLabel.adjustsFontSizeToFitWidth = YES;
|
||||
self.subtitleLabel.minimumScaleFactor = 0.75;
|
||||
|
||||
// Disable mutli-line pre iOS 11
|
||||
if (@available(iOS 11, *)) {
|
||||
self.subtitleLabel.numberOfLines = 5;
|
||||
} else {
|
||||
self.titleLabel.numberOfLines = 1;
|
||||
self.subtitleLabel.numberOfLines = 1;
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,13 @@
|
||||
//
|
||||
// FLEXKeyValueTableViewCell.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner Bennett on 1/23/20.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXTableViewCell.h"
|
||||
|
||||
@interface FLEXKeyValueTableViewCell : FLEXTableViewCell
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// FLEXKeyValueTableViewCell.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner Bennett on 1/23/20.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXKeyValueTableViewCell.h"
|
||||
|
||||
@implementation FLEXKeyValueTableViewCell
|
||||
|
||||
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
|
||||
return [super initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:reuseIdentifier];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,24 @@
|
||||
//
|
||||
// FLEXMultilineTableViewCell.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Ryan Olson on 2/13/15.
|
||||
// Copyright (c) 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXTableViewCell.h"
|
||||
|
||||
/// A cell with both labels set to be multi-line capable.
|
||||
@interface FLEXMultilineTableViewCell : FLEXTableViewCell
|
||||
|
||||
+ (CGFloat)preferredHeightWithAttributedText:(NSAttributedString *)attributedText
|
||||
maxWidth:(CGFloat)contentViewWidth
|
||||
style:(UITableViewStyle)style
|
||||
showsAccessory:(BOOL)showsAccessory;
|
||||
|
||||
@end
|
||||
|
||||
/// A \c FLEXMultilineTableViewCell initialized with \c UITableViewCellStyleSubtitle
|
||||
@interface FLEXMultilineDetailTableViewCell : FLEXMultilineTableViewCell
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,67 @@
|
||||
//
|
||||
// FLEXMultilineTableViewCell.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Ryan Olson on 2/13/15.
|
||||
// Copyright (c) 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXMultilineTableViewCell.h"
|
||||
#import "UIView+FLEX_Layout.h"
|
||||
#import "FLEXUtility.h"
|
||||
|
||||
@interface FLEXMultilineTableViewCell ()
|
||||
@property (nonatomic, readonly) UILabel *_titleLabel;
|
||||
@property (nonatomic, readonly) UILabel *_subtitleLabel;
|
||||
@property (nonatomic) BOOL constraintsUpdated;
|
||||
@end
|
||||
|
||||
@implementation FLEXMultilineTableViewCell
|
||||
|
||||
- (void)postInit {
|
||||
[super postInit];
|
||||
|
||||
self.titleLabel.numberOfLines = 0;
|
||||
self.subtitleLabel.numberOfLines = 0;
|
||||
}
|
||||
|
||||
+ (UIEdgeInsets)labelInsets {
|
||||
return UIEdgeInsetsMake(10.0, 16.0, 10.0, 8.0);
|
||||
}
|
||||
|
||||
+ (CGFloat)preferredHeightWithAttributedText:(NSAttributedString *)attributedText
|
||||
maxWidth:(CGFloat)contentViewWidth
|
||||
style:(UITableViewStyle)style
|
||||
showsAccessory:(BOOL)showsAccessory {
|
||||
CGFloat labelWidth = contentViewWidth;
|
||||
|
||||
// Content view inset due to accessory view observed on iOS 8.1 iPhone 6.
|
||||
if (showsAccessory) {
|
||||
labelWidth -= 34.0;
|
||||
}
|
||||
|
||||
UIEdgeInsets labelInsets = [self labelInsets];
|
||||
labelWidth -= (labelInsets.left + labelInsets.right);
|
||||
|
||||
CGSize constrainSize = CGSizeMake(labelWidth, CGFLOAT_MAX);
|
||||
CGRect boundingBox = [attributedText
|
||||
boundingRectWithSize:constrainSize
|
||||
options:NSStringDrawingUsesLineFragmentOrigin
|
||||
context:nil
|
||||
];
|
||||
CGFloat preferredLabelHeight = FLEXFloor(boundingBox.size.height);
|
||||
CGFloat preferredCellHeight = preferredLabelHeight + labelInsets.top + labelInsets.bottom + 1.0;
|
||||
|
||||
return preferredCellHeight;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation FLEXMultilineDetailTableViewCell
|
||||
|
||||
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
|
||||
return [super initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:reuseIdentifier];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,14 @@
|
||||
//
|
||||
// FLEXSubtitleTableViewCell.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 4/17/19.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXTableViewCell.h"
|
||||
|
||||
/// A cell initialized with \c UITableViewCellStyleSubtitle
|
||||
@interface FLEXSubtitleTableViewCell : FLEXTableViewCell
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// FLEXSubtitleTableViewCell.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 4/17/19.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXSubtitleTableViewCell.h"
|
||||
|
||||
@implementation FLEXSubtitleTableViewCell
|
||||
|
||||
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
|
||||
return [super initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:reuseIdentifier];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// FLEXTableViewCell.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 4/17/19.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface FLEXTableViewCell : UITableViewCell
|
||||
|
||||
/// Use this instead of .textLabel
|
||||
@property (nonatomic, readonly) UILabel *titleLabel;
|
||||
/// Use this instead of .detailTextLabel
|
||||
@property (nonatomic, readonly) UILabel *subtitleLabel;
|
||||
|
||||
/// Subclasses can override this instead of initializers to
|
||||
/// perform additional initialization without lots of boilerplate.
|
||||
/// Remember to call super!
|
||||
- (void)postInit;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,57 @@
|
||||
//
|
||||
// FLEXTableViewCell.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 4/17/19.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXTableViewCell.h"
|
||||
#import "FLEXUtility.h"
|
||||
#import "FLEXColor.h"
|
||||
#import "FLEXTableView.h"
|
||||
|
||||
@interface UITableView (Internal)
|
||||
// Exists at least since iOS 5
|
||||
- (BOOL)_canPerformAction:(SEL)action forCell:(UITableViewCell *)cell sender:(id)sender;
|
||||
- (void)_performAction:(SEL)action forCell:(UITableViewCell *)cell sender:(id)sender;
|
||||
@end
|
||||
|
||||
@interface UITableViewCell (Internal)
|
||||
// Exists at least since iOS 5
|
||||
@property (nonatomic, readonly) FLEXTableView *_tableView;
|
||||
@end
|
||||
|
||||
@implementation FLEXTableViewCell
|
||||
|
||||
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
|
||||
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
|
||||
if (self) {
|
||||
[self postInit];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)postInit {
|
||||
UIFont *cellFont = UIFont.flex_defaultTableCellFont;
|
||||
self.titleLabel.font = cellFont;
|
||||
self.subtitleLabel.font = cellFont;
|
||||
self.subtitleLabel.textColor = FLEXColor.deemphasizedTextColor;
|
||||
|
||||
self.titleLabel.lineBreakMode = NSLineBreakByTruncatingMiddle;
|
||||
self.subtitleLabel.lineBreakMode = NSLineBreakByTruncatingMiddle;
|
||||
|
||||
self.titleLabel.numberOfLines = 1;
|
||||
self.subtitleLabel.numberOfLines = 1;
|
||||
}
|
||||
|
||||
- (UILabel *)titleLabel {
|
||||
return self.textLabel;
|
||||
}
|
||||
|
||||
- (UILabel *)subtitleLabel {
|
||||
return self.detailTextLabel;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,48 @@
|
||||
//
|
||||
// FLEXTableView.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 4/17/19.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
#pragma mark Reuse identifiers
|
||||
|
||||
typedef NSString * FLEXTableViewCellReuseIdentifier;
|
||||
|
||||
/// A regular \c FLEXTableViewCell initialized with \c UITableViewCellStyleDefault
|
||||
extern FLEXTableViewCellReuseIdentifier const kFLEXDefaultCell;
|
||||
/// A \c FLEXSubtitleTableViewCell initialized with \c UITableViewCellStyleSubtitle
|
||||
extern FLEXTableViewCellReuseIdentifier const kFLEXDetailCell;
|
||||
/// A \c FLEXMultilineTableViewCell initialized with \c UITableViewCellStyleDefault
|
||||
extern FLEXTableViewCellReuseIdentifier const kFLEXMultilineCell;
|
||||
/// A \c FLEXMultilineTableViewCell initialized with \c UITableViewCellStyleSubtitle
|
||||
extern FLEXTableViewCellReuseIdentifier const kFLEXMultilineDetailCell;
|
||||
/// A \c FLEXTableViewCell initialized with \c UITableViewCellStyleValue1
|
||||
extern FLEXTableViewCellReuseIdentifier const kFLEXKeyValueCell;
|
||||
/// A \c FLEXSubtitleTableViewCell which uses monospaced fonts for both labels
|
||||
extern FLEXTableViewCellReuseIdentifier const kFLEXCodeFontCell;
|
||||
|
||||
#pragma mark - FLEXTableView
|
||||
@interface FLEXTableView : UITableView
|
||||
|
||||
+ (instancetype)flexDefaultTableView;
|
||||
+ (instancetype)groupedTableView;
|
||||
+ (instancetype)plainTableView;
|
||||
+ (instancetype)style:(UITableViewStyle)style;
|
||||
|
||||
/// You do not need to register classes for any of the default reuse identifiers above
|
||||
/// (annotated as \c FLEXTableViewCellReuseIdentifier types) unless you wish to provide
|
||||
/// a custom cell for any of those reuse identifiers. By default, \c FLEXTableViewCell,
|
||||
/// \c FLEXSubtitleTableViewCell, and \c FLEXMultilineTableViewCell are used, respectively.
|
||||
///
|
||||
/// @param registrationMapping A map of reuse identifiers to \c UITableViewCell (sub)class objects.
|
||||
- (void)registerCells:(NSDictionary<NSString *, Class> *)registrationMapping;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,83 @@
|
||||
//
|
||||
// FLEXTableView.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 4/17/19.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXTableView.h"
|
||||
#import "FLEXUtility.h"
|
||||
#import "FLEXSubtitleTableViewCell.h"
|
||||
#import "FLEXMultilineTableViewCell.h"
|
||||
#import "FLEXKeyValueTableViewCell.h"
|
||||
#import "FLEXCodeFontCell.h"
|
||||
|
||||
FLEXTableViewCellReuseIdentifier const kFLEXDefaultCell = @"kFLEXDefaultCell";
|
||||
FLEXTableViewCellReuseIdentifier const kFLEXDetailCell = @"kFLEXDetailCell";
|
||||
FLEXTableViewCellReuseIdentifier const kFLEXMultilineCell = @"kFLEXMultilineCell";
|
||||
FLEXTableViewCellReuseIdentifier const kFLEXMultilineDetailCell = @"kFLEXMultilineDetailCell";
|
||||
FLEXTableViewCellReuseIdentifier const kFLEXKeyValueCell = @"kFLEXKeyValueCell";
|
||||
FLEXTableViewCellReuseIdentifier const kFLEXCodeFontCell = @"kFLEXCodeFontCell";
|
||||
|
||||
#pragma mark Private
|
||||
|
||||
@interface UITableView (Private)
|
||||
- (CGFloat)_heightForHeaderInSection:(NSInteger)section;
|
||||
- (NSString *)_titleForHeaderInSection:(NSInteger)section;
|
||||
@end
|
||||
|
||||
@implementation FLEXTableView
|
||||
|
||||
+ (instancetype)flexDefaultTableView {
|
||||
if (@available(iOS 13.0, *)) {
|
||||
return [[self alloc] initWithFrame:CGRectZero style:UITableViewStyleInsetGrouped];
|
||||
} else {
|
||||
return [[self alloc] initWithFrame:CGRectZero style:UITableViewStyleGrouped];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Initialization
|
||||
|
||||
+ (id)groupedTableView {
|
||||
if (@available(iOS 13.0, *)) {
|
||||
return [[self alloc] initWithFrame:CGRectZero style:UITableViewStyleInsetGrouped];
|
||||
} else {
|
||||
return [[self alloc] initWithFrame:CGRectZero style:UITableViewStyleGrouped];
|
||||
}
|
||||
}
|
||||
|
||||
+ (id)plainTableView {
|
||||
return [[self alloc] initWithFrame:CGRectZero style:UITableViewStylePlain];
|
||||
}
|
||||
|
||||
+ (id)style:(UITableViewStyle)style {
|
||||
return [[self alloc] initWithFrame:CGRectZero style:style];
|
||||
}
|
||||
|
||||
- (id)initWithFrame:(CGRect)frame style:(UITableViewStyle)style {
|
||||
self = [super initWithFrame:frame style:style];
|
||||
if (self) {
|
||||
[self registerCells:@{
|
||||
kFLEXDefaultCell : [FLEXTableViewCell class],
|
||||
kFLEXDetailCell : [FLEXSubtitleTableViewCell class],
|
||||
kFLEXMultilineCell : [FLEXMultilineTableViewCell class],
|
||||
kFLEXMultilineDetailCell : [FLEXMultilineDetailTableViewCell class],
|
||||
kFLEXKeyValueCell : [FLEXKeyValueTableViewCell class],
|
||||
kFLEXCodeFontCell : [FLEXCodeFontCell class],
|
||||
}];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Public
|
||||
|
||||
- (void)registerCells:(NSDictionary<NSString*, Class> *)registrationMapping {
|
||||
[registrationMapping enumerateKeysAndObjectsUsingBlock:^(NSString *identifier, Class cellClass, BOOL *stop) {
|
||||
[self registerClass:cellClass forCellReuseIdentifier:identifier];
|
||||
}];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,13 +0,0 @@
|
||||
//
|
||||
// FLEXArgumentInputJSONObjectView.h
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/15/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputTextView.h"
|
||||
|
||||
@interface FLEXArgumentInputJSONObjectView : FLEXArgumentInputTextView
|
||||
|
||||
@end
|
||||
@@ -1,65 +0,0 @@
|
||||
//
|
||||
// FLEXArgumentInputJSONObjectView.m
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/15/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputJSONObjectView.h"
|
||||
#import "FLEXRuntimeUtility.h"
|
||||
|
||||
@implementation FLEXArgumentInputJSONObjectView
|
||||
|
||||
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding
|
||||
{
|
||||
self = [super initWithArgumentTypeEncoding:typeEncoding];
|
||||
if (self) {
|
||||
// Start with the numbers and punctuation keyboard since quotes, curly braces, or
|
||||
// square brackets are likely to be the first characters type for the JSON.
|
||||
self.inputTextView.keyboardType = UIKeyboardTypeNumbersAndPunctuation;
|
||||
self.targetSize = FLEXArgumentInputViewSizeLarge;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setInputValue:(id)inputValue
|
||||
{
|
||||
self.inputTextView.text = [FLEXRuntimeUtility editableJSONStringForObject:inputValue];
|
||||
}
|
||||
|
||||
- (id)inputValue
|
||||
{
|
||||
return [FLEXRuntimeUtility objectValueFromEditableJSONString:self.inputTextView.text];
|
||||
}
|
||||
|
||||
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value
|
||||
{
|
||||
// Must be object type.
|
||||
BOOL supported = type && type[0] == '@';
|
||||
|
||||
if (supported) {
|
||||
if (value) {
|
||||
// If there's a current value, it must be serializable to JSON
|
||||
supported = [FLEXRuntimeUtility editableJSONStringForObject:value] != nil;
|
||||
} else {
|
||||
// Otherwise, see if we have more type information than just 'id'.
|
||||
// If we do, make sure the encoding is something serializable to JSON.
|
||||
// Properties and ivars keep more detailed type encoding information than method arguments.
|
||||
if (strcmp(type, @encode(id)) != 0) {
|
||||
BOOL isJSONSerializableType = NO;
|
||||
// Note: we can't use @encode(NSString) here because that drops the string information and just goes to @encode(id).
|
||||
isJSONSerializableType = isJSONSerializableType || strcmp(type, FLEXEncodeClass(NSString)) == 0;
|
||||
isJSONSerializableType = isJSONSerializableType || strcmp(type, FLEXEncodeClass(NSNumber)) == 0;
|
||||
isJSONSerializableType = isJSONSerializableType || strcmp(type, FLEXEncodeClass(NSArray)) == 0;
|
||||
isJSONSerializableType = isJSONSerializableType || strcmp(type, FLEXEncodeClass(NSDictionary)) == 0;
|
||||
|
||||
supported = isJSONSerializableType;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return supported;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,57 +0,0 @@
|
||||
//
|
||||
// FLEXArgumentInputNumberView.m
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/15/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputNumberView.h"
|
||||
#import "FLEXRuntimeUtility.h"
|
||||
|
||||
@implementation FLEXArgumentInputNumberView
|
||||
|
||||
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding
|
||||
{
|
||||
self = [super initWithArgumentTypeEncoding:typeEncoding];
|
||||
if (self) {
|
||||
self.inputTextView.keyboardType = UIKeyboardTypeNumbersAndPunctuation;
|
||||
self.targetSize = FLEXArgumentInputViewSizeSmall;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setInputValue:(id)inputValue
|
||||
{
|
||||
if ([inputValue respondsToSelector:@selector(stringValue)]) {
|
||||
self.inputTextView.text = [inputValue stringValue];
|
||||
}
|
||||
}
|
||||
|
||||
- (id)inputValue
|
||||
{
|
||||
return [FLEXRuntimeUtility valueForNumberWithObjCType:[self.typeEncoding UTF8String] fromInputString:self.inputTextView.text];
|
||||
}
|
||||
|
||||
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value
|
||||
{
|
||||
static NSArray *primitiveTypes = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
primitiveTypes = @[@(@encode(char)),
|
||||
@(@encode(int)),
|
||||
@(@encode(short)),
|
||||
@(@encode(long)),
|
||||
@(@encode(long long)),
|
||||
@(@encode(unsigned char)),
|
||||
@(@encode(unsigned int)),
|
||||
@(@encode(unsigned short)),
|
||||
@(@encode(unsigned long)),
|
||||
@(@encode(unsigned long long)),
|
||||
@(@encode(float)),
|
||||
@(@encode(double))];
|
||||
});
|
||||
return type && [primitiveTypes containsObject:@(type)];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,45 +0,0 @@
|
||||
//
|
||||
// FLEXArgumentInputStringView.m
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/28/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputStringView.h"
|
||||
#import "FLEXRuntimeUtility.h"
|
||||
|
||||
@implementation FLEXArgumentInputStringView
|
||||
|
||||
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding
|
||||
{
|
||||
self = [super initWithArgumentTypeEncoding:typeEncoding];
|
||||
if (self) {
|
||||
self.targetSize = FLEXArgumentInputViewSizeLarge;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setInputValue:(id)inputValue
|
||||
{
|
||||
self.inputTextView.text = inputValue;
|
||||
}
|
||||
|
||||
- (id)inputValue
|
||||
{
|
||||
// Interpret empty string as nil. We loose the ablitiy to set empty string as a string value,
|
||||
// but we accept that tradeoff in exchange for not having to type quotes for every string.
|
||||
return [self.inputTextView.text length] > 0 ? [self.inputTextView.text copy] : nil;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
|
||||
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value
|
||||
{
|
||||
BOOL supported = type && strcmp(type, FLEXEncodeClass(NSString)) == 0;
|
||||
supported = supported || (value && [value isKindOfClass:[NSString class]]);
|
||||
return supported;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,13 +0,0 @@
|
||||
//
|
||||
// FLEXArgumentInputStructView.h
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/16/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputView.h"
|
||||
|
||||
@interface FLEXArgumentInputStructView : FLEXArgumentInputView
|
||||
|
||||
@end
|
||||
@@ -1,210 +0,0 @@
|
||||
//
|
||||
// FLEXArgumentInputStructView.m
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/16/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputStructView.h"
|
||||
#import "FLEXArgumentInputViewFactory.h"
|
||||
#import "FLEXRuntimeUtility.h"
|
||||
|
||||
@interface FLEXArgumentInputStructView ()
|
||||
|
||||
@property (nonatomic, strong) NSArray *argumentInputViews;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXArgumentInputStructView
|
||||
|
||||
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding
|
||||
{
|
||||
self = [super initWithArgumentTypeEncoding:typeEncoding];
|
||||
if (self) {
|
||||
NSMutableArray *inputViews = [NSMutableArray array];
|
||||
NSArray *customTitles = [[self class] customFieldTitlesForTypeEncoding:typeEncoding];
|
||||
[FLEXRuntimeUtility enumerateTypesInStructEncoding:typeEncoding usingBlock:^(NSString *structName, const char *fieldTypeEncoding, NSString *prettyTypeEncoding, NSUInteger fieldIndex, NSUInteger fieldOffset) {
|
||||
|
||||
FLEXArgumentInputView *inputView = [FLEXArgumentInputViewFactory argumentInputViewForTypeEncoding:fieldTypeEncoding];
|
||||
inputView.backgroundColor = self.backgroundColor;
|
||||
inputView.targetSize = FLEXArgumentInputViewSizeSmall;
|
||||
|
||||
if (fieldIndex < [customTitles count]) {
|
||||
inputView.title = [customTitles objectAtIndex:fieldIndex];
|
||||
} else {
|
||||
inputView.title = [NSString stringWithFormat:@"%@ field %lu (%@)", structName, (unsigned long)fieldIndex, prettyTypeEncoding];
|
||||
}
|
||||
|
||||
[inputViews addObject:inputView];
|
||||
[self addSubview:inputView];
|
||||
}];
|
||||
self.argumentInputViews = inputViews;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Superclass Overrides
|
||||
|
||||
- (void)setBackgroundColor:(UIColor *)backgroundColor
|
||||
{
|
||||
[super setBackgroundColor:backgroundColor];
|
||||
for (FLEXArgumentInputView *inputView in self.argumentInputViews) {
|
||||
inputView.backgroundColor = backgroundColor;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setInputValue:(id)inputValue
|
||||
{
|
||||
if ([inputValue isKindOfClass:[NSValue class]]) {
|
||||
const char *structTypeEncoding = [inputValue objCType];
|
||||
if (strcmp([self.typeEncoding UTF8String], structTypeEncoding) == 0) {
|
||||
NSUInteger valueSize = 0;
|
||||
@try {
|
||||
// NSGetSizeAndAlignment barfs on type encoding for bitfields.
|
||||
NSGetSizeAndAlignment(structTypeEncoding, &valueSize, NULL);
|
||||
} @catch (NSException *exception) { }
|
||||
|
||||
if (valueSize > 0) {
|
||||
void *unboxedValue = malloc(valueSize);
|
||||
[inputValue getValue:unboxedValue];
|
||||
[FLEXRuntimeUtility enumerateTypesInStructEncoding:structTypeEncoding usingBlock:^(NSString *structName, const char *fieldTypeEncoding, NSString *prettyTypeEncoding, NSUInteger fieldIndex, NSUInteger fieldOffset) {
|
||||
|
||||
void *fieldPointer = unboxedValue + fieldOffset;
|
||||
FLEXArgumentInputView *inputView = [self.argumentInputViews objectAtIndex:fieldIndex];
|
||||
|
||||
if (fieldTypeEncoding[0] == @encode(id)[0] || fieldTypeEncoding[0] == @encode(Class)[0]) {
|
||||
inputView.inputValue = (__bridge id)fieldPointer;
|
||||
} else {
|
||||
NSValue *boxedField = [FLEXRuntimeUtility valueForPrimitivePointer:fieldPointer objCType:fieldTypeEncoding];
|
||||
inputView.inputValue = boxedField;
|
||||
}
|
||||
}];
|
||||
free(unboxedValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (id)inputValue
|
||||
{
|
||||
NSValue *boxedStruct = nil;
|
||||
const char *structTypeEncoding = [self.typeEncoding UTF8String];
|
||||
NSUInteger structSize = 0;
|
||||
@try {
|
||||
// NSGetSizeAndAlignment barfs on type encoding for bitfields.
|
||||
NSGetSizeAndAlignment(structTypeEncoding, &structSize, NULL);
|
||||
} @catch (NSException *exception) { }
|
||||
|
||||
if (structSize > 0) {
|
||||
void *unboxedStruct = malloc(structSize);
|
||||
[FLEXRuntimeUtility enumerateTypesInStructEncoding:structTypeEncoding usingBlock:^(NSString *structName, const char *fieldTypeEncoding, NSString *prettyTypeEncoding, NSUInteger fieldIndex, NSUInteger fieldOffset) {
|
||||
|
||||
void *fieldPointer = unboxedStruct + fieldOffset;
|
||||
FLEXArgumentInputView *inputView = [self.argumentInputViews objectAtIndex:fieldIndex];
|
||||
|
||||
if (fieldTypeEncoding[0] == @encode(id)[0] || fieldTypeEncoding[0] == @encode(Class)[0]) {
|
||||
// Object fields
|
||||
memcpy(fieldPointer, (__bridge void *)inputView.inputValue, sizeof(id));
|
||||
} else {
|
||||
// Boxed primitive/struct fields
|
||||
id inputValue = inputView.inputValue;
|
||||
if ([inputValue isKindOfClass:[NSValue class]] && strcmp([inputValue objCType], fieldTypeEncoding) == 0) {
|
||||
[inputValue getValue:fieldPointer];
|
||||
}
|
||||
}
|
||||
}];
|
||||
|
||||
boxedStruct = [NSValue value:unboxedStruct withObjCType:structTypeEncoding];
|
||||
free(unboxedStruct);
|
||||
}
|
||||
|
||||
return boxedStruct;
|
||||
}
|
||||
|
||||
- (BOOL)inputViewIsFirstResponder
|
||||
{
|
||||
BOOL isFirstResponder = NO;
|
||||
for (FLEXArgumentInputView *inputView in self.argumentInputViews) {
|
||||
if ([inputView inputViewIsFirstResponder]) {
|
||||
isFirstResponder = YES;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return isFirstResponder;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Layout and Sizing
|
||||
|
||||
- (void)layoutSubviews
|
||||
{
|
||||
[super layoutSubviews];
|
||||
|
||||
CGFloat runningOriginY = self.topInputFieldVerticalLayoutGuide;
|
||||
|
||||
for (FLEXArgumentInputView *inputView in self.argumentInputViews) {
|
||||
CGSize inputFitSize = [inputView sizeThatFits:self.bounds.size];
|
||||
inputView.frame = CGRectMake(0, runningOriginY, inputFitSize.width, inputFitSize.height);
|
||||
runningOriginY = CGRectGetMaxY(inputView.frame) + [[self class] verticalPaddingBetweenFields];
|
||||
}
|
||||
}
|
||||
|
||||
+ (CGFloat)verticalPaddingBetweenFields
|
||||
{
|
||||
return 10.0;
|
||||
}
|
||||
|
||||
- (CGSize)sizeThatFits:(CGSize)size
|
||||
{
|
||||
CGSize fitSize = [super sizeThatFits:size];
|
||||
|
||||
CGSize constrainSize = CGSizeMake(size.width, CGFLOAT_MAX);
|
||||
CGFloat height = fitSize.height;
|
||||
|
||||
for (FLEXArgumentInputView *inputView in self.argumentInputViews) {
|
||||
height += [inputView sizeThatFits:constrainSize].height;
|
||||
height += [[self class] verticalPaddingBetweenFields];
|
||||
}
|
||||
|
||||
return CGSizeMake(fitSize.width, height);
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Class Helpers
|
||||
|
||||
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value
|
||||
{
|
||||
return type && type[0] == '{';
|
||||
}
|
||||
|
||||
+ (NSArray *)customFieldTitlesForTypeEncoding:(const char *)typeEncoding
|
||||
{
|
||||
NSArray *customTitles = nil;
|
||||
if (strcmp(typeEncoding, @encode(CGRect)) == 0) {
|
||||
customTitles = @[@"CGPoint origin", @"CGSize size"];
|
||||
} else if (strcmp(typeEncoding, @encode(CGPoint)) == 0) {
|
||||
customTitles = @[@"CGFloat x", @"CGFloat y"];
|
||||
} else if (strcmp(typeEncoding, @encode(CGSize)) == 0) {
|
||||
customTitles = @[@"CGFloat width", @"CGFloat height"];
|
||||
} else if (strcmp(typeEncoding, @encode(UIEdgeInsets)) == 0) {
|
||||
customTitles = @[@"CGFloat top", @"CGFloat left", @"CGFloat bottom", @"CGFloat right"];
|
||||
} else if (strcmp(typeEncoding, @encode(UIOffset)) == 0) {
|
||||
customTitles = @[@"CGFloat horizontal", @"CGFloat vertical"];
|
||||
} else if (strcmp(typeEncoding, @encode(NSRange)) == 0) {
|
||||
customTitles = @[@"NSUInteger location", @"NSUInteger length"];
|
||||
} else if (strcmp(typeEncoding, @encode(CATransform3D)) == 0) {
|
||||
customTitles = @[@"CGFloat m11", @"CGFloat m12", @"CGFloat m13", @"CGFloat m14",
|
||||
@"CGFloat m21", @"CGFloat m22", @"CGFloat m23", @"CGFloat m24",
|
||||
@"CGFloat m31", @"CGFloat m32", @"CGFloat m33", @"CGFloat m34",
|
||||
@"CGFloat m41", @"CGFloat m42", @"CGFloat m43", @"CGFloat m44"];
|
||||
} else if (strcmp(typeEncoding, @encode(CGAffineTransform)) == 0) {
|
||||
customTitles = @[@"CGFloat a", @"CGFloat b",
|
||||
@"CGFloat c", @"CGFloat d",
|
||||
@"CGFloat tx", @"CGFloat ty"];
|
||||
}
|
||||
return customTitles;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,17 +0,0 @@
|
||||
//
|
||||
// FLEXArgumentInputTextView.h
|
||||
// FLEXInjected
|
||||
//
|
||||
// Created by Ryan Olson on 6/15/14.
|
||||
//
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputView.h"
|
||||
|
||||
@interface FLEXArgumentInputTextView : FLEXArgumentInputView
|
||||
|
||||
// For subclass eyes only
|
||||
|
||||
@property (nonatomic, strong, readonly) UITextView *inputTextView;
|
||||
|
||||
@end
|
||||
@@ -1,121 +0,0 @@
|
||||
//
|
||||
// FLEXArgumentInputTextView.m
|
||||
// FLEXInjected
|
||||
//
|
||||
// Created by Ryan Olson on 6/15/14.
|
||||
//
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputTextView.h"
|
||||
#import "FLEXUtility.h"
|
||||
|
||||
@interface FLEXArgumentInputTextView () <UITextViewDelegate>
|
||||
|
||||
@property (nonatomic, strong) UITextView *inputTextView;
|
||||
@property (nonatomic, readonly) NSUInteger numberOfInputLines;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXArgumentInputTextView
|
||||
|
||||
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding
|
||||
{
|
||||
self = [super initWithArgumentTypeEncoding:typeEncoding];
|
||||
if (self) {
|
||||
self.inputTextView = [[UITextView alloc] init];
|
||||
self.inputTextView.font = [[self class] inputFont];
|
||||
self.inputTextView.backgroundColor = [UIColor whiteColor];
|
||||
self.inputTextView.layer.borderColor = [[UIColor blackColor] CGColor];
|
||||
self.inputTextView.layer.borderWidth = 1.0;
|
||||
self.inputTextView.autocapitalizationType = UITextAutocapitalizationTypeNone;
|
||||
self.inputTextView.autocorrectionType = UITextAutocorrectionTypeNo;
|
||||
self.inputTextView.delegate = self;
|
||||
self.inputTextView.inputAccessoryView = [self createToolBar];
|
||||
[self addSubview:self.inputTextView];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - private
|
||||
|
||||
- (UIToolbar*)createToolBar
|
||||
{
|
||||
UIToolbar *toolBar = [UIToolbar new];
|
||||
[toolBar sizeToFit];
|
||||
UIBarButtonItem *spaceItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil];
|
||||
UIBarButtonItem *doneItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(textViewDone)];
|
||||
toolBar.items = @[spaceItem, doneItem];
|
||||
return toolBar;
|
||||
}
|
||||
|
||||
- (void)textViewDone
|
||||
{
|
||||
[self.inputTextView resignFirstResponder];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Text View Changes
|
||||
|
||||
- (void)textViewDidChange:(UITextView *)textView
|
||||
{
|
||||
[self.delegate argumentInputViewValueDidChange:self];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Superclass Overrides
|
||||
|
||||
- (BOOL)inputViewIsFirstResponder
|
||||
{
|
||||
return self.inputTextView.isFirstResponder;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Layout and Sizing
|
||||
|
||||
- (void)layoutSubviews
|
||||
{
|
||||
[super layoutSubviews];
|
||||
|
||||
self.inputTextView.frame = CGRectMake(0, self.topInputFieldVerticalLayoutGuide, self.bounds.size.width, [self inputTextViewHeight]);
|
||||
}
|
||||
|
||||
- (NSUInteger)numberOfInputLines
|
||||
{
|
||||
NSUInteger numberOfInputLines = 0;
|
||||
switch (self.targetSize) {
|
||||
case FLEXArgumentInputViewSizeDefault:
|
||||
numberOfInputLines = 2;
|
||||
break;
|
||||
|
||||
case FLEXArgumentInputViewSizeSmall:
|
||||
numberOfInputLines = 1;
|
||||
break;
|
||||
|
||||
case FLEXArgumentInputViewSizeLarge:
|
||||
numberOfInputLines = 8;
|
||||
break;
|
||||
}
|
||||
return numberOfInputLines;
|
||||
}
|
||||
|
||||
- (CGFloat)inputTextViewHeight
|
||||
{
|
||||
return ceil([[self class] inputFont].lineHeight * self.numberOfInputLines) + 16.0;
|
||||
}
|
||||
|
||||
- (CGSize)sizeThatFits:(CGSize)size
|
||||
{
|
||||
CGSize fitSize = [super sizeThatFits:size];
|
||||
fitSize.height += [self inputTextViewHeight];
|
||||
return fitSize;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Class Helpers
|
||||
|
||||
+ (UIFont *)inputFont
|
||||
{
|
||||
return [FLEXUtility defaultFontOfSize:14.0];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,64 +0,0 @@
|
||||
//
|
||||
// FLEXArgumentInputViewFactory.m
|
||||
// FLEXInjected
|
||||
//
|
||||
// Created by Ryan Olson on 6/15/14.
|
||||
//
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputViewFactory.h"
|
||||
#import "FLEXArgumentInputView.h"
|
||||
#import "FLEXArgumentInputJSONObjectView.h"
|
||||
#import "FLEXArgumentInputNumberView.h"
|
||||
#import "FLEXArgumentInputSwitchView.h"
|
||||
#import "FLEXArgumentInputStructView.h"
|
||||
#import "FLEXArgumentInputNotSupportedView.h"
|
||||
#import "FLEXArgumentInputStringView.h"
|
||||
#import "FLEXArgumentInputFontView.h"
|
||||
#import "FLEXArgumentInputColorView.h"
|
||||
|
||||
@implementation FLEXArgumentInputViewFactory
|
||||
|
||||
+ (FLEXArgumentInputView *)argumentInputViewForTypeEncoding:(const char *)typeEncoding
|
||||
{
|
||||
Class subclass = [self argumentInputViewSubclassForTypeEncoding:typeEncoding currentValue:nil];
|
||||
if (!subclass) {
|
||||
// Fall back to a FLEXArgumentInputNotSupportedView if we can't find a subclass that fits the type encoding.
|
||||
// The unsupported view shows "nil" and does not allow user input.
|
||||
subclass = [FLEXArgumentInputNotSupportedView class];
|
||||
}
|
||||
return [[subclass alloc] initWithArgumentTypeEncoding:typeEncoding];
|
||||
}
|
||||
|
||||
+ (Class)argumentInputViewSubclassForTypeEncoding:(const char *)typeEncoding currentValue:(id)currentValue
|
||||
{
|
||||
Class argumentInputViewSubclass = nil;
|
||||
|
||||
// Note that order is important here since multiple subclasses may support the same type.
|
||||
// An example is the number subclass and the bool subclass for the type @encode(BOOL).
|
||||
// Both work, but we'd prefer to use the bool subclass.
|
||||
if ([FLEXArgumentInputColorView supportsObjCType:typeEncoding withCurrentValue:currentValue]) {
|
||||
argumentInputViewSubclass = [FLEXArgumentInputColorView class];
|
||||
} else if ([FLEXArgumentInputFontView supportsObjCType:typeEncoding withCurrentValue:currentValue]) {
|
||||
argumentInputViewSubclass = [FLEXArgumentInputFontView class];
|
||||
} else if ([FLEXArgumentInputStringView supportsObjCType:typeEncoding withCurrentValue:currentValue]) {
|
||||
argumentInputViewSubclass = [FLEXArgumentInputStringView class];
|
||||
} else if ([FLEXArgumentInputStructView supportsObjCType:typeEncoding withCurrentValue:currentValue]) {
|
||||
argumentInputViewSubclass = [FLEXArgumentInputStructView class];
|
||||
} else if ([FLEXArgumentInputSwitchView supportsObjCType:typeEncoding withCurrentValue:currentValue]) {
|
||||
argumentInputViewSubclass = [FLEXArgumentInputSwitchView class];
|
||||
} else if ([FLEXArgumentInputNumberView supportsObjCType:typeEncoding withCurrentValue:currentValue]) {
|
||||
argumentInputViewSubclass = [FLEXArgumentInputNumberView class];
|
||||
} else if ([FLEXArgumentInputJSONObjectView supportsObjCType:typeEncoding withCurrentValue:currentValue]) {
|
||||
argumentInputViewSubclass = [FLEXArgumentInputJSONObjectView class];
|
||||
}
|
||||
|
||||
return argumentInputViewSubclass;
|
||||
}
|
||||
|
||||
+ (BOOL)canEditFieldWithTypeEncoding:(const char *)typeEncoding currentValue:(id)currentValue
|
||||
{
|
||||
return [self argumentInputViewSubclassForTypeEncoding:typeEncoding currentValue:currentValue] != nil;
|
||||
}
|
||||
|
||||
@end
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/30/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputView.h"
|
||||
+61
-79
@@ -3,7 +3,7 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/30/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputColorView.h"
|
||||
@@ -14,8 +14,8 @@
|
||||
|
||||
@interface FLEXColorComponentInputView : UIView
|
||||
|
||||
@property (nonatomic, strong) UISlider *slider;
|
||||
@property (nonatomic, strong) UILabel *valueLabel;
|
||||
@property (nonatomic) UISlider *slider;
|
||||
@property (nonatomic) UILabel *valueLabel;
|
||||
|
||||
@property (nonatomic, weak) id <FLEXColorComponentInputViewDelegate> delegate;
|
||||
|
||||
@@ -30,18 +30,16 @@
|
||||
|
||||
@implementation FLEXColorComponentInputView
|
||||
|
||||
- (id)initWithFrame:(CGRect)frame
|
||||
{
|
||||
- (id)initWithFrame:(CGRect)frame {
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
self.slider = [[UISlider alloc] init];
|
||||
self.slider.backgroundColor = self.backgroundColor;
|
||||
self.slider = [UISlider new];
|
||||
[self.slider addTarget:self action:@selector(sliderChanged:) forControlEvents:UIControlEventValueChanged];
|
||||
[self addSubview:self.slider];
|
||||
|
||||
self.valueLabel = [[UILabel alloc] init];
|
||||
self.valueLabel = [UILabel new];
|
||||
self.valueLabel.backgroundColor = self.backgroundColor;
|
||||
self.valueLabel.font = [FLEXUtility defaultFontOfSize:14.0];
|
||||
self.valueLabel.font = [UIFont systemFontOfSize:14.0];
|
||||
self.valueLabel.textAlignment = NSTextAlignmentRight;
|
||||
[self addSubview:self.valueLabel];
|
||||
|
||||
@@ -50,15 +48,13 @@
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setBackgroundColor:(UIColor *)backgroundColor
|
||||
{
|
||||
- (void)setBackgroundColor:(UIColor *)backgroundColor {
|
||||
[super setBackgroundColor:backgroundColor];
|
||||
self.slider.backgroundColor = backgroundColor;
|
||||
self.valueLabel.backgroundColor = backgroundColor;
|
||||
}
|
||||
|
||||
- (void)layoutSubviews
|
||||
{
|
||||
- (void)layoutSubviews {
|
||||
[super layoutSubviews];
|
||||
|
||||
const CGFloat kValueLabelWidth = 50.0;
|
||||
@@ -73,19 +69,16 @@
|
||||
self.valueLabel.frame = CGRectMake(valueLabelOriginX, valueLabelOriginY, kValueLabelWidth, self.valueLabel.frame.size.height);
|
||||
}
|
||||
|
||||
- (void)sliderChanged:(id)sender
|
||||
{
|
||||
- (void)sliderChanged:(id)sender {
|
||||
[self.delegate colorComponentInputViewValueDidChange:self];
|
||||
[self updateValueLabel];
|
||||
}
|
||||
|
||||
- (void)updateValueLabel
|
||||
{
|
||||
- (void)updateValueLabel {
|
||||
self.valueLabel.text = [NSString stringWithFormat:@"%.3f", self.slider.value];
|
||||
}
|
||||
|
||||
- (CGSize)sizeThatFits:(CGSize)size
|
||||
{
|
||||
- (CGSize)sizeThatFits:(CGSize)size {
|
||||
CGFloat height = [self.slider sizeThatFits:size].height;
|
||||
return CGSizeMake(size.width, height);
|
||||
}
|
||||
@@ -94,52 +87,48 @@
|
||||
|
||||
@interface FLEXColorPreviewBox : UIView
|
||||
|
||||
@property (nonatomic, strong) UIColor *color;
|
||||
@property (nonatomic) UIColor *color;
|
||||
|
||||
@property (nonatomic, strong) UIView *colorOverlayView;
|
||||
@property (nonatomic) UIView *colorOverlayView;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXColorPreviewBox
|
||||
|
||||
- (id)initWithFrame:(CGRect)frame
|
||||
{
|
||||
- (id)initWithFrame:(CGRect)frame {
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
self.layer.borderWidth = 1.0;
|
||||
self.layer.borderColor = [[UIColor blackColor] CGColor];
|
||||
self.layer.borderColor = UIColor.blackColor.CGColor;
|
||||
self.backgroundColor = [UIColor colorWithPatternImage:[[self class] backgroundPatternImage]];
|
||||
|
||||
self.colorOverlayView = [[UIView alloc] initWithFrame:self.bounds];
|
||||
self.colorOverlayView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
|
||||
self.colorOverlayView.backgroundColor = [UIColor clearColor];
|
||||
self.colorOverlayView.backgroundColor = UIColor.clearColor;
|
||||
[self addSubview:self.colorOverlayView];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setColor:(UIColor *)color
|
||||
{
|
||||
- (void)setColor:(UIColor *)color {
|
||||
self.colorOverlayView.backgroundColor = color;
|
||||
}
|
||||
|
||||
- (UIColor *)color
|
||||
{
|
||||
- (UIColor *)color {
|
||||
return self.colorOverlayView.backgroundColor;
|
||||
}
|
||||
|
||||
+ (UIImage *)backgroundPatternImage
|
||||
{
|
||||
+ (UIImage *)backgroundPatternImage {
|
||||
const CGFloat kSquareDimension = 5.0;
|
||||
CGSize squareSize = CGSizeMake(kSquareDimension, kSquareDimension);
|
||||
CGSize imageSize = CGSizeMake(2.0 * kSquareDimension, 2.0 * kSquareDimension);
|
||||
|
||||
UIGraphicsBeginImageContextWithOptions(imageSize, YES, [[UIScreen mainScreen] scale]);
|
||||
UIGraphicsBeginImageContextWithOptions(imageSize, YES, UIScreen.mainScreen.scale);
|
||||
|
||||
[[UIColor whiteColor] setFill];
|
||||
[UIColor.whiteColor setFill];
|
||||
UIRectFill(CGRectMake(0, 0, imageSize.width, imageSize.height));
|
||||
|
||||
[[UIColor grayColor] setFill];
|
||||
[UIColor.grayColor setFill];
|
||||
UIRectFill(CGRectMake(squareSize.width, 0, squareSize.width, squareSize.height));
|
||||
UIRectFill(CGRectMake(0, squareSize.height, squareSize.width, squareSize.height));
|
||||
|
||||
@@ -153,55 +142,53 @@
|
||||
|
||||
@interface FLEXArgumentInputColorView () <FLEXColorComponentInputViewDelegate>
|
||||
|
||||
@property (nonatomic, strong) FLEXColorPreviewBox *colorPreviewBox;
|
||||
@property (nonatomic, strong) UILabel *hexLabel;
|
||||
@property (nonatomic, strong) FLEXColorComponentInputView *alphaInput;
|
||||
@property (nonatomic, strong) FLEXColorComponentInputView *redInput;
|
||||
@property (nonatomic, strong) FLEXColorComponentInputView *greenInput;
|
||||
@property (nonatomic, strong) FLEXColorComponentInputView *blueInput;
|
||||
@property (nonatomic) FLEXColorPreviewBox *colorPreviewBox;
|
||||
@property (nonatomic) UILabel *hexLabel;
|
||||
@property (nonatomic) FLEXColorComponentInputView *alphaInput;
|
||||
@property (nonatomic) FLEXColorComponentInputView *redInput;
|
||||
@property (nonatomic) FLEXColorComponentInputView *greenInput;
|
||||
@property (nonatomic) FLEXColorComponentInputView *blueInput;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXArgumentInputColorView
|
||||
|
||||
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding
|
||||
{
|
||||
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
|
||||
self = [super initWithArgumentTypeEncoding:typeEncoding];
|
||||
if (self) {
|
||||
self.colorPreviewBox = [[FLEXColorPreviewBox alloc] init];
|
||||
self.colorPreviewBox = [FLEXColorPreviewBox new];
|
||||
[self addSubview:self.colorPreviewBox];
|
||||
|
||||
self.hexLabel = [[UILabel alloc] init];
|
||||
self.hexLabel = [UILabel new];
|
||||
self.hexLabel.backgroundColor = [UIColor colorWithWhite:1.0 alpha:0.9];
|
||||
self.hexLabel.textAlignment = NSTextAlignmentCenter;
|
||||
self.hexLabel.font = [FLEXUtility defaultFontOfSize:12.0];
|
||||
self.hexLabel.font = [UIFont systemFontOfSize:12.0];
|
||||
[self addSubview:self.hexLabel];
|
||||
|
||||
self.alphaInput = [[FLEXColorComponentInputView alloc] init];
|
||||
self.alphaInput.slider.minimumTrackTintColor = [UIColor blackColor];
|
||||
self.alphaInput = [FLEXColorComponentInputView new];
|
||||
self.alphaInput.slider.minimumTrackTintColor = UIColor.blackColor;
|
||||
self.alphaInput.delegate = self;
|
||||
[self addSubview:self.alphaInput];
|
||||
|
||||
self.redInput = [[FLEXColorComponentInputView alloc] init];
|
||||
self.redInput.slider.minimumTrackTintColor = [UIColor redColor];
|
||||
self.redInput = [FLEXColorComponentInputView new];
|
||||
self.redInput.slider.minimumTrackTintColor = UIColor.redColor;
|
||||
self.redInput.delegate = self;
|
||||
[self addSubview:self.redInput];
|
||||
|
||||
self.greenInput = [[FLEXColorComponentInputView alloc] init];
|
||||
self.greenInput.slider.minimumTrackTintColor = [UIColor greenColor];
|
||||
self.greenInput = [FLEXColorComponentInputView new];
|
||||
self.greenInput.slider.minimumTrackTintColor = UIColor.greenColor;
|
||||
self.greenInput.delegate = self;
|
||||
[self addSubview:self.greenInput];
|
||||
|
||||
self.blueInput = [[FLEXColorComponentInputView alloc] init];
|
||||
self.blueInput.slider.minimumTrackTintColor = [UIColor blueColor];
|
||||
self.blueInput = [FLEXColorComponentInputView new];
|
||||
self.blueInput.slider.minimumTrackTintColor = UIColor.blueColor;
|
||||
self.blueInput.delegate = self;
|
||||
[self addSubview:self.blueInput];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setBackgroundColor:(UIColor *)backgroundColor
|
||||
{
|
||||
- (void)setBackgroundColor:(UIColor *)backgroundColor {
|
||||
[super setBackgroundColor:backgroundColor];
|
||||
self.alphaInput.backgroundColor = backgroundColor;
|
||||
self.redInput.backgroundColor = backgroundColor;
|
||||
@@ -209,8 +196,7 @@
|
||||
self.blueInput.backgroundColor = backgroundColor;
|
||||
}
|
||||
|
||||
- (void)layoutSubviews
|
||||
{
|
||||
- (void)layoutSubviews {
|
||||
[super layoutSubviews];
|
||||
|
||||
CGFloat runningOriginY = 0;
|
||||
@@ -221,14 +207,14 @@
|
||||
|
||||
[self.hexLabel sizeToFit];
|
||||
const CGFloat kLabelVerticalOutsetAmount = 0.0;
|
||||
const CGFloat kLabelHorizonalOutsetAmount = 2.0;
|
||||
UIEdgeInsets labelOutset = UIEdgeInsetsMake(-kLabelVerticalOutsetAmount, -kLabelHorizonalOutsetAmount, -kLabelVerticalOutsetAmount, -kLabelHorizonalOutsetAmount);
|
||||
const CGFloat kLabelHorizontalOutsetAmount = 2.0;
|
||||
UIEdgeInsets labelOutset = UIEdgeInsetsMake(-kLabelVerticalOutsetAmount, -kLabelHorizontalOutsetAmount, -kLabelVerticalOutsetAmount, -kLabelHorizontalOutsetAmount);
|
||||
self.hexLabel.frame = UIEdgeInsetsInsetRect(self.hexLabel.frame, labelOutset);
|
||||
CGFloat hexLabelOriginX = self.colorPreviewBox.layer.borderWidth;
|
||||
CGFloat hexLabelOriginY = CGRectGetMaxY(self.colorPreviewBox.frame) - self.colorPreviewBox.layer.borderWidth - self.hexLabel.frame.size.height;
|
||||
self.hexLabel.frame = CGRectMake(hexLabelOriginX, hexLabelOriginY, self.hexLabel.frame.size.width, self.hexLabel.frame.size.height);
|
||||
|
||||
NSArray *colorComponentInputViews = @[self.alphaInput, self.redInput, self.greenInput, self.blueInput];
|
||||
NSArray<FLEXColorComponentInputView *> *colorComponentInputViews = @[self.alphaInput, self.redInput, self.greenInput, self.blueInput];
|
||||
for (FLEXColorComponentInputView *inputView in colorComponentInputViews) {
|
||||
CGSize fitSize = [inputView sizeThatFits:constrainSize];
|
||||
inputView.frame = CGRectMake(0, runningOriginY, fitSize.width, fitSize.height);
|
||||
@@ -236,8 +222,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setInputValue:(id)inputValue
|
||||
{
|
||||
- (void)setInputValue:(id)inputValue {
|
||||
if ([inputValue isKindOfClass:[UIColor class]]) {
|
||||
[self updateWithColor:inputValue];
|
||||
} else if ([inputValue isKindOfClass:[NSValue class]]) {
|
||||
@@ -248,21 +233,20 @@
|
||||
UIColor *color = [[UIColor alloc] initWithCGColor:colorRef];
|
||||
[self updateWithColor:color];
|
||||
}
|
||||
} else {
|
||||
[self updateWithColor:UIColor.clearColor];
|
||||
}
|
||||
}
|
||||
|
||||
- (id)inputValue
|
||||
{
|
||||
- (id)inputValue {
|
||||
return [UIColor colorWithRed:self.redInput.slider.value green:self.greenInput.slider.value blue:self.blueInput.slider.value alpha:self.alphaInput.slider.value];
|
||||
}
|
||||
|
||||
- (void)colorComponentInputViewValueDidChange:(FLEXColorComponentInputView *)colorComponentInputView
|
||||
{
|
||||
- (void)colorComponentInputViewValueDidChange:(FLEXColorComponentInputView *)colorComponentInputView {
|
||||
[self updateColorPreview];
|
||||
}
|
||||
|
||||
- (void)updateWithColor:(UIColor *)color
|
||||
{
|
||||
- (void)updateWithColor:(UIColor *)color {
|
||||
CGFloat red, green, blue, white, alpha;
|
||||
if ([color getRed:&red green:&green blue:&blue alpha:&alpha]) {
|
||||
self.alphaInput.slider.value = alpha;
|
||||
@@ -286,8 +270,7 @@
|
||||
[self updateColorPreview];
|
||||
}
|
||||
|
||||
- (void)updateColorPreview
|
||||
{
|
||||
- (void)updateColorPreview {
|
||||
self.colorPreviewBox.color = self.inputValue;
|
||||
unsigned char redByte = self.redInput.slider.value * 255;
|
||||
unsigned char greenByte = self.greenInput.slider.value * 255;
|
||||
@@ -296,8 +279,7 @@
|
||||
[self setNeedsLayout];
|
||||
}
|
||||
|
||||
- (CGSize)sizeThatFits:(CGSize)size
|
||||
{
|
||||
- (CGSize)sizeThatFits:(CGSize)size {
|
||||
CGFloat height = 0;
|
||||
height += [[self class] colorPreviewBoxHeight];
|
||||
height += [[self class] inputViewVerticalPadding];
|
||||
@@ -311,19 +293,19 @@
|
||||
return CGSizeMake(size.width, height);
|
||||
}
|
||||
|
||||
+ (CGFloat)inputViewVerticalPadding
|
||||
{
|
||||
+ (CGFloat)inputViewVerticalPadding {
|
||||
return 10.0;
|
||||
}
|
||||
|
||||
+ (CGFloat)colorPreviewBoxHeight
|
||||
{
|
||||
+ (CGFloat)colorPreviewBoxHeight {
|
||||
return 40.0;
|
||||
}
|
||||
|
||||
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value
|
||||
{
|
||||
return (type && (strcmp(type, @encode(CGColorRef)) == 0 || strcmp(type, FLEXEncodeClass(UIColor)) == 0)) || [value isKindOfClass:[UIColor class]];
|
||||
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value {
|
||||
NSParameterAssert(type);
|
||||
|
||||
// We don't care if currentValue is a color or not; we will default to +clearColor
|
||||
return (strcmp(type, @encode(CGColorRef)) == 0) || (strcmp(type, FLEXEncodeClass(UIColor)) == 0);
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,13 @@
|
||||
//
|
||||
// FLEXArgumentInputDataView.h
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Daniel Rodriguez Troitino on 2/14/15.
|
||||
// Copyright (c) 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputView.h"
|
||||
|
||||
@interface FLEXArgumentInputDateView : FLEXArgumentInputView
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,58 @@
|
||||
//
|
||||
// FLEXArgumentInputDataView.m
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Daniel Rodriguez Troitino on 2/14/15.
|
||||
// Copyright (c) 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputDateView.h"
|
||||
#import "FLEXRuntimeUtility.h"
|
||||
|
||||
@interface FLEXArgumentInputDateView ()
|
||||
|
||||
@property (nonatomic) UIDatePicker *datePicker;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXArgumentInputDateView
|
||||
|
||||
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
|
||||
self = [super initWithArgumentTypeEncoding:typeEncoding];
|
||||
if (self) {
|
||||
self.datePicker = [UIDatePicker new];
|
||||
self.datePicker.datePickerMode = UIDatePickerModeDateAndTime;
|
||||
// Using UTC, because that's what the NSDate description prints
|
||||
self.datePicker.calendar = [NSCalendar calendarWithIdentifier:NSCalendarIdentifierGregorian];
|
||||
self.datePicker.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"UTC"];
|
||||
[self addSubview:self.datePicker];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setInputValue:(id)inputValue {
|
||||
if ([inputValue isKindOfClass:[NSDate class]]) {
|
||||
self.datePicker.date = inputValue;
|
||||
}
|
||||
}
|
||||
|
||||
- (id)inputValue {
|
||||
return self.datePicker.date;
|
||||
}
|
||||
|
||||
- (void)layoutSubviews {
|
||||
[super layoutSubviews];
|
||||
self.datePicker.frame = self.bounds;
|
||||
}
|
||||
|
||||
- (CGSize)sizeThatFits:(CGSize)size {
|
||||
CGFloat height = [self.datePicker sizeThatFits:size].height;
|
||||
return CGSizeMake(size.width, height);
|
||||
}
|
||||
|
||||
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value {
|
||||
NSParameterAssert(type);
|
||||
return strcmp(type, FLEXEncodeClass(NSDate)) == 0;
|
||||
}
|
||||
|
||||
@end
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/28/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputView.h"
|
||||
+14
-26
@@ -3,7 +3,7 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/28/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputFontView.h"
|
||||
@@ -13,25 +13,22 @@
|
||||
|
||||
@interface FLEXArgumentInputFontView ()
|
||||
|
||||
@property (nonatomic, strong) FLEXArgumentInputView *fontNameInput;
|
||||
@property (nonatomic, strong) FLEXArgumentInputView *pointSizeInput;
|
||||
@property (nonatomic) FLEXArgumentInputView *fontNameInput;
|
||||
@property (nonatomic) FLEXArgumentInputView *pointSizeInput;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXArgumentInputFontView
|
||||
|
||||
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding
|
||||
{
|
||||
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
|
||||
self = [super initWithArgumentTypeEncoding:typeEncoding];
|
||||
if (self) {
|
||||
self.fontNameInput = [[FLEXArgumentInputFontsPickerView alloc] initWithArgumentTypeEncoding:FLEXEncodeClass(NSString)];
|
||||
self.fontNameInput.backgroundColor = self.backgroundColor;
|
||||
self.fontNameInput.targetSize = FLEXArgumentInputViewSizeSmall;
|
||||
self.fontNameInput.title = @"Font Name:";
|
||||
[self addSubview:self.fontNameInput];
|
||||
|
||||
self.pointSizeInput = [FLEXArgumentInputViewFactory argumentInputViewForTypeEncoding:@encode(CGFloat)];
|
||||
self.pointSizeInput.backgroundColor = self.backgroundColor;
|
||||
self.pointSizeInput.targetSize = FLEXArgumentInputViewSizeSmall;
|
||||
self.pointSizeInput.title = @"Point Size:";
|
||||
[self addSubview:self.pointSizeInput];
|
||||
@@ -39,15 +36,13 @@
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setBackgroundColor:(UIColor *)backgroundColor
|
||||
{
|
||||
- (void)setBackgroundColor:(UIColor *)backgroundColor {
|
||||
[super setBackgroundColor:backgroundColor];
|
||||
self.fontNameInput.backgroundColor = backgroundColor;
|
||||
self.pointSizeInput.backgroundColor = backgroundColor;
|
||||
}
|
||||
|
||||
- (void)setInputValue:(id)inputValue
|
||||
{
|
||||
- (void)setInputValue:(id)inputValue {
|
||||
if ([inputValue isKindOfClass:[UIFont class]]) {
|
||||
UIFont *font = (UIFont *)inputValue;
|
||||
self.fontNameInput.inputValue = font.fontName;
|
||||
@@ -55,8 +50,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
- (id)inputValue
|
||||
{
|
||||
- (id)inputValue {
|
||||
CGFloat pointSize = 0;
|
||||
if ([self.pointSizeInput.inputValue isKindOfClass:[NSValue class]]) {
|
||||
NSValue *pointSizeValue = (NSValue *)self.pointSizeInput.inputValue;
|
||||
@@ -67,16 +61,14 @@
|
||||
return [UIFont fontWithName:self.fontNameInput.inputValue size:pointSize];
|
||||
}
|
||||
|
||||
- (BOOL)inputViewIsFirstResponder
|
||||
{
|
||||
- (BOOL)inputViewIsFirstResponder {
|
||||
return [self.fontNameInput inputViewIsFirstResponder] || [self.pointSizeInput inputViewIsFirstResponder];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Layout and Sizing
|
||||
|
||||
- (void)layoutSubviews
|
||||
{
|
||||
- (void)layoutSubviews {
|
||||
[super layoutSubviews];
|
||||
|
||||
CGFloat runningOriginY = self.topInputFieldVerticalLayoutGuide;
|
||||
@@ -89,13 +81,11 @@
|
||||
self.pointSizeInput.frame = CGRectMake(0, runningOriginY, pointSizeFitSize.width, pointSizeFitSize.height);
|
||||
}
|
||||
|
||||
+ (CGFloat)verticalPaddingBetweenFields
|
||||
{
|
||||
+ (CGFloat)verticalPaddingBetweenFields {
|
||||
return 10.0;
|
||||
}
|
||||
|
||||
- (CGSize)sizeThatFits:(CGSize)size
|
||||
{
|
||||
- (CGSize)sizeThatFits:(CGSize)size {
|
||||
CGSize fitSize = [super sizeThatFits:size];
|
||||
|
||||
CGSize constrainSize = CGSizeMake(size.width, CGFLOAT_MAX);
|
||||
@@ -111,11 +101,9 @@
|
||||
|
||||
#pragma mark -
|
||||
|
||||
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value
|
||||
{
|
||||
BOOL supported = type && strcmp(type, FLEXEncodeClass(UIFont)) == 0;
|
||||
supported = supported || (value && [value isKindOfClass:[UIFont class]]);
|
||||
return supported;
|
||||
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value {
|
||||
NSParameterAssert(type);
|
||||
return strcmp(type, FLEXEncodeClass(UIFont)) == 0;
|
||||
}
|
||||
|
||||
@end
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
//
|
||||
// FLEXArgumentInputFontsPickerView.h
|
||||
// UICatalog
|
||||
// FLEX
|
||||
//
|
||||
// Created by 啟倫 陳 on 2014/7/27.
|
||||
// Copyright (c) 2014年 f. All rights reserved.
|
||||
+18
-27
@@ -1,6 +1,6 @@
|
||||
//
|
||||
// FLEXArgumentInputFontsPickerView.m
|
||||
// UICatalog
|
||||
// FLEX
|
||||
//
|
||||
// Created by 啟倫 陳 on 2014/7/27.
|
||||
// Copyright (c) 2014年 f. All rights reserved.
|
||||
@@ -11,15 +11,14 @@
|
||||
|
||||
@interface FLEXArgumentInputFontsPickerView ()
|
||||
|
||||
@property (nonatomic, strong) NSMutableArray *availableFonts;
|
||||
@property (nonatomic) NSMutableArray<NSString *> *availableFonts;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation FLEXArgumentInputFontsPickerView
|
||||
|
||||
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding
|
||||
{
|
||||
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
|
||||
self = [super initWithArgumentTypeEncoding:typeEncoding];
|
||||
if (self) {
|
||||
self.targetSize = FLEXArgumentInputViewSizeSmall;
|
||||
@@ -29,24 +28,21 @@
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setInputValue:(id)inputValue
|
||||
{
|
||||
- (void)setInputValue:(id)inputValue {
|
||||
self.inputTextView.text = inputValue;
|
||||
if ([self.availableFonts indexOfObject:inputValue] == NSNotFound) {
|
||||
[self.availableFonts insertObject:inputValue atIndex:0];
|
||||
}
|
||||
[(UIPickerView*)self.inputTextView.inputView selectRow:[self.availableFonts indexOfObject:inputValue] inComponent:0 animated:NO];
|
||||
[(UIPickerView *)self.inputTextView.inputView selectRow:[self.availableFonts indexOfObject:inputValue] inComponent:0 animated:NO];
|
||||
}
|
||||
|
||||
- (id)inputValue
|
||||
{
|
||||
return [self.inputTextView.text length] > 0 ? [self.inputTextView.text copy] : nil;
|
||||
- (id)inputValue {
|
||||
return self.inputTextView.text.length > 0 ? [self.inputTextView.text copy] : nil;
|
||||
}
|
||||
|
||||
#pragma mark - private
|
||||
|
||||
- (UIPickerView*)createFontsPicker
|
||||
{
|
||||
- (UIPickerView*)createFontsPicker {
|
||||
UIPickerView *fontsPicker = [UIPickerView new];
|
||||
fontsPicker.dataSource = self;
|
||||
fontsPicker.delegate = self;
|
||||
@@ -54,10 +50,9 @@
|
||||
return fontsPicker;
|
||||
}
|
||||
|
||||
- (void)createAvailableFonts
|
||||
{
|
||||
NSMutableArray *unsortedFontsArray = [NSMutableArray array];
|
||||
for (NSString *eachFontFamily in [UIFont familyNames]) {
|
||||
- (void)createAvailableFonts {
|
||||
NSMutableArray<NSString *> *unsortedFontsArray = [NSMutableArray new];
|
||||
for (NSString *eachFontFamily in UIFont.familyNames) {
|
||||
for (NSString *eachFontName in [UIFont fontNamesForFamilyName:eachFontFamily]) {
|
||||
[unsortedFontsArray addObject:eachFontName];
|
||||
}
|
||||
@@ -67,38 +62,34 @@
|
||||
|
||||
#pragma mark - UIPickerViewDataSource
|
||||
|
||||
- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView
|
||||
{
|
||||
- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView {
|
||||
return 1;
|
||||
}
|
||||
|
||||
- (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component
|
||||
{
|
||||
return [self.availableFonts count];
|
||||
- (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component {
|
||||
return self.availableFonts.count;
|
||||
}
|
||||
|
||||
#pragma mark - UIPickerViewDelegate
|
||||
|
||||
- (UIView *)pickerView:(UIPickerView *)pickerView viewForRow:(NSInteger)row forComponent:(NSInteger)component reusingView:(UIView *)view
|
||||
{
|
||||
- (UIView *)pickerView:(UIPickerView *)pickerView viewForRow:(NSInteger)row forComponent:(NSInteger)component reusingView:(UIView *)view {
|
||||
UILabel *fontLabel;
|
||||
if (!view) {
|
||||
fontLabel = [UILabel new];
|
||||
fontLabel.backgroundColor = [UIColor clearColor];
|
||||
fontLabel.backgroundColor = UIColor.clearColor;
|
||||
fontLabel.textAlignment = NSTextAlignmentCenter;
|
||||
} else {
|
||||
fontLabel = (UILabel*)view;
|
||||
}
|
||||
UIFont *font = [UIFont fontWithName:self.availableFonts[row] size:15.0];
|
||||
NSDictionary *attributesDictionary = [NSDictionary dictionaryWithObject:font forKey:NSFontAttributeName];
|
||||
NSDictionary<NSString *, id> *attributesDictionary = [NSDictionary<NSString *, id> dictionaryWithObject:font forKey:NSFontAttributeName];
|
||||
NSAttributedString *attributesString = [[NSAttributedString alloc] initWithString:self.availableFonts[row] attributes:attributesDictionary];
|
||||
fontLabel.attributedText = attributesString;
|
||||
[fontLabel sizeToFit];
|
||||
return fontLabel;
|
||||
}
|
||||
|
||||
- (void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component
|
||||
{
|
||||
- (void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component {
|
||||
self.inputTextView.text = self.availableFonts[row];
|
||||
}
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/18/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputTextView.h"
|
||||
+5
-5
@@ -3,20 +3,20 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/18/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputNotSupportedView.h"
|
||||
#import "FLEXColor.h"
|
||||
|
||||
@implementation FLEXArgumentInputNotSupportedView
|
||||
|
||||
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding
|
||||
{
|
||||
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
|
||||
self = [super initWithArgumentTypeEncoding:typeEncoding];
|
||||
if (self) {
|
||||
self.inputTextView.userInteractionEnabled = NO;
|
||||
self.inputTextView.backgroundColor = [UIColor colorWithWhite:0.8 alpha:1.0];
|
||||
self.inputTextView.text = @"nil";
|
||||
self.inputTextView.backgroundColor = [FLEXColor secondaryGroupedBackgroundColorWithAlpha:0.5];
|
||||
self.inputPlaceholderText = @"nil (type not supported)";
|
||||
self.targetSize = FLEXArgumentInputViewSizeSmall;
|
||||
}
|
||||
return self;
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/15/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputTextView.h"
|
||||
@@ -0,0 +1,62 @@
|
||||
//
|
||||
// FLEXArgumentInputNumberView.m
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/15/14.
|
||||
// Copyright (c) 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputNumberView.h"
|
||||
#import "FLEXRuntimeUtility.h"
|
||||
|
||||
@implementation FLEXArgumentInputNumberView
|
||||
|
||||
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
|
||||
self = [super initWithArgumentTypeEncoding:typeEncoding];
|
||||
if (self) {
|
||||
self.inputTextView.keyboardType = UIKeyboardTypeNumbersAndPunctuation;
|
||||
self.targetSize = FLEXArgumentInputViewSizeSmall;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setInputValue:(id)inputValue {
|
||||
if ([inputValue respondsToSelector:@selector(stringValue)]) {
|
||||
self.inputTextView.text = [inputValue stringValue];
|
||||
}
|
||||
}
|
||||
|
||||
- (id)inputValue {
|
||||
return [FLEXRuntimeUtility valueForNumberWithObjCType:self.typeEncoding.UTF8String fromInputString:self.inputTextView.text];
|
||||
}
|
||||
|
||||
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value {
|
||||
NSParameterAssert(type);
|
||||
|
||||
static NSArray<NSString *> *supportedTypes = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
supportedTypes = @[
|
||||
@FLEXEncodeClass(NSNumber),
|
||||
@FLEXEncodeClass(NSDecimalNumber),
|
||||
@(@encode(char)),
|
||||
@(@encode(int)),
|
||||
@(@encode(short)),
|
||||
@(@encode(long)),
|
||||
@(@encode(long long)),
|
||||
@(@encode(unsigned char)),
|
||||
@(@encode(unsigned int)),
|
||||
@(@encode(unsigned short)),
|
||||
@(@encode(unsigned long)),
|
||||
@(@encode(unsigned long long)),
|
||||
@(@encode(float)),
|
||||
@(@encode(double)),
|
||||
@(@encode(long double))
|
||||
];
|
||||
});
|
||||
|
||||
return type && [supportedTypes containsObject:@(type)];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,13 @@
|
||||
//
|
||||
// FLEXArgumentInputObjectView.h
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/15/14.
|
||||
// Copyright (c) 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputTextView.h"
|
||||
|
||||
@interface FLEXArgumentInputObjectView : FLEXArgumentInputTextView
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,232 @@
|
||||
//
|
||||
// FLEXArgumentInputJSONObjectView.m
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/15/14.
|
||||
// Copyright (c) 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputObjectView.h"
|
||||
#import "FLEXRuntimeUtility.h"
|
||||
|
||||
static const CGFloat kSegmentInputMargin = 10;
|
||||
|
||||
typedef NS_ENUM(NSUInteger, FLEXArgInputObjectType) {
|
||||
FLEXArgInputObjectTypeJSON,
|
||||
FLEXArgInputObjectTypeAddress
|
||||
};
|
||||
|
||||
@interface FLEXArgumentInputObjectView ()
|
||||
|
||||
@property (nonatomic) UISegmentedControl *objectTypeSegmentControl;
|
||||
@property (nonatomic) FLEXArgInputObjectType inputType;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXArgumentInputObjectView
|
||||
|
||||
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
|
||||
self = [super initWithArgumentTypeEncoding:typeEncoding];
|
||||
if (self) {
|
||||
// Start with the numbers and punctuation keyboard since quotes, curly braces, or
|
||||
// square brackets are likely to be the first characters type for the JSON.
|
||||
self.inputTextView.keyboardType = UIKeyboardTypeNumbersAndPunctuation;
|
||||
self.targetSize = FLEXArgumentInputViewSizeLarge;
|
||||
|
||||
self.objectTypeSegmentControl = [[UISegmentedControl alloc] initWithItems:@[@"Value", @"Address"]];
|
||||
[self.objectTypeSegmentControl addTarget:self action:@selector(didChangeType) forControlEvents:UIControlEventValueChanged];
|
||||
self.objectTypeSegmentControl.selectedSegmentIndex = 0;
|
||||
[self addSubview:self.objectTypeSegmentControl];
|
||||
|
||||
self.inputType = [[self class] preferredDefaultTypeForObjCType:typeEncoding withCurrentValue:nil];
|
||||
self.objectTypeSegmentControl.selectedSegmentIndex = self.inputType;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)didChangeType {
|
||||
self.inputType = self.objectTypeSegmentControl.selectedSegmentIndex;
|
||||
|
||||
if (super.inputValue) {
|
||||
// Trigger an update to the text field to show
|
||||
// the address of the stored object we were given,
|
||||
// or to show a JSON representation of the object
|
||||
[self populateTextAreaFromValue:super.inputValue];
|
||||
} else {
|
||||
// Clear the text field
|
||||
[self populateTextAreaFromValue:nil];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setInputType:(FLEXArgInputObjectType)inputType {
|
||||
if (_inputType == inputType) return;
|
||||
|
||||
_inputType = inputType;
|
||||
|
||||
// Resize input view
|
||||
switch (inputType) {
|
||||
case FLEXArgInputObjectTypeJSON:
|
||||
self.targetSize = FLEXArgumentInputViewSizeLarge;
|
||||
break;
|
||||
case FLEXArgInputObjectTypeAddress:
|
||||
self.targetSize = FLEXArgumentInputViewSizeSmall;
|
||||
break;
|
||||
}
|
||||
|
||||
// Change placeholder
|
||||
switch (inputType) {
|
||||
case FLEXArgInputObjectTypeJSON:
|
||||
self.inputPlaceholderText =
|
||||
@"You can put any valid JSON here, such as a string, number, array, or dictionary:"
|
||||
"\n\"This is a string\""
|
||||
"\n1234"
|
||||
"\n{ \"name\": \"Bob\", \"age\": 47 }"
|
||||
"\n["
|
||||
"\n 1, 2, 3"
|
||||
"\n]";
|
||||
break;
|
||||
case FLEXArgInputObjectTypeAddress:
|
||||
self.inputPlaceholderText = @"0x0000deadb33f";
|
||||
break;
|
||||
}
|
||||
|
||||
[self setNeedsLayout];
|
||||
[self.superview setNeedsLayout];
|
||||
}
|
||||
|
||||
- (void)setInputValue:(id)inputValue {
|
||||
super.inputValue = inputValue;
|
||||
[self populateTextAreaFromValue:inputValue];
|
||||
}
|
||||
|
||||
- (id)inputValue {
|
||||
switch (self.inputType) {
|
||||
case FLEXArgInputObjectTypeJSON:
|
||||
return [FLEXRuntimeUtility objectValueFromEditableJSONString:self.inputTextView.text];
|
||||
case FLEXArgInputObjectTypeAddress: {
|
||||
NSScanner *scanner = [NSScanner scannerWithString:self.inputTextView.text];
|
||||
|
||||
unsigned long long objectPointerValue;
|
||||
if ([scanner scanHexLongLong:&objectPointerValue]) {
|
||||
return (__bridge id)(void *)objectPointerValue;
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)populateTextAreaFromValue:(id)value {
|
||||
if (!value) {
|
||||
self.inputTextView.text = nil;
|
||||
} else {
|
||||
if (self.inputType == FLEXArgInputObjectTypeJSON) {
|
||||
self.inputTextView.text = [FLEXRuntimeUtility editableJSONStringForObject:value];
|
||||
} else if (self.inputType == FLEXArgInputObjectTypeAddress) {
|
||||
self.inputTextView.text = [NSString stringWithFormat:@"%p", value];
|
||||
}
|
||||
}
|
||||
|
||||
// Delegate methods are not called for programmatic changes
|
||||
[self textViewDidChange:self.inputTextView];
|
||||
}
|
||||
|
||||
- (CGSize)sizeThatFits:(CGSize)size {
|
||||
CGSize fitSize = [super sizeThatFits:size];
|
||||
fitSize.height += [self.objectTypeSegmentControl sizeThatFits:size].height + kSegmentInputMargin;
|
||||
|
||||
return fitSize;
|
||||
}
|
||||
|
||||
- (void)layoutSubviews {
|
||||
CGFloat segmentHeight = [self.objectTypeSegmentControl sizeThatFits:self.frame.size].height;
|
||||
self.objectTypeSegmentControl.frame = CGRectMake(
|
||||
0.0,
|
||||
// Our segmented control is taking the position
|
||||
// of the text view, as far as super is concerned,
|
||||
// and we override this property to be different
|
||||
super.topInputFieldVerticalLayoutGuide,
|
||||
self.frame.size.width,
|
||||
segmentHeight
|
||||
);
|
||||
|
||||
[super layoutSubviews];
|
||||
}
|
||||
|
||||
- (CGFloat)topInputFieldVerticalLayoutGuide {
|
||||
// Our text view is offset from the segmented control
|
||||
CGFloat segmentHeight = [self.objectTypeSegmentControl sizeThatFits:self.frame.size].height;
|
||||
return segmentHeight + super.topInputFieldVerticalLayoutGuide + kSegmentInputMargin;
|
||||
}
|
||||
|
||||
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value {
|
||||
NSParameterAssert(type);
|
||||
// Must be object type
|
||||
return type[0] == FLEXTypeEncodingObjcObject || type[0] == FLEXTypeEncodingObjcClass;
|
||||
}
|
||||
|
||||
+ (FLEXArgInputObjectType)preferredDefaultTypeForObjCType:(const char *)type withCurrentValue:(id)value {
|
||||
NSParameterAssert(type[0] == FLEXTypeEncodingObjcObject || type[0] == FLEXTypeEncodingObjcClass);
|
||||
|
||||
if (value) {
|
||||
// If there's a current value, it must be serializable to JSON
|
||||
// to display the JSON editor. Otherwise display the address field.
|
||||
if ([FLEXRuntimeUtility editableJSONStringForObject:value]) {
|
||||
return FLEXArgInputObjectTypeJSON;
|
||||
} else {
|
||||
return FLEXArgInputObjectTypeAddress;
|
||||
}
|
||||
} else {
|
||||
// Otherwise, see if we have more type information than just 'id'.
|
||||
// If we do, make sure the encoding is something serializable to JSON.
|
||||
// Properties and ivars keep more detailed type encoding information than method arguments.
|
||||
if (strcmp(type, @encode(id)) != 0) {
|
||||
BOOL isJSONSerializableType = NO;
|
||||
|
||||
// Parse class name out of the string,
|
||||
// which is in the form `@"ClassName"`
|
||||
Class cls = NSClassFromString(({
|
||||
NSString *className = nil;
|
||||
NSScanner *scan = [NSScanner scannerWithString:@(type)];
|
||||
NSCharacterSet *allowed = [NSCharacterSet
|
||||
characterSetWithCharactersInString:@"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_$"
|
||||
];
|
||||
|
||||
// Skip over the @" then scan the name
|
||||
if ([scan scanString:@"@\"" intoString:nil]) {
|
||||
[scan scanCharactersFromSet:allowed intoString:&className];
|
||||
}
|
||||
|
||||
className;
|
||||
}));
|
||||
|
||||
// Note: we can't use @encode(NSString) here because that drops
|
||||
// the class information and just goes to @encode(id).
|
||||
NSArray<Class> *jsonTypes = @[
|
||||
[NSString class],
|
||||
[NSNumber class],
|
||||
[NSArray class],
|
||||
[NSDictionary class],
|
||||
];
|
||||
|
||||
// Look for matching types
|
||||
for (Class jsonClass in jsonTypes) {
|
||||
if ([cls isSubclassOfClass:jsonClass]) {
|
||||
isJSONSerializableType = YES;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (isJSONSerializableType) {
|
||||
return FLEXArgInputObjectTypeJSON;
|
||||
} else {
|
||||
return FLEXArgInputObjectTypeAddress;
|
||||
}
|
||||
} else {
|
||||
return FLEXArgInputObjectTypeAddress;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/28/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputTextView.h"
|
||||
@@ -0,0 +1,129 @@
|
||||
//
|
||||
// FLEXArgumentInputStringView.m
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/28/14.
|
||||
// Copyright (c) 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputStringView.h"
|
||||
#import "FLEXRuntimeUtility.h"
|
||||
|
||||
@implementation FLEXArgumentInputStringView
|
||||
|
||||
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
|
||||
self = [super initWithArgumentTypeEncoding:typeEncoding];
|
||||
if (self) {
|
||||
FLEXTypeEncoding type = typeEncoding[0];
|
||||
if (type == FLEXTypeEncodingConst) {
|
||||
// A crash here would mean an invalid type encoding string
|
||||
type = typeEncoding[1];
|
||||
}
|
||||
|
||||
// Selectors don't need a multi-line text box
|
||||
if (type == FLEXTypeEncodingSelector) {
|
||||
self.targetSize = FLEXArgumentInputViewSizeSmall;
|
||||
} else {
|
||||
self.targetSize = FLEXArgumentInputViewSizeLarge;
|
||||
}
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setInputValue:(id)inputValue {
|
||||
if ([inputValue isKindOfClass:[NSString class]]) {
|
||||
self.inputTextView.text = inputValue;
|
||||
} else if ([inputValue isKindOfClass:[NSValue class]]) {
|
||||
NSValue *value = (id)inputValue;
|
||||
NSParameterAssert(strlen(value.objCType) == 1);
|
||||
|
||||
// C-String or SEL from NSValue
|
||||
FLEXTypeEncoding type = value.objCType[0];
|
||||
if (type == FLEXTypeEncodingConst) {
|
||||
// A crash here would mean an invalid type encoding string
|
||||
type = value.objCType[1];
|
||||
}
|
||||
|
||||
if (type == FLEXTypeEncodingCString) {
|
||||
self.inputTextView.text = @((const char *)value.pointerValue);
|
||||
} else if (type == FLEXTypeEncodingSelector) {
|
||||
self.inputTextView.text = NSStringFromSelector((SEL)value.pointerValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (id)inputValue {
|
||||
NSString *text = self.inputTextView.text;
|
||||
// Interpret empty string as nil. We loose the ability to set empty string as a string value,
|
||||
// but we accept that tradeoff in exchange for not having to type quotes for every string.
|
||||
if (!text.length) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
// Case: C-strings and SELs
|
||||
if (self.typeEncoding.length <= 2) {
|
||||
FLEXTypeEncoding type = [self.typeEncoding characterAtIndex:0];
|
||||
if (type == FLEXTypeEncodingConst) {
|
||||
// A crash here would mean an invalid type encoding string
|
||||
type = [self.typeEncoding characterAtIndex:1];
|
||||
}
|
||||
|
||||
if (type == FLEXTypeEncodingCString || type == FLEXTypeEncodingSelector) {
|
||||
const char *encoding = self.typeEncoding.UTF8String;
|
||||
SEL selector = NSSelectorFromString(text);
|
||||
return [NSValue valueWithBytes:&selector objCType:encoding];
|
||||
}
|
||||
}
|
||||
|
||||
// Case: NSStrings
|
||||
return self.inputTextView.text.copy;
|
||||
}
|
||||
|
||||
// TODO: Support using object address for strings, as in the object arg view.
|
||||
|
||||
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value {
|
||||
NSParameterAssert(type);
|
||||
unsigned long len = strlen(type);
|
||||
|
||||
BOOL isConst = type[0] == FLEXTypeEncodingConst;
|
||||
NSInteger i = isConst ? 1 : 0;
|
||||
|
||||
BOOL typeIsString = strcmp(type, FLEXEncodeClass(NSString)) == 0;
|
||||
BOOL typeIsCString = len <= 2 && type[i] == FLEXTypeEncodingCString;
|
||||
BOOL typeIsSEL = len <= 2 && type[i] == FLEXTypeEncodingSelector;
|
||||
BOOL valueIsString = [value isKindOfClass:[NSString class]];
|
||||
|
||||
BOOL typeIsPrimitiveString = typeIsSEL || typeIsCString;
|
||||
BOOL typeIsSupported = typeIsString || typeIsCString || typeIsSEL;
|
||||
|
||||
BOOL valueIsNSValueWithCorrectType = NO;
|
||||
if ([value isKindOfClass:[NSValue class]]) {
|
||||
NSValue *v = (id)value;
|
||||
len = strlen(v.objCType);
|
||||
if (len == 1) {
|
||||
FLEXTypeEncoding type = v.objCType[i];
|
||||
if (type == FLEXTypeEncodingCString && typeIsCString) {
|
||||
valueIsNSValueWithCorrectType = YES;
|
||||
} else if (type == FLEXTypeEncodingSelector && typeIsSEL) {
|
||||
valueIsNSValueWithCorrectType = YES;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!value && typeIsSupported) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
if (typeIsString && valueIsString) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
// Primitive strings can be input as NSStrings or NSValues
|
||||
if (typeIsPrimitiveString && (valueIsString || valueIsNSValueWithCorrectType)) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,16 @@
|
||||
//
|
||||
// FLEXArgumentInputStructView.h
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/16/14.
|
||||
// Copyright (c) 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputView.h"
|
||||
|
||||
@interface FLEXArgumentInputStructView : FLEXArgumentInputView
|
||||
|
||||
/// Enable displaying ivar names for custom struct types
|
||||
+ (void)registerFieldNames:(NSArray<NSString *> *)names forTypeEncoding:(NSString *)typeEncoding;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,228 @@
|
||||
//
|
||||
// FLEXArgumentInputStructView.m
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/16/14.
|
||||
// Copyright (c) 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputStructView.h"
|
||||
#import "FLEXArgumentInputViewFactory.h"
|
||||
#import "FLEXRuntimeUtility.h"
|
||||
#import "FLEXTypeEncodingParser.h"
|
||||
|
||||
@interface FLEXArgumentInputStructView ()
|
||||
|
||||
@property (nonatomic) NSArray<FLEXArgumentInputView *> *argumentInputViews;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXArgumentInputStructView
|
||||
|
||||
static NSMutableDictionary<NSString *, NSArray<NSString *> *> *structFieldNameRegistrar = nil;
|
||||
+ (void)initialize {
|
||||
if (self == [FLEXArgumentInputStructView class]) {
|
||||
structFieldNameRegistrar = [NSMutableDictionary new];
|
||||
[self registerDefaultFieldNames];
|
||||
}
|
||||
}
|
||||
|
||||
+ (void)registerDefaultFieldNames {
|
||||
NSDictionary *defaults = @{
|
||||
@(@encode(CGRect)): @[@"CGPoint origin", @"CGSize size"],
|
||||
@(@encode(CGPoint)): @[@"CGFloat x", @"CGFloat y"],
|
||||
@(@encode(CGSize)): @[@"CGFloat width", @"CGFloat height"],
|
||||
@(@encode(CGVector)): @[@"CGFloat dx", @"CGFloat dy"],
|
||||
@(@encode(UIEdgeInsets)): @[@"CGFloat top", @"CGFloat left", @"CGFloat bottom", @"CGFloat right"],
|
||||
@(@encode(UIOffset)): @[@"CGFloat horizontal", @"CGFloat vertical"],
|
||||
@(@encode(NSRange)): @[@"NSUInteger location", @"NSUInteger length"],
|
||||
@(@encode(CATransform3D)): @[@"CGFloat m11", @"CGFloat m12", @"CGFloat m13", @"CGFloat m14",
|
||||
@"CGFloat m21", @"CGFloat m22", @"CGFloat m23", @"CGFloat m24",
|
||||
@"CGFloat m31", @"CGFloat m32", @"CGFloat m33", @"CGFloat m34",
|
||||
@"CGFloat m41", @"CGFloat m42", @"CGFloat m43", @"CGFloat m44"],
|
||||
@(@encode(CGAffineTransform)): @[@"CGFloat a", @"CGFloat b",
|
||||
@"CGFloat c", @"CGFloat d",
|
||||
@"CGFloat tx", @"CGFloat ty"],
|
||||
};
|
||||
|
||||
[structFieldNameRegistrar addEntriesFromDictionary:defaults];
|
||||
|
||||
if (@available(iOS 11.0, *)) {
|
||||
structFieldNameRegistrar[@(@encode(NSDirectionalEdgeInsets))] = @[
|
||||
@"CGFloat top", @"CGFloat leading", @"CGFloat bottom", @"CGFloat trailing"
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
|
||||
self = [super initWithArgumentTypeEncoding:typeEncoding];
|
||||
if (self) {
|
||||
NSMutableArray<FLEXArgumentInputView *> *inputViews = [NSMutableArray new];
|
||||
NSArray<NSString *> *customTitles = [[self class] customFieldTitlesForTypeEncoding:typeEncoding];
|
||||
[FLEXRuntimeUtility enumerateTypesInStructEncoding:typeEncoding usingBlock:^(NSString *structName,
|
||||
const char *fieldTypeEncoding,
|
||||
NSString *prettyTypeEncoding,
|
||||
NSUInteger fieldIndex,
|
||||
NSUInteger fieldOffset) {
|
||||
|
||||
FLEXArgumentInputView *inputView = [FLEXArgumentInputViewFactory argumentInputViewForTypeEncoding:fieldTypeEncoding];
|
||||
inputView.targetSize = FLEXArgumentInputViewSizeSmall;
|
||||
|
||||
if (fieldIndex < customTitles.count) {
|
||||
inputView.title = customTitles[fieldIndex];
|
||||
} else {
|
||||
inputView.title = [NSString stringWithFormat:@"%@ field %lu (%@)",
|
||||
structName, (unsigned long)fieldIndex, prettyTypeEncoding
|
||||
];
|
||||
}
|
||||
|
||||
[inputViews addObject:inputView];
|
||||
[self addSubview:inputView];
|
||||
}];
|
||||
self.argumentInputViews = inputViews;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Superclass Overrides
|
||||
|
||||
- (void)setBackgroundColor:(UIColor *)backgroundColor {
|
||||
[super setBackgroundColor:backgroundColor];
|
||||
for (FLEXArgumentInputView *inputView in self.argumentInputViews) {
|
||||
inputView.backgroundColor = backgroundColor;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setInputValue:(id)inputValue {
|
||||
if ([inputValue isKindOfClass:[NSValue class]]) {
|
||||
const char *structTypeEncoding = [inputValue objCType];
|
||||
if (strcmp(self.typeEncoding.UTF8String, structTypeEncoding) == 0) {
|
||||
NSUInteger valueSize = 0;
|
||||
|
||||
if (FLEXGetSizeAndAlignment(structTypeEncoding, &valueSize, NULL)) {
|
||||
void *unboxedValue = malloc(valueSize);
|
||||
[inputValue getValue:unboxedValue];
|
||||
[FLEXRuntimeUtility enumerateTypesInStructEncoding:structTypeEncoding usingBlock:^(NSString *structName,
|
||||
const char *fieldTypeEncoding,
|
||||
NSString *prettyTypeEncoding,
|
||||
NSUInteger fieldIndex,
|
||||
NSUInteger fieldOffset) {
|
||||
|
||||
void *fieldPointer = unboxedValue + fieldOffset;
|
||||
FLEXArgumentInputView *inputView = self.argumentInputViews[fieldIndex];
|
||||
|
||||
if (fieldTypeEncoding[0] == FLEXTypeEncodingObjcObject || fieldTypeEncoding[0] == FLEXTypeEncodingObjcClass) {
|
||||
inputView.inputValue = (__bridge id)fieldPointer;
|
||||
} else {
|
||||
NSValue *boxedField = [FLEXRuntimeUtility valueForPrimitivePointer:fieldPointer objCType:fieldTypeEncoding];
|
||||
inputView.inputValue = boxedField;
|
||||
}
|
||||
}];
|
||||
free(unboxedValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (id)inputValue {
|
||||
NSValue *boxedStruct = nil;
|
||||
const char *structTypeEncoding = self.typeEncoding.UTF8String;
|
||||
NSUInteger structSize = 0;
|
||||
|
||||
if (FLEXGetSizeAndAlignment(structTypeEncoding, &structSize, NULL)) {
|
||||
void *unboxedStruct = malloc(structSize);
|
||||
[FLEXRuntimeUtility enumerateTypesInStructEncoding:structTypeEncoding usingBlock:^(NSString *structName,
|
||||
const char *fieldTypeEncoding,
|
||||
NSString *prettyTypeEncoding,
|
||||
NSUInteger fieldIndex,
|
||||
NSUInteger fieldOffset) {
|
||||
|
||||
void *fieldPointer = unboxedStruct + fieldOffset;
|
||||
FLEXArgumentInputView *inputView = self.argumentInputViews[fieldIndex];
|
||||
|
||||
if (fieldTypeEncoding[0] == FLEXTypeEncodingObjcObject || fieldTypeEncoding[0] == FLEXTypeEncodingObjcClass) {
|
||||
// Object fields
|
||||
memcpy(fieldPointer, (__bridge void *)inputView.inputValue, sizeof(id));
|
||||
} else {
|
||||
// Boxed primitive/struct fields
|
||||
id inputValue = inputView.inputValue;
|
||||
if ([inputValue isKindOfClass:[NSValue class]] && strcmp([inputValue objCType], fieldTypeEncoding) == 0) {
|
||||
[inputValue getValue:fieldPointer];
|
||||
}
|
||||
}
|
||||
}];
|
||||
|
||||
boxedStruct = [NSValue value:unboxedStruct withObjCType:structTypeEncoding];
|
||||
free(unboxedStruct);
|
||||
}
|
||||
|
||||
return boxedStruct;
|
||||
}
|
||||
|
||||
- (BOOL)inputViewIsFirstResponder {
|
||||
BOOL isFirstResponder = NO;
|
||||
for (FLEXArgumentInputView *inputView in self.argumentInputViews) {
|
||||
if ([inputView inputViewIsFirstResponder]) {
|
||||
isFirstResponder = YES;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return isFirstResponder;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Layout and Sizing
|
||||
|
||||
- (void)layoutSubviews {
|
||||
[super layoutSubviews];
|
||||
|
||||
CGFloat runningOriginY = self.topInputFieldVerticalLayoutGuide;
|
||||
|
||||
for (FLEXArgumentInputView *inputView in self.argumentInputViews) {
|
||||
CGSize inputFitSize = [inputView sizeThatFits:self.bounds.size];
|
||||
inputView.frame = CGRectMake(0, runningOriginY, inputFitSize.width, inputFitSize.height);
|
||||
runningOriginY = CGRectGetMaxY(inputView.frame) + [[self class] verticalPaddingBetweenFields];
|
||||
}
|
||||
}
|
||||
|
||||
+ (CGFloat)verticalPaddingBetweenFields {
|
||||
return 10.0;
|
||||
}
|
||||
|
||||
- (CGSize)sizeThatFits:(CGSize)size {
|
||||
CGSize fitSize = [super sizeThatFits:size];
|
||||
|
||||
CGSize constrainSize = CGSizeMake(size.width, CGFLOAT_MAX);
|
||||
CGFloat height = fitSize.height;
|
||||
|
||||
for (FLEXArgumentInputView *inputView in self.argumentInputViews) {
|
||||
height += [inputView sizeThatFits:constrainSize].height;
|
||||
height += [[self class] verticalPaddingBetweenFields];
|
||||
}
|
||||
|
||||
return CGSizeMake(fitSize.width, height);
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Class Helpers
|
||||
|
||||
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value {
|
||||
NSParameterAssert(type);
|
||||
if (type[0] == FLEXTypeEncodingStructBegin) {
|
||||
return FLEXGetSizeAndAlignment(type, nil, nil);
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
+ (void)registerFieldNames:(NSArray<NSString *> *)names forTypeEncoding:(NSString *)typeEncoding {
|
||||
NSParameterAssert(typeEncoding); NSParameterAssert(names);
|
||||
structFieldNameRegistrar[typeEncoding] = names;
|
||||
}
|
||||
|
||||
+ (NSArray<NSString *> *)customFieldTitlesForTypeEncoding:(const char *)typeEncoding {
|
||||
return structFieldNameRegistrar[@(typeEncoding)];
|
||||
}
|
||||
|
||||
@end
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/16/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputView.h"
|
||||
+12
-18
@@ -3,24 +3,23 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/16/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputSwitchView.h"
|
||||
|
||||
@interface FLEXArgumentInputSwitchView ()
|
||||
|
||||
@property (nonatomic, strong) UISwitch *inputSwitch;
|
||||
@property (nonatomic) UISwitch *inputSwitch;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXArgumentInputSwitchView
|
||||
|
||||
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding
|
||||
{
|
||||
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
|
||||
self = [super initWithArgumentTypeEncoding:typeEncoding];
|
||||
if (self) {
|
||||
self.inputSwitch = [[UISwitch alloc] init];
|
||||
self.inputSwitch = [UISwitch new];
|
||||
[self.inputSwitch addTarget:self action:@selector(switchValueDidChange:) forControlEvents:UIControlEventValueChanged];
|
||||
[self.inputSwitch sizeToFit];
|
||||
[self addSubview:self.inputSwitch];
|
||||
@@ -31,8 +30,7 @@
|
||||
|
||||
#pragma mark Input/Output
|
||||
|
||||
- (void)setInputValue:(id)inputValue
|
||||
{
|
||||
- (void)setInputValue:(id)inputValue {
|
||||
BOOL on = NO;
|
||||
if ([inputValue isKindOfClass:[NSNumber class]]) {
|
||||
NSNumber *number = (NSNumber *)inputValue;
|
||||
@@ -46,30 +44,26 @@
|
||||
self.inputSwitch.on = on;
|
||||
}
|
||||
|
||||
- (id)inputValue
|
||||
{
|
||||
- (id)inputValue {
|
||||
BOOL isOn = [self.inputSwitch isOn];
|
||||
NSValue *boxedBool = [NSValue value:&isOn withObjCType:@encode(BOOL)];
|
||||
return boxedBool;
|
||||
}
|
||||
|
||||
- (void)switchValueDidChange:(id)sender
|
||||
{
|
||||
- (void)switchValueDidChange:(id)sender {
|
||||
[self.delegate argumentInputViewValueDidChange:self];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Layout and Sizing
|
||||
|
||||
- (void)layoutSubviews
|
||||
{
|
||||
- (void)layoutSubviews {
|
||||
[super layoutSubviews];
|
||||
|
||||
self.inputSwitch.frame = CGRectMake(0, self.topInputFieldVerticalLayoutGuide, self.inputSwitch.frame.size.width, self.inputSwitch.frame.size.height);
|
||||
}
|
||||
|
||||
- (CGSize)sizeThatFits:(CGSize)size
|
||||
{
|
||||
- (CGSize)sizeThatFits:(CGSize)size {
|
||||
CGSize fitSize = [super sizeThatFits:size];
|
||||
fitSize.height += self.inputSwitch.frame.size.height;
|
||||
return fitSize;
|
||||
@@ -78,10 +72,10 @@
|
||||
|
||||
#pragma mark - Class Helpers
|
||||
|
||||
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value
|
||||
{
|
||||
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value {
|
||||
NSParameterAssert(type);
|
||||
// Only BOOLs. Current value is irrelevant.
|
||||
return type && strcmp(type, @encode(BOOL)) == 0;
|
||||
return strcmp(type, @encode(BOOL)) == 0;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,18 @@
|
||||
//
|
||||
// FLEXArgumentInputTextView.h
|
||||
// FLEXInjected
|
||||
//
|
||||
// Created by Ryan Olson on 6/15/14.
|
||||
//
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputView.h"
|
||||
|
||||
@interface FLEXArgumentInputTextView : FLEXArgumentInputView <UITextViewDelegate>
|
||||
|
||||
// For subclass eyes only
|
||||
|
||||
@property (nonatomic, readonly) UITextView *inputTextView;
|
||||
@property (nonatomic) NSString *inputPlaceholderText;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,155 @@
|
||||
//
|
||||
// FLEXArgumentInputTextView.m
|
||||
// FLEXInjected
|
||||
//
|
||||
// Created by Ryan Olson on 6/15/14.
|
||||
//
|
||||
//
|
||||
|
||||
#import "FLEXColor.h"
|
||||
#import "FLEXArgumentInputTextView.h"
|
||||
#import "FLEXUtility.h"
|
||||
|
||||
@interface FLEXArgumentInputTextView ()
|
||||
|
||||
@property (nonatomic) UITextView *inputTextView;
|
||||
@property (nonatomic) UILabel *placeholderLabel;
|
||||
@property (nonatomic, readonly) NSUInteger numberOfInputLines;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXArgumentInputTextView
|
||||
|
||||
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
|
||||
self = [super initWithArgumentTypeEncoding:typeEncoding];
|
||||
if (self) {
|
||||
self.inputTextView = [UITextView new];
|
||||
self.inputTextView.font = [[self class] inputFont];
|
||||
self.inputTextView.backgroundColor = FLEXColor.secondaryGroupedBackgroundColor;
|
||||
self.inputTextView.layer.cornerRadius = 10.f;
|
||||
self.inputTextView.contentInset = UIEdgeInsetsMake(0, 5, 0, 0);
|
||||
self.inputTextView.autocapitalizationType = UITextAutocapitalizationTypeNone;
|
||||
self.inputTextView.autocorrectionType = UITextAutocorrectionTypeNo;
|
||||
self.inputTextView.delegate = self;
|
||||
self.inputTextView.inputAccessoryView = [self createToolBar];
|
||||
if (@available(iOS 11, *)) {
|
||||
self.inputTextView.smartQuotesType = UITextSmartQuotesTypeNo;
|
||||
[self.inputTextView.layer setValue:@YES forKey:@"continuousCorners"];
|
||||
} else {
|
||||
self.inputTextView.layer.borderWidth = 1.f;
|
||||
self.inputTextView.layer.borderColor = FLEXColor.borderColor.CGColor;
|
||||
}
|
||||
|
||||
self.placeholderLabel = [UILabel new];
|
||||
self.placeholderLabel.font = self.inputTextView.font;
|
||||
self.placeholderLabel.textColor = FLEXColor.deemphasizedTextColor;
|
||||
self.placeholderLabel.numberOfLines = 0;
|
||||
|
||||
[self addSubview:self.inputTextView];
|
||||
[self.inputTextView addSubview:self.placeholderLabel];
|
||||
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - Private
|
||||
|
||||
- (UIToolbar *)createToolBar {
|
||||
UIToolbar *toolBar = [UIToolbar new];
|
||||
[toolBar sizeToFit];
|
||||
UIBarButtonItem *spaceItem = [[UIBarButtonItem alloc]
|
||||
initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace
|
||||
target:nil action:nil
|
||||
];
|
||||
UIBarButtonItem *pasteItem = [[UIBarButtonItem alloc]
|
||||
initWithTitle:@"Paste" style:UIBarButtonItemStyleDone
|
||||
target:self.inputTextView action:@selector(paste:)
|
||||
];
|
||||
UIBarButtonItem *doneItem = [[UIBarButtonItem alloc]
|
||||
initWithBarButtonSystemItem:UIBarButtonSystemItemDone
|
||||
target:self.inputTextView action:@selector(resignFirstResponder)
|
||||
];
|
||||
toolBar.items = @[spaceItem, pasteItem, doneItem];
|
||||
return toolBar;
|
||||
}
|
||||
|
||||
- (void)setInputPlaceholderText:(NSString *)placeholder {
|
||||
self.placeholderLabel.text = placeholder;
|
||||
if (placeholder.length) {
|
||||
if (!self.inputTextView.text.length) {
|
||||
self.placeholderLabel.hidden = NO;
|
||||
} else {
|
||||
self.placeholderLabel.hidden = YES;
|
||||
}
|
||||
} else {
|
||||
self.placeholderLabel.hidden = YES;
|
||||
}
|
||||
|
||||
[self setNeedsLayout];
|
||||
}
|
||||
|
||||
- (NSString *)inputPlaceholderText {
|
||||
return self.placeholderLabel.text;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Superclass Overrides
|
||||
|
||||
- (BOOL)inputViewIsFirstResponder {
|
||||
return self.inputTextView.isFirstResponder;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Layout and Sizing
|
||||
|
||||
- (void)layoutSubviews {
|
||||
[super layoutSubviews];
|
||||
|
||||
self.inputTextView.frame = CGRectMake(0, self.topInputFieldVerticalLayoutGuide, self.bounds.size.width, [self inputTextViewHeight]);
|
||||
// Placeholder label is positioned by insetting then origin
|
||||
// by the content inset then the text container inset
|
||||
CGSize s = self.inputTextView.frame.size;
|
||||
self.placeholderLabel.frame = CGRectMake(0, 0, s.width, s.height);
|
||||
self.placeholderLabel.frame = UIEdgeInsetsInsetRect(
|
||||
UIEdgeInsetsInsetRect(self.placeholderLabel.frame, self.inputTextView.contentInset),
|
||||
self.inputTextView.textContainerInset
|
||||
);
|
||||
}
|
||||
|
||||
- (NSUInteger)numberOfInputLines {
|
||||
switch (self.targetSize) {
|
||||
case FLEXArgumentInputViewSizeDefault:
|
||||
return 2;
|
||||
case FLEXArgumentInputViewSizeSmall:
|
||||
return 1;
|
||||
case FLEXArgumentInputViewSizeLarge:
|
||||
return 8;
|
||||
}
|
||||
}
|
||||
|
||||
- (CGFloat)inputTextViewHeight {
|
||||
return ceil([[self class] inputFont].lineHeight * self.numberOfInputLines) + 16.0;
|
||||
}
|
||||
|
||||
- (CGSize)sizeThatFits:(CGSize)size {
|
||||
CGSize fitSize = [super sizeThatFits:size];
|
||||
fitSize.height += [self inputTextViewHeight];
|
||||
return fitSize;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Class Helpers
|
||||
|
||||
+ (UIFont *)inputFont {
|
||||
return [UIFont systemFontOfSize:14.0];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - UITextViewDelegate
|
||||
|
||||
- (void)textViewDidChange:(UITextView *)textView {
|
||||
[self.delegate argumentInputViewValueDidChange:self];
|
||||
self.placeholderLabel.hidden = !(self.inputPlaceholderText.length && !textView.text.length);
|
||||
}
|
||||
|
||||
@end
|
||||
+9
-5
@@ -3,14 +3,17 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 5/30/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
typedef NS_ENUM(NSUInteger, FLEXArgumentInputViewSize) {
|
||||
/// 2 lines, medium-sized
|
||||
FLEXArgumentInputViewSizeDefault = 0,
|
||||
/// One line
|
||||
FLEXArgumentInputViewSizeSmall,
|
||||
/// Several lines
|
||||
FLEXArgumentInputViewSizeLarge
|
||||
};
|
||||
|
||||
@@ -26,12 +29,13 @@ typedef NS_ENUM(NSUInteger, FLEXArgumentInputViewSize) {
|
||||
/// To populate the filed with an initial value, set this property.
|
||||
/// To reteive the value input by the user, access the property.
|
||||
/// Primitive types and structs should/will be boxed in NSValue containers.
|
||||
/// Concrete subclasses *must* override both the setter and getter for this property.
|
||||
/// Concrete subclasses should override both the setter and getter for this property.
|
||||
/// Subclasses can call super.inputValue to access a backing store for the value.
|
||||
@property (nonatomic) id inputValue;
|
||||
|
||||
/// Setting this value to large will make some argument input views increase the size of their input field(s).
|
||||
/// Useful to increase the use of space if there is only one input view on screen (i.e. for property and ivar editing).
|
||||
@property (nonatomic, assign) FLEXArgumentInputViewSize targetSize;
|
||||
@property (nonatomic) FLEXArgumentInputViewSize targetSize;
|
||||
|
||||
/// Users of the input view can get delegate callbacks for incremental changes in user input.
|
||||
@property (nonatomic, weak) id <FLEXArgumentInputViewDelegate> delegate;
|
||||
@@ -47,8 +51,8 @@ typedef NS_ENUM(NSUInteger, FLEXArgumentInputViewSize) {
|
||||
|
||||
// For subclass eyes only
|
||||
|
||||
@property (nonatomic, strong, readonly) UILabel *titleLabel;
|
||||
@property (nonatomic, strong, readonly) NSString *typeEncoding;
|
||||
@property (nonatomic, readonly) UILabel *titleLabel;
|
||||
@property (nonatomic, readonly) NSString *typeEncoding;
|
||||
@property (nonatomic, readonly) CGFloat topInputFieldVerticalLayoutGuide;
|
||||
|
||||
@end
|
||||
+22
-45
@@ -3,32 +3,31 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 5/30/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputView.h"
|
||||
#import "FLEXUtility.h"
|
||||
#import "FLEXColor.h"
|
||||
|
||||
@interface FLEXArgumentInputView ()
|
||||
|
||||
@property (nonatomic, strong) UILabel *titleLabel;
|
||||
@property (nonatomic, strong) NSString *typeEncoding;
|
||||
@property (nonatomic) UILabel *titleLabel;
|
||||
@property (nonatomic) NSString *typeEncoding;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXArgumentInputView
|
||||
|
||||
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding
|
||||
{
|
||||
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
|
||||
self = [super initWithFrame:CGRectZero];
|
||||
if (self) {
|
||||
self.typeEncoding = @(typeEncoding);
|
||||
self.typeEncoding = typeEncoding != NULL ? @(typeEncoding) : nil;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)layoutSubviews
|
||||
{
|
||||
- (void)layoutSubviews {
|
||||
[super layoutSubviews];
|
||||
|
||||
if (self.showsTitle) {
|
||||
@@ -38,14 +37,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setBackgroundColor:(UIColor *)backgroundColor
|
||||
{
|
||||
- (void)setBackgroundColor:(UIColor *)backgroundColor {
|
||||
[super setBackgroundColor:backgroundColor];
|
||||
self.titleLabel.backgroundColor = backgroundColor;
|
||||
}
|
||||
|
||||
- (void)setTitle:(NSString *)title
|
||||
{
|
||||
- (void)setTitle:(NSString *)title {
|
||||
if (![_title isEqual:title]) {
|
||||
_title = title;
|
||||
self.titleLabel.text = title;
|
||||
@@ -53,26 +50,22 @@
|
||||
}
|
||||
}
|
||||
|
||||
- (UILabel *)titleLabel
|
||||
{
|
||||
- (UILabel *)titleLabel {
|
||||
if (!_titleLabel) {
|
||||
_titleLabel = [[UILabel alloc] init];
|
||||
_titleLabel = [UILabel new];
|
||||
_titleLabel.font = [[self class] titleFont];
|
||||
_titleLabel.backgroundColor = self.backgroundColor;
|
||||
_titleLabel.textColor = [UIColor colorWithWhite:0.3 alpha:1.0];
|
||||
_titleLabel.textColor = FLEXColor.primaryTextColor;
|
||||
_titleLabel.numberOfLines = 0;
|
||||
[self addSubview:_titleLabel];
|
||||
}
|
||||
return _titleLabel;
|
||||
}
|
||||
|
||||
- (BOOL)showsTitle
|
||||
{
|
||||
return [self.title length] > 0;
|
||||
- (BOOL)showsTitle {
|
||||
return self.title.length > 0;
|
||||
}
|
||||
|
||||
- (CGFloat)topInputFieldVerticalLayoutGuide
|
||||
{
|
||||
- (CGFloat)topInputFieldVerticalLayoutGuide {
|
||||
CGFloat verticalLayoutGuide = 0;
|
||||
if (self.showsTitle) {
|
||||
CGFloat titleHeight = [self.titleLabel sizeThatFits:self.bounds.size].height;
|
||||
@@ -84,48 +77,32 @@
|
||||
|
||||
#pragma mark - Subclasses Can Override
|
||||
|
||||
- (BOOL)inputViewIsFirstResponder
|
||||
{
|
||||
- (BOOL)inputViewIsFirstResponder {
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (void)setInputValue:(id)inputValue
|
||||
{
|
||||
// Subclasses should override.
|
||||
}
|
||||
|
||||
- (id)inputValue
|
||||
{
|
||||
// Subclasses should override.
|
||||
return nil;
|
||||
}
|
||||
|
||||
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value
|
||||
{
|
||||
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value {
|
||||
return NO;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Class Helpers
|
||||
|
||||
+ (UIFont *)titleFont
|
||||
{
|
||||
return [FLEXUtility defaultFontOfSize:12.0];
|
||||
+ (UIFont *)titleFont {
|
||||
return [UIFont systemFontOfSize:12.0];
|
||||
}
|
||||
|
||||
+ (CGFloat)titleBottomPadding
|
||||
{
|
||||
+ (CGFloat)titleBottomPadding {
|
||||
return 4.0;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Sizing
|
||||
|
||||
- (CGSize)sizeThatFits:(CGSize)size
|
||||
{
|
||||
- (CGSize)sizeThatFits:(CGSize)size {
|
||||
CGFloat height = 0;
|
||||
|
||||
if ([self.title length] > 0) {
|
||||
if (self.title.length > 0) {
|
||||
CGSize constrainSize = CGSizeMake(size.width, CGFLOAT_MAX);
|
||||
height += ceil([self.titleLabel sizeThatFits:constrainSize].height);
|
||||
height += [[self class] titleBottomPadding];
|
||||
+8
-3
@@ -7,16 +7,21 @@
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@class FLEXArgumentInputView;
|
||||
#import "FLEXArgumentInputSwitchView.h"
|
||||
|
||||
@interface FLEXArgumentInputViewFactory : NSObject
|
||||
|
||||
/// The main factory method for making argument input view subclasses that are the best fit for the type.
|
||||
/// Forwards to argumentInputViewForTypeEncoding:currentValue: with a nil currentValue.
|
||||
+ (FLEXArgumentInputView *)argumentInputViewForTypeEncoding:(const char *)typeEncoding;
|
||||
|
||||
/// The main factory method for making argument input view subclasses that are the best fit for the type.
|
||||
+ (FLEXArgumentInputView *)argumentInputViewForTypeEncoding:(const char *)typeEncoding currentValue:(id)currentValue;
|
||||
|
||||
/// A way to check if we should try editing a filed given its type encoding and value.
|
||||
/// Useful when deciding whether to edit or explore a property, ivar, or NSUserDefaults value.
|
||||
+ (BOOL)canEditFieldWithTypeEncoding:(const char *)typeEncoding currentValue:(id)currentValue;
|
||||
|
||||
/// Enable displaying ivar names for custom struct types
|
||||
+ (void)registerFieldNames:(NSArray<NSString *> *)names forTypeEncoding:(NSString *)typeEncoding;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,75 @@
|
||||
//
|
||||
// FLEXArgumentInputViewFactory.m
|
||||
// FLEXInjected
|
||||
//
|
||||
// Created by Ryan Olson on 6/15/14.
|
||||
//
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputViewFactory.h"
|
||||
#import "FLEXArgumentInputView.h"
|
||||
#import "FLEXArgumentInputObjectView.h"
|
||||
#import "FLEXArgumentInputNumberView.h"
|
||||
#import "FLEXArgumentInputSwitchView.h"
|
||||
#import "FLEXArgumentInputStructView.h"
|
||||
#import "FLEXArgumentInputNotSupportedView.h"
|
||||
#import "FLEXArgumentInputStringView.h"
|
||||
#import "FLEXArgumentInputFontView.h"
|
||||
#import "FLEXArgumentInputColorView.h"
|
||||
#import "FLEXArgumentInputDateView.h"
|
||||
#import "FLEXRuntimeUtility.h"
|
||||
|
||||
@implementation FLEXArgumentInputViewFactory
|
||||
|
||||
+ (FLEXArgumentInputView *)argumentInputViewForTypeEncoding:(const char *)typeEncoding {
|
||||
return [self argumentInputViewForTypeEncoding:typeEncoding currentValue:nil];
|
||||
}
|
||||
|
||||
+ (FLEXArgumentInputView *)argumentInputViewForTypeEncoding:(const char *)typeEncoding currentValue:(id)currentValue {
|
||||
Class subclass = [self argumentInputViewSubclassForTypeEncoding:typeEncoding currentValue:currentValue];
|
||||
if (!subclass) {
|
||||
// Fall back to a FLEXArgumentInputNotSupportedView if we can't find a subclass that fits the type encoding.
|
||||
// The unsupported view shows "nil" and does not allow user input.
|
||||
subclass = [FLEXArgumentInputNotSupportedView class];
|
||||
}
|
||||
// Remove the field name if there is any (e.g. \"width\"d -> d)
|
||||
const NSUInteger fieldNameOffset = [FLEXRuntimeUtility fieldNameOffsetForTypeEncoding:typeEncoding];
|
||||
return [[subclass alloc] initWithArgumentTypeEncoding:typeEncoding + fieldNameOffset];
|
||||
}
|
||||
|
||||
+ (Class)argumentInputViewSubclassForTypeEncoding:(const char *)typeEncoding currentValue:(id)currentValue {
|
||||
// Remove the field name if there is any (e.g. \"width\"d -> d)
|
||||
const NSUInteger fieldNameOffset = [FLEXRuntimeUtility fieldNameOffsetForTypeEncoding:typeEncoding];
|
||||
Class argumentInputViewSubclass = nil;
|
||||
NSArray<Class> *inputViewClasses = @[[FLEXArgumentInputColorView class],
|
||||
[FLEXArgumentInputFontView class],
|
||||
[FLEXArgumentInputStringView class],
|
||||
[FLEXArgumentInputStructView class],
|
||||
[FLEXArgumentInputSwitchView class],
|
||||
[FLEXArgumentInputDateView class],
|
||||
[FLEXArgumentInputNumberView class],
|
||||
[FLEXArgumentInputObjectView class]];
|
||||
|
||||
// Note that order is important here since multiple subclasses may support the same type.
|
||||
// An example is the number subclass and the bool subclass for the type @encode(BOOL).
|
||||
// Both work, but we'd prefer to use the bool subclass.
|
||||
for (Class inputViewClass in inputViewClasses) {
|
||||
if ([inputViewClass supportsObjCType:typeEncoding + fieldNameOffset withCurrentValue:currentValue]) {
|
||||
argumentInputViewSubclass = inputViewClass;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return argumentInputViewSubclass;
|
||||
}
|
||||
|
||||
+ (BOOL)canEditFieldWithTypeEncoding:(const char *)typeEncoding currentValue:(id)currentValue {
|
||||
return [self argumentInputViewSubclassForTypeEncoding:typeEncoding currentValue:currentValue] != nil;
|
||||
}
|
||||
|
||||
/// Enable displaying ivar names for custom struct types
|
||||
+ (void)registerFieldNames:(NSArray<NSString *> *)names forTypeEncoding:(NSString *)typeEncoding {
|
||||
[FLEXArgumentInputStructView registerFieldNames:names forTypeEncoding:typeEncoding];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -3,15 +3,19 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 5/23/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXFieldEditorViewController.h"
|
||||
|
||||
@interface FLEXDefaultEditorViewController : FLEXFieldEditorViewController
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
- (id)initWithDefaults:(NSUserDefaults *)defaults key:(NSString *)key;
|
||||
@interface FLEXDefaultEditorViewController : FLEXVariableEditorViewController
|
||||
|
||||
+ (BOOL)canEditDefaultWithValue:(id)currentValue;
|
||||
+ (instancetype)target:(NSUserDefaults *)defaults key:(NSString *)key commitHandler:(void(^_Nullable)(void))onCommit;
|
||||
|
||||
+ (BOOL)canEditDefaultWithValue:(nullable id)currentValue;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 5/23/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXDefaultEditorViewController.h"
|
||||
@@ -15,43 +15,42 @@
|
||||
@interface FLEXDefaultEditorViewController ()
|
||||
|
||||
@property (nonatomic, readonly) NSUserDefaults *defaults;
|
||||
@property (nonatomic, strong) NSString *key;
|
||||
@property (nonatomic, readonly) NSString *key;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXDefaultEditorViewController
|
||||
|
||||
- (id)initWithDefaults:(NSUserDefaults *)defaults key:(NSString *)key
|
||||
{
|
||||
self = [super initWithTarget:defaults];
|
||||
if (self) {
|
||||
self.key = key;
|
||||
self.title = @"Edit Default";
|
||||
}
|
||||
return self;
|
||||
+ (instancetype)target:(NSUserDefaults *)defaults key:(NSString *)key commitHandler:(void(^_Nullable)(void))onCommit {
|
||||
FLEXDefaultEditorViewController *editor = [self target:defaults data:key commitHandler:onCommit];
|
||||
editor.title = @"Edit Default";
|
||||
return editor;
|
||||
}
|
||||
|
||||
- (NSUserDefaults *)defaults
|
||||
{
|
||||
return [self.target isKindOfClass:[NSUserDefaults class]] ? self.target : nil;
|
||||
- (NSUserDefaults *)defaults {
|
||||
return [_target isKindOfClass:[NSUserDefaults class]] ? _target : nil;
|
||||
}
|
||||
|
||||
- (void)viewDidLoad
|
||||
{
|
||||
- (NSString *)key {
|
||||
return _data;
|
||||
}
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
|
||||
self.fieldEditorView.fieldDescription = self.key;
|
||||
|
||||
FLEXArgumentInputView *inputView = [FLEXArgumentInputViewFactory argumentInputViewForTypeEncoding:@encode(id)];
|
||||
|
||||
id currentValue = [self.defaults objectForKey:self.key];
|
||||
FLEXArgumentInputView *inputView = [FLEXArgumentInputViewFactory
|
||||
argumentInputViewForTypeEncoding:FLEXEncodeObject(currentValue)
|
||||
currentValue:currentValue
|
||||
];
|
||||
inputView.backgroundColor = self.view.backgroundColor;
|
||||
inputView.inputValue = [self.defaults objectForKey:self.key];
|
||||
inputView.inputValue = currentValue;
|
||||
self.fieldEditorView.argumentInputViews = @[inputView];
|
||||
}
|
||||
|
||||
- (void)actionButtonPressed:(id)sender
|
||||
{
|
||||
[super actionButtonPressed:sender];
|
||||
|
||||
- (void)actionButtonPressed:(id)sender {
|
||||
id value = self.firstInputView.inputValue;
|
||||
if (value) {
|
||||
[self.defaults setObject:value forKey:self.key];
|
||||
@@ -59,13 +58,23 @@
|
||||
[self.defaults removeObjectForKey:self.key];
|
||||
}
|
||||
[self.defaults synchronize];
|
||||
|
||||
self.firstInputView.inputValue = [self.defaults objectForKey:self.key];
|
||||
|
||||
// Dismiss keyboard and handle committed changes
|
||||
[super actionButtonPressed:sender];
|
||||
|
||||
// Go back after setting, but not for switches.
|
||||
if (sender) {
|
||||
[self.navigationController popViewControllerAnimated:YES];
|
||||
} else {
|
||||
self.firstInputView.inputValue = [self.defaults objectForKey:self.key];
|
||||
}
|
||||
}
|
||||
|
||||
+ (BOOL)canEditDefaultWithValue:(id)currentValue
|
||||
{
|
||||
return [FLEXArgumentInputViewFactory canEditFieldWithTypeEncoding:@encode(id) currentValue:currentValue];
|
||||
+ (BOOL)canEditDefaultWithValue:(id)currentValue {
|
||||
return [FLEXArgumentInputViewFactory
|
||||
canEditFieldWithTypeEncoding:FLEXEncodeObject(currentValue)
|
||||
currentValue:currentValue
|
||||
];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -3,16 +3,18 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 5/16/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@class FLEXArgumentInputView;
|
||||
|
||||
@interface FLEXFieldEditorView : UIView
|
||||
|
||||
@property (nonatomic, copy) NSString *targetDescription;
|
||||
@property (nonatomic, copy) NSString *fieldDescription;
|
||||
|
||||
@property (nonatomic, strong) NSArray *argumentInputViews;
|
||||
@property (nonatomic, copy) NSArray<FLEXArgumentInputView *> *argumentInputViews;
|
||||
|
||||
@end
|
||||
|
||||
@@ -3,29 +3,29 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 5/16/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXFieldEditorView.h"
|
||||
#import "FLEXArgumentInputView.h"
|
||||
#import "FLEXUtility.h"
|
||||
#import "FLEXColor.h"
|
||||
|
||||
@interface FLEXFieldEditorView ()
|
||||
|
||||
@property (nonatomic, strong) UILabel *targetDescriptionLabel;
|
||||
@property (nonatomic, strong) UIView *targetDescriptionDivider;
|
||||
@property (nonatomic, strong) UILabel *fieldDescriptionLabel;
|
||||
@property (nonatomic, strong) UIView *fieldDescriptionDivider;
|
||||
@property (nonatomic) UILabel *targetDescriptionLabel;
|
||||
@property (nonatomic) UIView *targetDescriptionDivider;
|
||||
@property (nonatomic) UILabel *fieldDescriptionLabel;
|
||||
@property (nonatomic) UIView *fieldDescriptionDivider;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXFieldEditorView
|
||||
|
||||
- (id)initWithFrame:(CGRect)frame
|
||||
{
|
||||
- (id)initWithFrame:(CGRect)frame {
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
self.targetDescriptionLabel = [[UILabel alloc] init];
|
||||
self.targetDescriptionLabel = [UILabel new];
|
||||
self.targetDescriptionLabel.numberOfLines = 0;
|
||||
self.targetDescriptionLabel.font = [[self class] labelFont];
|
||||
[self addSubview:self.targetDescriptionLabel];
|
||||
@@ -33,7 +33,7 @@
|
||||
self.targetDescriptionDivider = [[self class] dividerView];
|
||||
[self addSubview:self.targetDescriptionDivider];
|
||||
|
||||
self.fieldDescriptionLabel = [[UILabel alloc] init];
|
||||
self.fieldDescriptionLabel = [UILabel new];
|
||||
self.fieldDescriptionLabel.numberOfLines = 0;
|
||||
self.fieldDescriptionLabel.font = [[self class] labelFont];
|
||||
[self addSubview:self.fieldDescriptionLabel];
|
||||
@@ -44,8 +44,7 @@
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)layoutSubviews
|
||||
{
|
||||
- (void)layoutSubviews {
|
||||
[super layoutSubviews];
|
||||
|
||||
CGFloat horizontalPadding = [[self class] horizontalPadding];
|
||||
@@ -78,15 +77,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setBackgroundColor:(UIColor *)backgroundColor
|
||||
{
|
||||
- (void)setBackgroundColor:(UIColor *)backgroundColor {
|
||||
[super setBackgroundColor:backgroundColor];
|
||||
self.targetDescriptionLabel.backgroundColor = backgroundColor;
|
||||
self.fieldDescriptionLabel.backgroundColor = backgroundColor;
|
||||
}
|
||||
|
||||
- (void)setTargetDescription:(NSString *)targetDescription
|
||||
{
|
||||
- (void)setTargetDescription:(NSString *)targetDescription {
|
||||
if (![_targetDescription isEqual:targetDescription]) {
|
||||
_targetDescription = targetDescription;
|
||||
self.targetDescriptionLabel.text = targetDescription;
|
||||
@@ -94,8 +91,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setFieldDescription:(NSString *)fieldDescription
|
||||
{
|
||||
- (void)setFieldDescription:(NSString *)fieldDescription {
|
||||
if (![_fieldDescription isEqual:fieldDescription]) {
|
||||
_fieldDescription = fieldDescription;
|
||||
self.fieldDescriptionLabel.text = fieldDescription;
|
||||
@@ -103,8 +99,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setArgumentInputViews:(NSArray *)argumentInputViews
|
||||
{
|
||||
- (void)setArgumentInputViews:(NSArray<FLEXArgumentInputView *> *)argumentInputViews {
|
||||
if (![_argumentInputViews isEqual:argumentInputViews]) {
|
||||
|
||||
for (FLEXArgumentInputView *inputView in _argumentInputViews) {
|
||||
@@ -121,40 +116,33 @@
|
||||
}
|
||||
}
|
||||
|
||||
+ (UIView *)dividerView
|
||||
{
|
||||
UIView *dividerView = [[UIView alloc] init];
|
||||
+ (UIView *)dividerView {
|
||||
UIView *dividerView = [UIView new];
|
||||
dividerView.backgroundColor = [self dividerColor];
|
||||
return dividerView;
|
||||
}
|
||||
|
||||
+ (UIColor *)dividerColor
|
||||
{
|
||||
return [UIColor lightGrayColor];
|
||||
+ (UIColor *)dividerColor {
|
||||
return FLEXColor.tertiaryBackgroundColor;
|
||||
}
|
||||
|
||||
+ (CGFloat)horizontalPadding
|
||||
{
|
||||
+ (CGFloat)horizontalPadding {
|
||||
return 10.0;
|
||||
}
|
||||
|
||||
+ (CGFloat)verticalPadding
|
||||
{
|
||||
+ (CGFloat)verticalPadding {
|
||||
return 20.0;
|
||||
}
|
||||
|
||||
+ (UIFont *)labelFont
|
||||
{
|
||||
return [FLEXUtility defaultFontOfSize:14.0];
|
||||
+ (UIFont *)labelFont {
|
||||
return [UIFont systemFontOfSize:14.0];
|
||||
}
|
||||
|
||||
+ (CGFloat)dividerLineHeight
|
||||
{
|
||||
+ (CGFloat)dividerLineHeight {
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
- (CGSize)sizeThatFits:(CGSize)size
|
||||
{
|
||||
- (CGSize)sizeThatFits:(CGSize)size {
|
||||
CGFloat horizontalPadding = [[self class] horizontalPadding];
|
||||
CGFloat verticalPadding = [[self class] verticalPadding];
|
||||
CGFloat dividerLineHeight = [[self class] dividerLineHeight];
|
||||
|
||||
@@ -1,28 +1,29 @@
|
||||
//
|
||||
// FLEXFieldEditorViewController.h
|
||||
// Flipboard
|
||||
// FLEX
|
||||
//
|
||||
// Created by Ryan Olson on 5/16/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Created by Tanner on 11/22/18.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "FLEXVariableEditorViewController.h"
|
||||
#import "FLEXProperty.h"
|
||||
#import "FLEXIvar.h"
|
||||
|
||||
@class FLEXFieldEditorView;
|
||||
@class FLEXArgumentInputView;
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface FLEXFieldEditorViewController : UIViewController
|
||||
@interface FLEXFieldEditorViewController : FLEXVariableEditorViewController
|
||||
|
||||
- (id)initWithTarget:(id)target;
|
||||
/// @return nil if the property is readonly or if the type is unsupported
|
||||
+ (nullable instancetype)target:(id)target property:(FLEXProperty *)property commitHandler:(void(^_Nullable)(void))onCommit;
|
||||
/// @return nil if the ivar type is unsupported
|
||||
+ (nullable instancetype)target:(id)target ivar:(FLEXIvar *)ivar commitHandler:(void(^_Nullable)(void))onCommit;
|
||||
|
||||
// Convenience accessor since many subclasses only use one input view
|
||||
@property (nonatomic, readonly) FLEXArgumentInputView *firstInputView;
|
||||
/// Subclasses can change the button title via the \c title property
|
||||
@property (nonatomic, readonly) UIBarButtonItem *getterButton;
|
||||
|
||||
// For subclass use only.
|
||||
@property (nonatomic, strong, readonly) id target;
|
||||
@property (nonatomic, strong, readonly) FLEXFieldEditorView *fieldEditorView;
|
||||
@property (nonatomic, strong, readonly) UIBarButtonItem *setterButton;
|
||||
- (void)actionButtonPressed:(id)sender;
|
||||
- (NSString *)titleForActionButton;
|
||||
- (void)getterButtonPressed:(id)sender;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
@@ -1,117 +1,168 @@
|
||||
//
|
||||
// FLEXFieldEditorViewController.m
|
||||
// Flipboard
|
||||
// FLEX
|
||||
//
|
||||
// Created by Ryan Olson on 5/16/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Created by Tanner on 11/22/18.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXFieldEditorViewController.h"
|
||||
#import "FLEXFieldEditorView.h"
|
||||
#import "FLEXRuntimeUtility.h"
|
||||
#import "FLEXUtility.h"
|
||||
#import "FLEXArgumentInputView.h"
|
||||
#import "FLEXArgumentInputViewFactory.h"
|
||||
#import "FLEXPropertyAttributes.h"
|
||||
#import "FLEXRuntimeUtility.h"
|
||||
#import "FLEXMetadataExtras.h"
|
||||
#import "FLEXUtility.h"
|
||||
#import "FLEXColor.h"
|
||||
#import "UIBarButtonItem+FLEX.h"
|
||||
|
||||
@interface FLEXFieldEditorViewController () <UIScrollViewDelegate>
|
||||
@interface FLEXFieldEditorViewController () <FLEXArgumentInputViewDelegate>
|
||||
|
||||
@property (nonatomic, strong) UIScrollView *scrollView;
|
||||
@property (nonatomic, readonly) id<FLEXMetadataAuxiliaryInfo> auxiliaryInfoProvider;
|
||||
@property (nonatomic) FLEXProperty *property;
|
||||
@property (nonatomic) FLEXIvar *ivar;
|
||||
|
||||
@property (nonatomic, strong, readwrite) id target;
|
||||
@property (nonatomic, strong, readwrite) FLEXFieldEditorView *fieldEditorView;
|
||||
@property (nonatomic, strong, readwrite) UIBarButtonItem *setterButton;
|
||||
@property (nonatomic, readonly) id currentValue;
|
||||
@property (nonatomic, readonly) const FLEXTypeEncoding *typeEncoding;
|
||||
@property (nonatomic, readonly) NSString *fieldDescription;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXFieldEditorViewController
|
||||
|
||||
- (id)initWithTarget:(id)target
|
||||
{
|
||||
self = [super initWithNibName:nil bundle:nil];
|
||||
if (self) {
|
||||
self.target = target;
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardDidShow:) name:UIKeyboardDidShowNotification object:nil];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillHide:) name:UIKeyboardWillHideNotification object:nil];
|
||||
}
|
||||
return self;
|
||||
#pragma mark - Initialization
|
||||
|
||||
+ (instancetype)target:(id)target property:(nonnull FLEXProperty *)property commitHandler:(void(^)(void))onCommit {
|
||||
FLEXFieldEditorViewController *editor = [self target:target data:property commitHandler:onCommit];
|
||||
editor.title = [@"Property: " stringByAppendingString:property.name];
|
||||
editor.property = property;
|
||||
return editor;
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
+ (instancetype)target:(id)target ivar:(nonnull FLEXIvar *)ivar commitHandler:(void(^)(void))onCommit {
|
||||
FLEXFieldEditorViewController *editor = [self target:target data:ivar commitHandler:onCommit];
|
||||
editor.title = [@"Ivar: " stringByAppendingString:ivar.name];
|
||||
editor.ivar = ivar;
|
||||
return editor;
|
||||
}
|
||||
|
||||
- (void)keyboardDidShow:(NSNotification *)notification
|
||||
{
|
||||
CGRect keyboardRectInWindow = [[[notification userInfo] objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
|
||||
CGSize keyboardSize = [self.view convertRect:keyboardRectInWindow fromView:nil].size;
|
||||
UIEdgeInsets scrollInsets = self.scrollView.contentInset;
|
||||
scrollInsets.bottom = keyboardSize.height;
|
||||
self.scrollView.contentInset = scrollInsets;
|
||||
self.scrollView.scrollIndicatorInsets = scrollInsets;
|
||||
|
||||
// Find the active input view and scroll to make sure it's visible.
|
||||
for (FLEXArgumentInputView *argumentInputView in self.fieldEditorView.argumentInputViews) {
|
||||
if (argumentInputView.inputViewIsFirstResponder) {
|
||||
CGRect scrollToVisibleRect = [self.scrollView convertRect:argumentInputView.bounds fromView:argumentInputView];
|
||||
[self.scrollView scrollRectToVisible:scrollToVisibleRect animated:YES];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
#pragma mark - Overrides
|
||||
|
||||
- (void)keyboardWillHide:(NSNotification *)notification
|
||||
{
|
||||
UIEdgeInsets scrollInsets = self.scrollView.contentInset;
|
||||
scrollInsets.bottom = 0.0;
|
||||
self.scrollView.contentInset = scrollInsets;
|
||||
self.scrollView.scrollIndicatorInsets = scrollInsets;
|
||||
}
|
||||
|
||||
- (void)viewDidLoad
|
||||
{
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
|
||||
self.view.backgroundColor = FLEXColor.groupedBackgroundColor;
|
||||
|
||||
// Create getter button
|
||||
_getterButton = [[UIBarButtonItem alloc]
|
||||
initWithTitle:@"Get"
|
||||
style:UIBarButtonItemStyleDone
|
||||
target:self
|
||||
action:@selector(getterButtonPressed:)
|
||||
];
|
||||
self.toolbarItems = @[
|
||||
UIBarButtonItem.flex_flexibleSpace, self.getterButton, self.actionButton
|
||||
];
|
||||
|
||||
self.view.backgroundColor = [FLEXUtility scrollViewGrayColor];
|
||||
|
||||
self.scrollView = [[UIScrollView alloc] initWithFrame:self.view.bounds];
|
||||
self.scrollView.backgroundColor = self.view.backgroundColor;
|
||||
self.scrollView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
|
||||
self.scrollView.delegate = self;
|
||||
[self.view addSubview:self.scrollView];
|
||||
|
||||
self.fieldEditorView = [[FLEXFieldEditorView alloc] init];
|
||||
self.fieldEditorView.backgroundColor = self.view.backgroundColor;
|
||||
self.fieldEditorView.targetDescription = [NSString stringWithFormat:@"%@ %p", [self.target class], self.target];
|
||||
[self.scrollView addSubview:self.fieldEditorView];
|
||||
|
||||
self.setterButton = [[UIBarButtonItem alloc] initWithTitle:[self titleForActionButton] style:UIBarButtonItemStyleDone target:self action:@selector(actionButtonPressed:)];
|
||||
self.navigationItem.rightBarButtonItem = self.setterButton;
|
||||
[self registerAuxiliaryInfo];
|
||||
|
||||
// Configure input view
|
||||
self.fieldEditorView.fieldDescription = self.fieldDescription;
|
||||
FLEXArgumentInputView *inputView = [FLEXArgumentInputViewFactory argumentInputViewForTypeEncoding:self.typeEncoding];
|
||||
inputView.inputValue = self.currentValue;
|
||||
inputView.delegate = self;
|
||||
self.fieldEditorView.argumentInputViews = @[inputView];
|
||||
|
||||
// Don't show a "set" button for switches; we mutate when the switch is flipped
|
||||
if ([inputView isKindOfClass:[FLEXArgumentInputSwitchView class]]) {
|
||||
self.actionButton.enabled = NO;
|
||||
self.actionButton.title = @"Flip the switch to call the setter";
|
||||
// Put getter button before setter button
|
||||
self.toolbarItems = @[
|
||||
UIBarButtonItem.flex_flexibleSpace, self.actionButton, self.getterButton
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)viewWillLayoutSubviews
|
||||
{
|
||||
CGSize constrainSize = CGSizeMake(self.scrollView.bounds.size.width, CGFLOAT_MAX);
|
||||
CGSize fieldEditorSize = [self.fieldEditorView sizeThatFits:constrainSize];
|
||||
self.fieldEditorView.frame = CGRectMake(0, 0, fieldEditorSize.width, fieldEditorSize.height);
|
||||
self.scrollView.contentSize = fieldEditorSize;
|
||||
- (void)actionButtonPressed:(id)sender {
|
||||
if (self.property) {
|
||||
id userInputObject = self.firstInputView.inputValue;
|
||||
NSArray *arguments = userInputObject ? @[userInputObject] : nil;
|
||||
SEL setterSelector = self.property.likelySetter;
|
||||
NSError *error = nil;
|
||||
[FLEXRuntimeUtility performSelector:setterSelector onObject:self.target withArguments:arguments error:&error];
|
||||
if (error) {
|
||||
[FLEXAlert showAlert:@"Property Setter Failed" message:error.localizedDescription from:self];
|
||||
sender = nil; // Don't pop back
|
||||
}
|
||||
} else {
|
||||
// TODO: check mutability and use mutableCopy if necessary;
|
||||
// this currently could and would assign NSArray to NSMutableArray
|
||||
[self.ivar setValue:self.firstInputView.inputValue onObject:self.target];
|
||||
}
|
||||
|
||||
// Dismiss keyboard and handle committed changes
|
||||
[super actionButtonPressed:sender];
|
||||
|
||||
// Go back after setting, but not for switches.
|
||||
if (sender) {
|
||||
[self.navigationController popViewControllerAnimated:YES];
|
||||
} else {
|
||||
self.firstInputView.inputValue = self.currentValue;
|
||||
}
|
||||
}
|
||||
|
||||
- (FLEXArgumentInputView *)firstInputView
|
||||
{
|
||||
return [[self.fieldEditorView argumentInputViews] firstObject];
|
||||
}
|
||||
|
||||
- (void)actionButtonPressed:(id)sender
|
||||
{
|
||||
// Subclasses can override
|
||||
- (void)getterButtonPressed:(id)sender {
|
||||
[self.fieldEditorView endEditing:YES];
|
||||
|
||||
[self exploreObjectOrPopViewController:self.currentValue];
|
||||
}
|
||||
|
||||
- (NSString *)titleForActionButton
|
||||
{
|
||||
// Subclasses can override.
|
||||
return @"Set";
|
||||
- (void)argumentInputViewValueDidChange:(FLEXArgumentInputView *)argumentInputView {
|
||||
if ([argumentInputView isKindOfClass:[FLEXArgumentInputSwitchView class]]) {
|
||||
[self actionButtonPressed:nil];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Private
|
||||
|
||||
- (void)registerAuxiliaryInfo {
|
||||
// This is how Reflex will get Swift struct field names into the editor at runtime
|
||||
NSDictionary<NSString *, NSArray *> *labels = [self.auxiliaryInfoProvider
|
||||
auxiliaryInfoForKey:FLEXAuxiliarynfoKeyFieldLabels
|
||||
];
|
||||
|
||||
for (NSString *type in labels) {
|
||||
[FLEXArgumentInputViewFactory registerFieldNames:labels[type] forTypeEncoding:type];
|
||||
}
|
||||
}
|
||||
|
||||
- (id)currentValue {
|
||||
if (self.property) {
|
||||
return [self.property getValue:self.target];
|
||||
} else {
|
||||
return [self.ivar getValue:self.target];
|
||||
}
|
||||
}
|
||||
|
||||
- (id<FLEXMetadataAuxiliaryInfo>)auxiliaryInfoProvider {
|
||||
return self.ivar ?: self.property;
|
||||
}
|
||||
|
||||
- (const FLEXTypeEncoding *)typeEncoding {
|
||||
if (self.property) {
|
||||
return self.property.attributes.typeEncoding.UTF8String;
|
||||
} else {
|
||||
return self.ivar.typeEncoding.UTF8String;
|
||||
}
|
||||
}
|
||||
|
||||
- (NSString *)fieldDescription {
|
||||
if (self.property) {
|
||||
return self.property.fullDescription;
|
||||
} else {
|
||||
return self.ivar.description;
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
//
|
||||
// FLEXIvarEditorViewController.h
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 5/23/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXFieldEditorViewController.h"
|
||||
#import <objc/runtime.h>
|
||||
|
||||
@interface FLEXIvarEditorViewController : FLEXFieldEditorViewController
|
||||
|
||||
- (id)initWithTarget:(id)target ivar:(Ivar)ivar;
|
||||
|
||||
+ (BOOL)canEditIvar:(Ivar)ivar currentValue:(id)value;
|
||||
|
||||
@end
|
||||
@@ -1,72 +0,0 @@
|
||||
//
|
||||
// FLEXIvarEditorViewController.m
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 5/23/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXIvarEditorViewController.h"
|
||||
#import "FLEXFieldEditorView.h"
|
||||
#import "FLEXRuntimeUtility.h"
|
||||
#import "FLEXArgumentInputView.h"
|
||||
#import "FLEXArgumentInputViewFactory.h"
|
||||
#import "FLEXArgumentInputSwitchView.h"
|
||||
|
||||
@interface FLEXIvarEditorViewController () <FLEXArgumentInputViewDelegate>
|
||||
|
||||
@property (nonatomic, assign) Ivar ivar;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXIvarEditorViewController
|
||||
|
||||
- (id)initWithTarget:(id)target ivar:(Ivar)ivar
|
||||
{
|
||||
self = [super initWithTarget:target];
|
||||
if (self) {
|
||||
self.ivar = ivar;
|
||||
self.title = @"Instance Variable";
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)viewDidLoad
|
||||
{
|
||||
[super viewDidLoad];
|
||||
|
||||
self.fieldEditorView.fieldDescription = [FLEXRuntimeUtility prettyNameForIvar:self.ivar];
|
||||
|
||||
FLEXArgumentInputView *inputView = [FLEXArgumentInputViewFactory argumentInputViewForTypeEncoding:ivar_getTypeEncoding(self.ivar)];
|
||||
inputView.backgroundColor = self.view.backgroundColor;
|
||||
inputView.inputValue = [FLEXRuntimeUtility valueForIvar:self.ivar onObject:self.target];
|
||||
inputView.delegate = self;
|
||||
self.fieldEditorView.argumentInputViews = @[inputView];
|
||||
|
||||
// Don't show a "set" button for switches. Set the ivar when the switch toggles.
|
||||
if ([inputView isKindOfClass:[FLEXArgumentInputSwitchView class]]) {
|
||||
self.navigationItem.rightBarButtonItem = nil;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)actionButtonPressed:(id)sender
|
||||
{
|
||||
[super actionButtonPressed:sender];
|
||||
|
||||
[FLEXRuntimeUtility setValue:self.firstInputView.inputValue forIvar:self.ivar onObject:self.target];
|
||||
self.firstInputView.inputValue = [FLEXRuntimeUtility valueForIvar:self.ivar onObject:self.target];
|
||||
}
|
||||
|
||||
- (void)argumentInputViewValueDidChange:(FLEXArgumentInputView *)argumentInputView
|
||||
{
|
||||
if ([argumentInputView isKindOfClass:[FLEXArgumentInputSwitchView class]]) {
|
||||
[self actionButtonPressed:nil];
|
||||
}
|
||||
}
|
||||
|
||||
+ (BOOL)canEditIvar:(Ivar)ivar currentValue:(id)value
|
||||
{
|
||||
return [FLEXArgumentInputViewFactory canEditFieldWithTypeEncoding:ivar_getTypeEncoding(ivar) currentValue:value];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -3,14 +3,14 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 5/23/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXFieldEditorViewController.h"
|
||||
#import <objc/runtime.h>
|
||||
#import "FLEXVariableEditorViewController.h"
|
||||
#import "FLEXMethod.h"
|
||||
|
||||
@interface FLEXMethodCallingViewController : FLEXFieldEditorViewController
|
||||
@interface FLEXMethodCallingViewController : FLEXVariableEditorViewController
|
||||
|
||||
- (id)initWithTarget:(id)target method:(Method)method;
|
||||
+ (instancetype)target:(id)target method:(FLEXMethod *)method;
|
||||
|
||||
@end
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 5/23/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXMethodCallingViewController.h"
|
||||
@@ -13,88 +13,98 @@
|
||||
#import "FLEXObjectExplorerViewController.h"
|
||||
#import "FLEXArgumentInputView.h"
|
||||
#import "FLEXArgumentInputViewFactory.h"
|
||||
#import "FLEXUtility.h"
|
||||
|
||||
@interface FLEXMethodCallingViewController ()
|
||||
|
||||
@property (nonatomic, assign) Method method;
|
||||
|
||||
@property (nonatomic, readonly) FLEXMethod *method;
|
||||
@end
|
||||
|
||||
@implementation FLEXMethodCallingViewController
|
||||
|
||||
- (id)initWithTarget:(id)target method:(Method)method
|
||||
{
|
||||
self = [super initWithTarget:target];
|
||||
+ (instancetype)target:(id)target method:(FLEXMethod *)method {
|
||||
return [[self alloc] initWithTarget:target method:method];
|
||||
}
|
||||
|
||||
- (id)initWithTarget:(id)target method:(FLEXMethod *)method {
|
||||
NSParameterAssert(method.isInstanceMethod == !object_isClass(target));
|
||||
|
||||
self = [super initWithTarget:target data:method commitHandler:nil];
|
||||
if (self) {
|
||||
self.method = method;
|
||||
self.title = [self isClassMethod] ? @"Class Method" : @"Method";
|
||||
self.title = method.isInstanceMethod ? @"Method: " : @"Class Method: ";
|
||||
self.title = [self.title stringByAppendingString:method.selectorString];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)viewDidLoad
|
||||
{
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
|
||||
self.fieldEditorView.fieldDescription = [FLEXRuntimeUtility prettyNameForMethod:self.method isClassMethod:[self isClassMethod]];
|
||||
|
||||
NSArray *methodComponents = [FLEXRuntimeUtility prettyArgumentComponentsForMethod:self.method];
|
||||
NSMutableArray *argumentInputViews = [NSMutableArray array];
|
||||
|
||||
self.actionButton.title = @"Call";
|
||||
|
||||
// Configure field editor view
|
||||
self.fieldEditorView.argumentInputViews = [self argumentInputViews];
|
||||
self.fieldEditorView.fieldDescription = [NSString stringWithFormat:
|
||||
@"Signature:\n%@\n\nReturn Type:\n%s",
|
||||
self.method.description, (char *)self.method.returnType
|
||||
];
|
||||
}
|
||||
|
||||
- (NSArray<FLEXArgumentInputView *> *)argumentInputViews {
|
||||
Method method = self.method.objc_method;
|
||||
NSArray *methodComponents = [FLEXRuntimeUtility prettyArgumentComponentsForMethod:method];
|
||||
NSMutableArray<FLEXArgumentInputView *> *argumentInputViews = [NSMutableArray new];
|
||||
unsigned int argumentIndex = kFLEXNumberOfImplicitArgs;
|
||||
|
||||
for (NSString *methodComponent in methodComponents) {
|
||||
char *argumentTypeEncoding = method_copyArgumentType(self.method, argumentIndex);
|
||||
char *argumentTypeEncoding = method_copyArgumentType(method, argumentIndex);
|
||||
FLEXArgumentInputView *inputView = [FLEXArgumentInputViewFactory argumentInputViewForTypeEncoding:argumentTypeEncoding];
|
||||
free(argumentTypeEncoding);
|
||||
|
||||
|
||||
inputView.backgroundColor = self.view.backgroundColor;
|
||||
inputView.title = methodComponent;
|
||||
[argumentInputViews addObject:inputView];
|
||||
argumentIndex++;
|
||||
}
|
||||
self.fieldEditorView.argumentInputViews = argumentInputViews;
|
||||
|
||||
return argumentInputViews;
|
||||
}
|
||||
|
||||
- (BOOL)isClassMethod
|
||||
{
|
||||
return self.target && self.target == [self.target class];
|
||||
}
|
||||
|
||||
- (NSString *)titleForActionButton
|
||||
{
|
||||
return @"Call";
|
||||
}
|
||||
|
||||
- (void)actionButtonPressed:(id)sender
|
||||
{
|
||||
[super actionButtonPressed:sender];
|
||||
|
||||
NSMutableArray *arguments = [NSMutableArray array];
|
||||
- (void)actionButtonPressed:(id)sender {
|
||||
// Gather arguments
|
||||
NSMutableArray *arguments = [NSMutableArray new];
|
||||
for (FLEXArgumentInputView *inputView in self.fieldEditorView.argumentInputViews) {
|
||||
id argumentValue = inputView.inputValue;
|
||||
if (!argumentValue) {
|
||||
// Use NSNulls as placeholders in the array. They will be interpreted as nil arguments.
|
||||
argumentValue = [NSNull null];
|
||||
}
|
||||
[arguments addObject:argumentValue];
|
||||
// Use NSNull as a nil placeholder; it will be interpreted as nil
|
||||
[arguments addObject:inputView.inputValue ?: NSNull.null];
|
||||
}
|
||||
|
||||
|
||||
// Call method
|
||||
NSError *error = nil;
|
||||
id returnedObject = [FLEXRuntimeUtility performSelector:method_getName(self.method) onObject:self.target withArguments:arguments error:&error];
|
||||
id returnValue = [FLEXRuntimeUtility
|
||||
performSelector:self.method.selector
|
||||
onObject:self.target
|
||||
withArguments:arguments
|
||||
error:&error
|
||||
];
|
||||
|
||||
// Dismiss keyboard and handle committed changes
|
||||
[super actionButtonPressed:sender];
|
||||
|
||||
// Display return value or error
|
||||
if (error) {
|
||||
NSString *title = @"Method Call Failed";
|
||||
NSString *message = [error localizedDescription];
|
||||
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:title message:message delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];
|
||||
[alert show];
|
||||
} else if (returnedObject) {
|
||||
[FLEXAlert showAlert:@"Method Call Failed" message:error.localizedDescription from:self];
|
||||
} else if (returnValue) {
|
||||
// For non-nil (or void) return types, push an explorer view controller to display the returned object
|
||||
FLEXObjectExplorerViewController *explorerViewController = [FLEXObjectExplorerFactory explorerViewControllerForObject:returnedObject];
|
||||
[self.navigationController pushViewController:explorerViewController animated:YES];
|
||||
returnValue = [FLEXRuntimeUtility potentiallyUnwrapBoxedPointer:returnValue type:self.method.returnType];
|
||||
FLEXObjectExplorerViewController *explorer = [FLEXObjectExplorerFactory explorerViewControllerForObject:returnValue];
|
||||
[self.navigationController pushViewController:explorer animated:YES];
|
||||
} else {
|
||||
// If we didn't get a returned object but the method call succeeded,
|
||||
// pop this view controller off the stack to indicate that the call went through.
|
||||
[self.navigationController popViewControllerAnimated:YES];
|
||||
[self exploreObjectOrPopViewController:returnValue];
|
||||
}
|
||||
}
|
||||
|
||||
- (FLEXMethod *)method {
|
||||
return _data;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
//
|
||||
// FLEXPropertyEditorViewController.h
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 5/20/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXFieldEditorViewController.h"
|
||||
#import <objc/runtime.h>
|
||||
|
||||
@interface FLEXPropertyEditorViewController : FLEXFieldEditorViewController
|
||||
|
||||
- (id)initWithTarget:(id)target property:(objc_property_t)property;
|
||||
|
||||
+ (BOOL)canEditProperty:(objc_property_t)property currentValue:(id)value;
|
||||
|
||||
@end
|
||||
@@ -1,94 +0,0 @@
|
||||
//
|
||||
// FLEXPropertyEditorViewController.m
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 5/20/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXPropertyEditorViewController.h"
|
||||
#import "FLEXRuntimeUtility.h"
|
||||
#import "FLEXFieldEditorView.h"
|
||||
#import "FLEXArgumentInputView.h"
|
||||
#import "FLEXArgumentInputViewFactory.h"
|
||||
#import "FLEXArgumentInputSwitchView.h"
|
||||
|
||||
@interface FLEXPropertyEditorViewController () <FLEXArgumentInputViewDelegate>
|
||||
|
||||
@property (nonatomic, assign) objc_property_t property;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXPropertyEditorViewController
|
||||
|
||||
- (id)initWithTarget:(id)target property:(objc_property_t)property
|
||||
{
|
||||
self = [super initWithTarget:target];
|
||||
if (self) {
|
||||
self.property = property;
|
||||
self.title = @"Property";
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)viewDidLoad
|
||||
{
|
||||
[super viewDidLoad];
|
||||
|
||||
self.fieldEditorView.fieldDescription = [FLEXRuntimeUtility fullDescriptionForProperty:self.property];
|
||||
id currentValue = [FLEXRuntimeUtility valueForProperty:self.property onObject:self.target];
|
||||
self.setterButton.enabled = [[self class] canEditProperty:self.property currentValue:currentValue];
|
||||
|
||||
const char *typeEncoding = [[FLEXRuntimeUtility typeEncodingForProperty:self.property] UTF8String];
|
||||
FLEXArgumentInputView *inputView = [FLEXArgumentInputViewFactory argumentInputViewForTypeEncoding:typeEncoding];
|
||||
inputView.backgroundColor = self.view.backgroundColor;
|
||||
inputView.inputValue = [FLEXRuntimeUtility valueForProperty:self.property onObject:self.target];
|
||||
inputView.delegate = self;
|
||||
self.fieldEditorView.argumentInputViews = @[inputView];
|
||||
|
||||
// Don't show a "set" button for switches - just call the setter immediately after the switch toggles.
|
||||
if ([inputView isKindOfClass:[FLEXArgumentInputSwitchView class]]) {
|
||||
self.navigationItem.rightBarButtonItem = nil;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)actionButtonPressed:(id)sender
|
||||
{
|
||||
[super actionButtonPressed:sender];
|
||||
|
||||
id userInputObject = self.firstInputView.inputValue;
|
||||
NSArray *arguments = userInputObject ? @[userInputObject] : nil;
|
||||
SEL setterSelector = [FLEXRuntimeUtility setterSelectorForProperty:self.property];
|
||||
NSError *error = nil;
|
||||
[FLEXRuntimeUtility performSelector:setterSelector onObject:self.target withArguments:arguments error:&error];
|
||||
if (error) {
|
||||
NSString *title = @"Property Setter Failed";
|
||||
NSString *message = [error localizedDescription];
|
||||
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:title message:message delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];
|
||||
[alert show];
|
||||
self.firstInputView.inputValue = [FLEXRuntimeUtility valueForProperty:self.property onObject:self.target];
|
||||
} else {
|
||||
// If the setter was called without error, pop the view controller to indicate that and make the user's life easier.
|
||||
// Don't do this for simulated taps on the action button (i.e. from switch/BOOL editors). The experience is weird there.
|
||||
if (sender) {
|
||||
[self.navigationController popViewControllerAnimated:YES];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)argumentInputViewValueDidChange:(FLEXArgumentInputView *)argumentInputView
|
||||
{
|
||||
if ([argumentInputView isKindOfClass:[FLEXArgumentInputSwitchView class]]) {
|
||||
[self actionButtonPressed:nil];
|
||||
}
|
||||
}
|
||||
|
||||
+ (BOOL)canEditProperty:(objc_property_t)property currentValue:(id)value
|
||||
{
|
||||
const char *typeEncoding = [[FLEXRuntimeUtility typeEncodingForProperty:property] UTF8String];
|
||||
BOOL canEditType = [FLEXArgumentInputViewFactory canEditFieldWithTypeEncoding:typeEncoding currentValue:value];
|
||||
BOOL isReadonly = [FLEXRuntimeUtility isReadonlyProperty:property];
|
||||
return canEditType && !isReadonly;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,55 @@
|
||||
//
|
||||
// FLEXVariableEditorViewController.h
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 5/16/14.
|
||||
// Copyright (c) 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@class FLEXFieldEditorView;
|
||||
@class FLEXArgumentInputView;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/// An abstract screen for editing or configuring one or more variables.
|
||||
/// "Target" is the target of the edit operation, and "data" is the data
|
||||
/// you want to mutate or pass to the target when the action is performed.
|
||||
/// The action may be something like calling a method, setting an ivar, etc.
|
||||
@interface FLEXVariableEditorViewController : UIViewController {
|
||||
@protected
|
||||
id _target;
|
||||
_Nullable id _data;
|
||||
void (^_Nullable _commitHandler)(void);
|
||||
}
|
||||
|
||||
/// @param target The target of the operation
|
||||
/// @param data The data associated with the operation
|
||||
/// @param onCommit An action to perform when the data changes
|
||||
+ (instancetype)target:(id)target data:(nullable id)data commitHandler:(void(^_Nullable)(void))onCommit;
|
||||
/// @param target The target of the operation
|
||||
/// @param data The data associated with the operation
|
||||
/// @param onCommit An action to perform when the data changes
|
||||
- (id)initWithTarget:(id)target data:(nullable id)data commitHandler:(void(^_Nullable)(void))onCommit;
|
||||
|
||||
@property (nonatomic, readonly) id target;
|
||||
|
||||
/// Convenience accessor since many subclasses only use one input view
|
||||
@property (nonatomic, readonly, nullable) FLEXArgumentInputView *firstInputView;
|
||||
|
||||
@property (nonatomic, readonly) FLEXFieldEditorView *fieldEditorView;
|
||||
/// Subclasses can change the button title via the button's \c title property
|
||||
@property (nonatomic, readonly) UIBarButtonItem *actionButton;
|
||||
|
||||
/// Subclasses should override to provide "set" functionality.
|
||||
/// The commit handler--if present--is called here.
|
||||
- (void)actionButtonPressed:(nullable id)sender;
|
||||
|
||||
/// Pushes an explorer view controller for the given object
|
||||
/// or pops the current view controller.
|
||||
- (void)exploreObjectOrPopViewController:(nullable id)objectOrNil;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,141 @@
|
||||
//
|
||||
// FLEXVariableEditorViewController.m
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 5/16/14.
|
||||
// Copyright (c) 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXColor.h"
|
||||
#import "FLEXVariableEditorViewController.h"
|
||||
#import "FLEXFieldEditorView.h"
|
||||
#import "FLEXRuntimeUtility.h"
|
||||
#import "FLEXUtility.h"
|
||||
#import "FLEXObjectExplorerFactory.h"
|
||||
#import "FLEXArgumentInputView.h"
|
||||
#import "FLEXArgumentInputViewFactory.h"
|
||||
#import "FLEXObjectExplorerViewController.h"
|
||||
#import "UIBarButtonItem+FLEX.h"
|
||||
|
||||
@interface FLEXVariableEditorViewController () <UIScrollViewDelegate>
|
||||
@property (nonatomic) UIScrollView *scrollView;
|
||||
@end
|
||||
|
||||
@implementation FLEXVariableEditorViewController
|
||||
|
||||
#pragma mark - Initialization
|
||||
|
||||
+ (instancetype)target:(id)target data:(nullable id)data commitHandler:(void(^_Nullable)(void))onCommit {
|
||||
return [[self alloc] initWithTarget:target data:data commitHandler:onCommit];
|
||||
}
|
||||
|
||||
- (id)initWithTarget:(id)target data:(nullable id)data commitHandler:(void(^_Nullable)(void))onCommit {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_target = target;
|
||||
_data = data;
|
||||
_commitHandler = onCommit;
|
||||
[NSNotificationCenter.defaultCenter
|
||||
addObserver:self selector:@selector(keyboardDidShow:)
|
||||
name:UIKeyboardDidShowNotification object:nil
|
||||
];
|
||||
[NSNotificationCenter.defaultCenter
|
||||
addObserver:self selector:@selector(keyboardWillHide:)
|
||||
name:UIKeyboardWillHideNotification object:nil
|
||||
];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[NSNotificationCenter.defaultCenter removeObserver:self];
|
||||
}
|
||||
|
||||
#pragma mark - UIViewController methods
|
||||
|
||||
- (void)keyboardDidShow:(NSNotification *)notification {
|
||||
CGRect keyboardRectInWindow = [[[notification userInfo] objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
|
||||
CGSize keyboardSize = [self.view convertRect:keyboardRectInWindow fromView:nil].size;
|
||||
UIEdgeInsets scrollInsets = self.scrollView.contentInset;
|
||||
scrollInsets.bottom = keyboardSize.height;
|
||||
self.scrollView.contentInset = scrollInsets;
|
||||
self.scrollView.scrollIndicatorInsets = scrollInsets;
|
||||
|
||||
// Find the active input view and scroll to make sure it's visible.
|
||||
for (FLEXArgumentInputView *argumentInputView in self.fieldEditorView.argumentInputViews) {
|
||||
if (argumentInputView.inputViewIsFirstResponder) {
|
||||
CGRect scrollToVisibleRect = [self.scrollView convertRect:argumentInputView.bounds fromView:argumentInputView];
|
||||
[self.scrollView scrollRectToVisible:scrollToVisibleRect animated:YES];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)keyboardWillHide:(NSNotification *)notification {
|
||||
UIEdgeInsets scrollInsets = self.scrollView.contentInset;
|
||||
scrollInsets.bottom = 0.0;
|
||||
self.scrollView.contentInset = scrollInsets;
|
||||
self.scrollView.scrollIndicatorInsets = scrollInsets;
|
||||
}
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
|
||||
self.view.backgroundColor = FLEXColor.scrollViewBackgroundColor;
|
||||
|
||||
self.scrollView = [[UIScrollView alloc] initWithFrame:self.view.bounds];
|
||||
self.scrollView.backgroundColor = self.view.backgroundColor;
|
||||
self.scrollView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
|
||||
self.scrollView.delegate = self;
|
||||
[self.view addSubview:self.scrollView];
|
||||
|
||||
_fieldEditorView = [FLEXFieldEditorView new];
|
||||
self.fieldEditorView.targetDescription = [NSString stringWithFormat:@"%@ %p", [self.target class], self.target];
|
||||
[self.scrollView addSubview:self.fieldEditorView];
|
||||
|
||||
_actionButton = [[UIBarButtonItem alloc]
|
||||
initWithTitle:@"Set"
|
||||
style:UIBarButtonItemStyleDone
|
||||
target:self
|
||||
action:@selector(actionButtonPressed:)
|
||||
];
|
||||
|
||||
self.navigationController.toolbarHidden = NO;
|
||||
self.toolbarItems = @[UIBarButtonItem.flex_flexibleSpace, self.actionButton];
|
||||
}
|
||||
|
||||
- (void)viewWillLayoutSubviews {
|
||||
CGSize constrainSize = CGSizeMake(self.scrollView.bounds.size.width, CGFLOAT_MAX);
|
||||
CGSize fieldEditorSize = [self.fieldEditorView sizeThatFits:constrainSize];
|
||||
self.fieldEditorView.frame = CGRectMake(0, 0, fieldEditorSize.width, fieldEditorSize.height);
|
||||
self.scrollView.contentSize = fieldEditorSize;
|
||||
}
|
||||
|
||||
#pragma mark - Public
|
||||
|
||||
- (FLEXArgumentInputView *)firstInputView {
|
||||
return [self.fieldEditorView argumentInputViews].firstObject;
|
||||
}
|
||||
|
||||
- (void)actionButtonPressed:(id)sender {
|
||||
// Subclasses can override
|
||||
[self.fieldEditorView endEditing:YES];
|
||||
if (_commitHandler) {
|
||||
_commitHandler();
|
||||
}
|
||||
}
|
||||
|
||||
- (void)exploreObjectOrPopViewController:(id)objectOrNil {
|
||||
if (objectOrNil) {
|
||||
// For non-nil (or void) return types, push an explorer view controller to display the object
|
||||
FLEXObjectExplorerViewController *explorerViewController = [FLEXObjectExplorerFactory explorerViewControllerForObject:objectOrNil];
|
||||
[self.navigationController pushViewController:explorerViewController animated:YES];
|
||||
} else {
|
||||
// If we didn't get a returned object but the method call succeeded,
|
||||
// pop this view controller off the stack to indicate that the call went through.
|
||||
[self.navigationController popViewControllerAnimated:YES];
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,49 +0,0 @@
|
||||
//
|
||||
// FLEXExplorerToolbar.h
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 4/4/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@class FLEXToolbarItem;
|
||||
|
||||
@interface FLEXExplorerToolbar : UIView
|
||||
|
||||
/// Toolbar item for selecting views.
|
||||
/// Users of the toolbar can configure the enabled/selected state and event targets/actions.
|
||||
@property (nonatomic, strong, readonly) FLEXToolbarItem *selectItem;
|
||||
|
||||
/// Toolbar item for presenting a list with the view hierarchy.
|
||||
/// Users of the toolbar can configure the enabled state and event targets/actions.
|
||||
@property (nonatomic, strong, readonly) FLEXToolbarItem *hierarchyItem;
|
||||
|
||||
/// Toolbar item for moving views.
|
||||
/// Users of the toolbar can configure the enabled/selected state and event targets/actions.
|
||||
@property (nonatomic, strong, readonly) FLEXToolbarItem *moveItem;
|
||||
|
||||
/// Toolbar item for inspecting details of the selected view.
|
||||
/// Users of the toolbar can configure the enabled state and event targets/actions.
|
||||
@property (nonatomic, strong, readonly) FLEXToolbarItem *globalsItem;
|
||||
|
||||
/// Toolbar item for hiding the explorer.
|
||||
/// Users of the toolbar can configure the event targets/actions.
|
||||
@property (nonatomic, strong, readonly) FLEXToolbarItem *closeItem;
|
||||
|
||||
/// A view for moving the entire toolbar.
|
||||
/// Users of the toolbar can attach a pan gesture recognizer to decide how to reposition the toolbar.
|
||||
@property (nonatomic, strong, readonly) UIView *dragHandle;
|
||||
|
||||
/// A color matching the overlay on color on the selected view.
|
||||
@property (nonatomic, strong) UIColor *selectedViewOverlayColor;
|
||||
|
||||
/// Description text for the selected view displayed below the toolbar items.
|
||||
@property (nonatomic, copy) NSString *selectedViewDescription;
|
||||
|
||||
/// Area where details of the selected view are shown
|
||||
/// Users of the toolbar can attach a tap gesture recognizer to show additional details.
|
||||
@property (nonatomic, strong, readonly) UIView *selectedViewDescriptionContainer;
|
||||
|
||||
@end
|
||||
@@ -1,226 +0,0 @@
|
||||
//
|
||||
// FLEXExplorerToolbar.m
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 4/4/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXExplorerToolbar.h"
|
||||
#import "FLEXToolbarItem.h"
|
||||
#import "FLEXResources.h"
|
||||
#import "FLEXUtility.h"
|
||||
|
||||
@interface FLEXExplorerToolbar ()
|
||||
|
||||
@property (nonatomic, strong, readwrite) FLEXToolbarItem *selectItem;
|
||||
@property (nonatomic, strong, readwrite) FLEXToolbarItem *moveItem;
|
||||
@property (nonatomic, strong, readwrite) FLEXToolbarItem *globalsItem;
|
||||
@property (nonatomic, strong, readwrite) FLEXToolbarItem *closeItem;
|
||||
@property (nonatomic, strong, readwrite) FLEXToolbarItem *hierarchyItem;
|
||||
@property (nonatomic, strong, readwrite) UIView *dragHandle;
|
||||
|
||||
@property (nonatomic, strong) UIImageView *dragHandleImageView;
|
||||
|
||||
@property (nonatomic, strong) NSArray *toolbarItems;
|
||||
|
||||
@property (nonatomic, strong) UIView *selectedViewDescriptionContainer;
|
||||
@property (nonatomic, strong) UIView *selectedViewColorIndicator;
|
||||
@property (nonatomic, strong) UILabel *selectedViewDescriptionLabel;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXExplorerToolbar
|
||||
|
||||
- (id)initWithFrame:(CGRect)frame
|
||||
{
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
NSMutableArray *toolbarItems = [NSMutableArray array];
|
||||
|
||||
self.dragHandle = [[UIView alloc] init];
|
||||
self.dragHandle.backgroundColor = [FLEXToolbarItem defaultBackgroundColor];
|
||||
[self addSubview:self.dragHandle];
|
||||
|
||||
UIImage *dragHandle = [FLEXResources dragHandle];
|
||||
self.dragHandleImageView = [[UIImageView alloc] initWithImage:dragHandle];
|
||||
[self.dragHandle addSubview:self.dragHandleImageView];
|
||||
|
||||
UIImage *globalsIcon = [FLEXResources globeIcon];
|
||||
self.globalsItem = [FLEXToolbarItem toolbarItemWithTitle:@"globals" image:globalsIcon];
|
||||
[self addSubview:self.globalsItem];
|
||||
[toolbarItems addObject:self.globalsItem];
|
||||
|
||||
UIImage *listIcon = [FLEXResources listIcon];
|
||||
self.hierarchyItem = [FLEXToolbarItem toolbarItemWithTitle:@"views" image:listIcon];
|
||||
[self addSubview:self.hierarchyItem];
|
||||
[toolbarItems addObject:self.hierarchyItem];
|
||||
|
||||
UIImage *selectIcon = [FLEXResources selectIcon];
|
||||
self.selectItem = [FLEXToolbarItem toolbarItemWithTitle:@"select" image:selectIcon];
|
||||
[self addSubview:self.selectItem];
|
||||
[toolbarItems addObject:self.selectItem];
|
||||
|
||||
UIImage *moveIcon = [FLEXResources moveIcon];
|
||||
self.moveItem = [FLEXToolbarItem toolbarItemWithTitle:@"move" image:moveIcon];
|
||||
[self addSubview:self.moveItem];
|
||||
[toolbarItems addObject:self.moveItem];
|
||||
|
||||
UIImage *closeIcon = [FLEXResources closeIcon];
|
||||
self.closeItem = [FLEXToolbarItem toolbarItemWithTitle:@"close" image:closeIcon];
|
||||
[self addSubview:self.closeItem];
|
||||
[toolbarItems addObject:self.closeItem];
|
||||
|
||||
self.toolbarItems = toolbarItems;
|
||||
self.backgroundColor = [UIColor clearColor];
|
||||
|
||||
self.selectedViewDescriptionContainer = [[UIView alloc] init];
|
||||
self.selectedViewDescriptionContainer.backgroundColor = [UIColor colorWithWhite:0.9 alpha:0.95];
|
||||
self.selectedViewDescriptionContainer.hidden = YES;
|
||||
[self addSubview:self.selectedViewDescriptionContainer];
|
||||
|
||||
self.selectedViewColorIndicator = [[UIView alloc] init];
|
||||
self.selectedViewColorIndicator.backgroundColor = [UIColor redColor];
|
||||
[self.selectedViewDescriptionContainer addSubview:self.selectedViewColorIndicator];
|
||||
|
||||
self.selectedViewDescriptionLabel = [[UILabel alloc] init];
|
||||
self.selectedViewDescriptionLabel.backgroundColor = [UIColor clearColor];
|
||||
self.selectedViewDescriptionLabel.font = [[self class] descriptionLabelFont];
|
||||
[self.selectedViewDescriptionContainer addSubview:self.selectedViewDescriptionLabel];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)layoutSubviews
|
||||
{
|
||||
[super layoutSubviews];
|
||||
|
||||
// Drag Handle
|
||||
const CGFloat kToolbarItemHeight = [[self class] toolbarItemHeight];
|
||||
self.dragHandle.frame = CGRectMake(self.bounds.origin.x, self.bounds.origin.y, [[self class] dragHandleWidth], kToolbarItemHeight);
|
||||
CGRect dragHandleImageFrame = self.dragHandleImageView.frame;
|
||||
dragHandleImageFrame.origin.x = FLEXFloor((self.dragHandle.frame.size.width - dragHandleImageFrame.size.width) / 2.0);
|
||||
dragHandleImageFrame.origin.y = FLEXFloor((self.dragHandle.frame.size.height - dragHandleImageFrame.size.height) / 2.0);
|
||||
self.dragHandleImageView.frame = dragHandleImageFrame;
|
||||
|
||||
|
||||
// Toolbar Items
|
||||
CGFloat originX = CGRectGetMaxX(self.dragHandle.frame);
|
||||
CGFloat originY = self.bounds.origin.y;
|
||||
CGFloat height = kToolbarItemHeight;
|
||||
CGFloat width = FLEXFloor((CGRectGetMaxX(self.bounds) - originX) / [self.toolbarItems count]);
|
||||
for (UIView *toolbarItem in self.toolbarItems) {
|
||||
toolbarItem.frame = CGRectMake(originX, originY, width, height);
|
||||
originX = CGRectGetMaxX(toolbarItem.frame);
|
||||
}
|
||||
|
||||
// Make sure the last toolbar item goes to the edge to account for any accumulated rounding effects.
|
||||
UIView *lastToolbarItem = [self.toolbarItems lastObject];
|
||||
CGRect lastToolbarItemFrame = lastToolbarItem.frame;
|
||||
lastToolbarItemFrame.size.width = CGRectGetMaxX(self.bounds) - lastToolbarItemFrame.origin.x;
|
||||
lastToolbarItem.frame = lastToolbarItemFrame;
|
||||
|
||||
const CGFloat kSelectedViewColorDiameter = [[self class] selectedViewColorIndicatorDiameter];
|
||||
const CGFloat kDescriptionLabelHeight = [[self class] descriptionLabelHeight];
|
||||
const CGFloat kHorizontalPadding = [[self class] horizontalPadding];
|
||||
const CGFloat kDescriptionVerticalPadding = [[self class] descriptionVerticalPadding];
|
||||
const CGFloat kDescriptionContainerHeight = [[self class] descriptionContainerHeight];
|
||||
|
||||
CGRect descriptionContainerFrame = CGRectZero;
|
||||
descriptionContainerFrame.size.height = kDescriptionContainerHeight;
|
||||
descriptionContainerFrame.origin.y = CGRectGetMaxY(self.bounds) - kDescriptionContainerHeight;
|
||||
descriptionContainerFrame.size.width = self.bounds.size.width;
|
||||
self.selectedViewDescriptionContainer.frame = descriptionContainerFrame;
|
||||
|
||||
// Selected View Color
|
||||
CGRect selectedViewColorFrame = CGRectZero;
|
||||
selectedViewColorFrame.size.width = kSelectedViewColorDiameter;
|
||||
selectedViewColorFrame.size.height = kSelectedViewColorDiameter;
|
||||
selectedViewColorFrame.origin.x = kHorizontalPadding;
|
||||
selectedViewColorFrame.origin.y = FLEXFloor((kDescriptionContainerHeight - kSelectedViewColorDiameter) / 2.0);
|
||||
self.selectedViewColorIndicator.frame = selectedViewColorFrame;
|
||||
self.selectedViewColorIndicator.layer.cornerRadius = ceil(selectedViewColorFrame.size.height / 2.0);
|
||||
|
||||
// Selected View Description
|
||||
CGRect descriptionLabelFrame = CGRectZero;
|
||||
CGFloat descriptionOriginX = CGRectGetMaxX(selectedViewColorFrame) + kHorizontalPadding;
|
||||
descriptionLabelFrame.size.height = kDescriptionLabelHeight;
|
||||
descriptionLabelFrame.origin.x = descriptionOriginX;
|
||||
descriptionLabelFrame.origin.y = kDescriptionVerticalPadding;
|
||||
descriptionLabelFrame.size.width = CGRectGetMaxX(self.selectedViewDescriptionContainer.bounds) - kHorizontalPadding - descriptionOriginX;
|
||||
self.selectedViewDescriptionLabel.frame = descriptionLabelFrame;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Setter Overrides
|
||||
|
||||
- (void)setSelectedViewOverlayColor:(UIColor *)selectedViewOverlayColor
|
||||
{
|
||||
if (![_selectedViewOverlayColor isEqual:selectedViewOverlayColor]) {
|
||||
_selectedViewOverlayColor = selectedViewOverlayColor;
|
||||
self.selectedViewColorIndicator.backgroundColor = selectedViewOverlayColor;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setSelectedViewDescription:(NSString *)selectedViewDescription
|
||||
{
|
||||
if (![_selectedViewDescription isEqual:selectedViewDescription]) {
|
||||
_selectedViewDescription = selectedViewDescription;
|
||||
self.selectedViewDescriptionLabel.text = selectedViewDescription;
|
||||
BOOL showDescription = [selectedViewDescription length] > 0;
|
||||
self.selectedViewDescriptionContainer.hidden = !showDescription;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Sizing Convenience Methods
|
||||
|
||||
+ (UIFont *)descriptionLabelFont
|
||||
{
|
||||
return [UIFont systemFontOfSize:12.0];
|
||||
}
|
||||
|
||||
+ (CGFloat)toolbarItemHeight
|
||||
{
|
||||
return 44.0;
|
||||
}
|
||||
|
||||
+ (CGFloat)dragHandleWidth
|
||||
{
|
||||
return 30.0;
|
||||
}
|
||||
|
||||
+ (CGFloat)descriptionLabelHeight
|
||||
{
|
||||
return ceil([[self descriptionLabelFont] lineHeight]);
|
||||
}
|
||||
|
||||
+ (CGFloat)descriptionVerticalPadding
|
||||
{
|
||||
return 2.0;
|
||||
}
|
||||
|
||||
+ (CGFloat)descriptionContainerHeight
|
||||
{
|
||||
return [self descriptionVerticalPadding] * 2.0 + [self descriptionLabelHeight];
|
||||
}
|
||||
|
||||
+ (CGFloat)selectedViewColorIndicatorDiameter
|
||||
{
|
||||
return ceil([self descriptionLabelHeight] / 2.0);
|
||||
}
|
||||
|
||||
+ (CGFloat)horizontalPadding
|
||||
{
|
||||
return 11.0;
|
||||
}
|
||||
|
||||
- (CGSize)sizeThatFits:(CGSize)size
|
||||
{
|
||||
CGFloat height = 0.0;
|
||||
height += [[self class] toolbarItemHeight];
|
||||
height += [[self class] descriptionContainerHeight];
|
||||
return CGSizeMake(size.width, height);
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,25 +0,0 @@
|
||||
//
|
||||
// FLEXExplorerViewController.h
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 4/4/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@protocol FLEXExplorerViewControllerDelegate;
|
||||
|
||||
@interface FLEXExplorerViewController : UIViewController
|
||||
|
||||
@property (nonatomic, weak) id <FLEXExplorerViewControllerDelegate> delegate;
|
||||
|
||||
- (BOOL)shouldReceiveTouchAtWindowPoint:(CGPoint)pointInWindowCoordinates;
|
||||
|
||||
@end
|
||||
|
||||
@protocol FLEXExplorerViewControllerDelegate <NSObject>
|
||||
|
||||
- (void)explorerViewControllerDidFinish:(FLEXExplorerViewController *)explorerViewController;
|
||||
|
||||
@end
|
||||
@@ -1,874 +0,0 @@
|
||||
//
|
||||
// FLEXExplorerViewController.m
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 4/4/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXExplorerViewController.h"
|
||||
#import "FLEXExplorerToolbar.h"
|
||||
#import "FLEXToolbarItem.h"
|
||||
#import "FLEXUtility.h"
|
||||
#import "FLEXHierarchyTableViewController.h"
|
||||
#import "FLEXGlobalsTableViewController.h"
|
||||
#import "FLEXObjectExplorerViewController.h"
|
||||
#import "FLEXObjectExplorerFactory.h"
|
||||
|
||||
typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
FLEXExplorerModeDefault,
|
||||
FLEXExplorerModeSelect,
|
||||
FLEXExplorerModeMove
|
||||
};
|
||||
|
||||
@interface FLEXExplorerViewController () <FLEXHierarchyTableViewControllerDelegate, FLEXGlobalsTableViewControllerDelegate>
|
||||
|
||||
@property (nonatomic, strong) FLEXExplorerToolbar *explorerToolbar;
|
||||
|
||||
/// Tracks the currently active tool/mode
|
||||
@property (nonatomic, assign) FLEXExplorerMode currentMode;
|
||||
|
||||
/// Gesture recognizer for dragging a view in move mode
|
||||
@property (nonatomic, strong) UIPanGestureRecognizer *movePanGR;
|
||||
|
||||
/// Gesture recognizer for showing additional details on the selected view
|
||||
@property (nonatomic, strong) UITapGestureRecognizer *detailsTapGR;
|
||||
|
||||
/// Only valid while a move pan gesture is in progress.
|
||||
@property (nonatomic, assign) CGRect selectedViewFrameBeforeDragging;
|
||||
|
||||
/// Only valid while a toolbar drag pan gesture is in progress.
|
||||
@property (nonatomic, assign) CGRect toolbarFrameBeforeDragging;
|
||||
|
||||
/// Borders of all the visible views in the hierarchy at the selection point.
|
||||
/// The keys are NSValues with the correponding view (nonretained).
|
||||
@property (nonatomic, strong) NSDictionary *outlineViewsForVisibleViews;
|
||||
|
||||
/// The actual views at the selection point with the deepest view last.
|
||||
@property (nonatomic, strong) NSArray *viewsAtTapPoint;
|
||||
|
||||
/// The view that we're currently highlighting with an overlay and displaying details for.
|
||||
@property (nonatomic, strong) UIView *selectedView;
|
||||
|
||||
/// A colored transparent overlay to indicate that the view is selected.
|
||||
@property (nonatomic, strong) UIView *selectedViewOverlay;
|
||||
|
||||
/// Tracked so we can restore the key window after dismissing a modal.
|
||||
/// We need to become key after modal presentation so we can correctly capture intput.
|
||||
/// If we're just showing the toolbar, we want the main app's window to remain key so that we don't interfere with input, status bar, etc.
|
||||
@property (nonatomic, strong) UIWindow *previousKeyWindow;
|
||||
|
||||
/// Similar to the previousKeyWindow property above, we need to track status bar styling if
|
||||
/// the app doesn't use view controller based status bar management. When we present a modal,
|
||||
/// we want to change the status bar style to UIStausBarStyleDefault. Before changing, we stash
|
||||
/// the current style. On dismissal, we return the staus bar to the style that the app was using previously.
|
||||
@property (nonatomic, assign) UIStatusBarStyle previousStatusBarStyle;
|
||||
|
||||
/// All views that we're KVOing. Used to help us clean up properly.
|
||||
@property (nonatomic, strong) NSMutableSet *observedViews;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXExplorerViewController
|
||||
|
||||
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
|
||||
{
|
||||
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
|
||||
if (self) {
|
||||
self.observedViews = [NSMutableSet set];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
-(void)dealloc
|
||||
{
|
||||
for (UIView *view in _observedViews) {
|
||||
[self stopObservingView:view];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)viewDidLoad
|
||||
{
|
||||
[super viewDidLoad];
|
||||
|
||||
// Toolbar
|
||||
self.explorerToolbar = [[FLEXExplorerToolbar alloc] init];
|
||||
CGSize toolbarSize = [self.explorerToolbar sizeThatFits:self.view.bounds.size];
|
||||
// Start the toolbar off below any bars that may be at the top of the view.
|
||||
CGFloat toolbarOriginY = 100.0;
|
||||
self.explorerToolbar.frame = CGRectMake(0.0, toolbarOriginY, toolbarSize.width, toolbarSize.height);
|
||||
self.explorerToolbar.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleBottomMargin | UIViewAutoresizingFlexibleTopMargin;
|
||||
[self.view addSubview:self.explorerToolbar];
|
||||
[self setupToolbarActions];
|
||||
[self setupToolbarGestures];
|
||||
|
||||
// View selection
|
||||
UITapGestureRecognizer *selectionTapGR = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleSelectionTap:)];
|
||||
[self.view addGestureRecognizer:selectionTapGR];
|
||||
|
||||
// View moving
|
||||
self.movePanGR = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handleMovePan:)];
|
||||
self.movePanGR.enabled = self.currentMode == FLEXExplorerModeMove;
|
||||
[self.view addGestureRecognizer:self.movePanGR];
|
||||
}
|
||||
|
||||
- (void)viewWillAppear:(BOOL)animated
|
||||
{
|
||||
[super viewWillAppear:animated];
|
||||
|
||||
[self updateButtonStates];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Status Bar Wrangling for iOS 7
|
||||
|
||||
// Try to get the preferred status bar properties from the app's root view controller (not us).
|
||||
// In general, our window shouldn't be the key window when this view controller is asked about the status bar.
|
||||
// However, we guard against infinite recursion and provide a reasonable default for status bar behavior in case our window is the keyWindow.
|
||||
|
||||
- (UIViewController *)viewControllerForStatusBarAndOrientationProperties
|
||||
{
|
||||
UIViewController *viewControllerToAsk = [[[UIApplication sharedApplication] keyWindow] rootViewController];
|
||||
|
||||
// On iPhone, modal view controllers get asked
|
||||
if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) {
|
||||
while (viewControllerToAsk.presentedViewController) {
|
||||
viewControllerToAsk = viewControllerToAsk.presentedViewController;
|
||||
}
|
||||
}
|
||||
|
||||
return viewControllerToAsk;
|
||||
}
|
||||
|
||||
- (UIStatusBarStyle)preferredStatusBarStyle
|
||||
{
|
||||
UIViewController *viewControllerToAsk = [self viewControllerForStatusBarAndOrientationProperties];
|
||||
UIStatusBarStyle preferredStyle = UIStatusBarStyleDefault;
|
||||
if (viewControllerToAsk && viewControllerToAsk != self) {
|
||||
// We might need to foward to a child
|
||||
UIViewController *childViewControllerToAsk = [viewControllerToAsk childViewControllerForStatusBarStyle];
|
||||
if (childViewControllerToAsk) {
|
||||
preferredStyle = [childViewControllerToAsk preferredStatusBarStyle];
|
||||
} else {
|
||||
preferredStyle = [viewControllerToAsk preferredStatusBarStyle];
|
||||
}
|
||||
}
|
||||
return preferredStyle;
|
||||
}
|
||||
|
||||
- (UIStatusBarAnimation)preferredStatusBarUpdateAnimation
|
||||
{
|
||||
UIViewController *viewControllerToAsk = [self viewControllerForStatusBarAndOrientationProperties];
|
||||
UIStatusBarAnimation preferredAnimation = UIStatusBarAnimationFade;
|
||||
if (viewControllerToAsk && viewControllerToAsk != self) {
|
||||
preferredAnimation = [viewControllerToAsk preferredStatusBarUpdateAnimation];
|
||||
}
|
||||
return preferredAnimation;
|
||||
}
|
||||
|
||||
- (BOOL)prefersStatusBarHidden
|
||||
{
|
||||
UIViewController *viewControllerToAsk = [self viewControllerForStatusBarAndOrientationProperties];
|
||||
BOOL prefersHidden = NO;
|
||||
if (viewControllerToAsk && viewControllerToAsk != self) {
|
||||
// Again, we might need to forward to a child
|
||||
UIViewController *childViewControllerToAsk = [viewControllerToAsk childViewControllerForStatusBarHidden];
|
||||
if (childViewControllerToAsk) {
|
||||
prefersHidden = [childViewControllerToAsk prefersStatusBarHidden];
|
||||
} else {
|
||||
prefersHidden = [viewControllerToAsk prefersStatusBarHidden];
|
||||
}
|
||||
}
|
||||
return prefersHidden;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Rotation
|
||||
|
||||
- (NSUInteger)supportedInterfaceOrientations
|
||||
{
|
||||
UIViewController *viewControllerToAsk = [self viewControllerForStatusBarAndOrientationProperties];
|
||||
NSUInteger supportedOrientations = [FLEXUtility infoPlistSupportedInterfaceOrientationsMask];
|
||||
if (viewControllerToAsk && viewControllerToAsk != self) {
|
||||
supportedOrientations = [viewControllerToAsk supportedInterfaceOrientations];
|
||||
}
|
||||
|
||||
// The UIViewController docs state that this method must not return zero.
|
||||
// If we weren't able to get a valid value for the supported interface orientations, default to all supported.
|
||||
if (supportedOrientations == 0) {
|
||||
supportedOrientations = UIInterfaceOrientationMaskAll;
|
||||
}
|
||||
|
||||
return supportedOrientations;
|
||||
}
|
||||
|
||||
- (BOOL)shouldAutorotate
|
||||
{
|
||||
UIViewController *viewControllerToAsk = [self viewControllerForStatusBarAndOrientationProperties];
|
||||
BOOL shouldAutorotate = YES;
|
||||
if (viewControllerToAsk && viewControllerToAsk != self) {
|
||||
shouldAutorotate = [viewControllerToAsk shouldAutorotate];
|
||||
}
|
||||
return shouldAutorotate;
|
||||
}
|
||||
|
||||
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
|
||||
{
|
||||
for (UIView *outlineView in [self.outlineViewsForVisibleViews allValues]) {
|
||||
outlineView.hidden = YES;
|
||||
}
|
||||
self.selectedViewOverlay.hidden = YES;
|
||||
}
|
||||
|
||||
- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation
|
||||
{
|
||||
for (UIView *view in self.viewsAtTapPoint) {
|
||||
NSValue *key = [NSValue valueWithNonretainedObject:view];
|
||||
UIView *outlineView = self.outlineViewsForVisibleViews[key];
|
||||
outlineView.frame = [self frameInLocalCoordinatesForView:view];
|
||||
if (self.currentMode == FLEXExplorerModeSelect) {
|
||||
outlineView.hidden = NO;
|
||||
}
|
||||
}
|
||||
|
||||
if (self.selectedView) {
|
||||
self.selectedViewOverlay.frame = [self frameInLocalCoordinatesForView:self.selectedView];
|
||||
self.selectedViewOverlay.hidden = NO;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Setter Overrides
|
||||
|
||||
- (void)setSelectedView:(UIView *)selectedView
|
||||
{
|
||||
if (![_selectedView isEqual:selectedView]) {
|
||||
if (![self.viewsAtTapPoint containsObject:_selectedView]) {
|
||||
[self stopObservingView:_selectedView];
|
||||
}
|
||||
|
||||
_selectedView = selectedView;
|
||||
|
||||
[self beginObservingView:selectedView];
|
||||
|
||||
// Update the toolbar and selected overlay
|
||||
self.explorerToolbar.selectedViewDescription = [FLEXUtility descriptionForView:selectedView includingFrame:YES];
|
||||
self.explorerToolbar.selectedViewOverlayColor = [FLEXUtility consistentRandomColorForObject:selectedView];;
|
||||
|
||||
if (selectedView) {
|
||||
if (!self.selectedViewOverlay) {
|
||||
self.selectedViewOverlay = [[UIView alloc] init];
|
||||
[self.view addSubview:self.selectedViewOverlay];
|
||||
self.selectedViewOverlay.layer.borderWidth = 1.0;
|
||||
}
|
||||
UIColor *outlineColor = [FLEXUtility consistentRandomColorForObject:selectedView];
|
||||
self.selectedViewOverlay.backgroundColor = [outlineColor colorWithAlphaComponent:0.2];
|
||||
self.selectedViewOverlay.layer.borderColor = [outlineColor CGColor];
|
||||
self.selectedViewOverlay.frame = [self.view convertRect:selectedView.bounds fromView:selectedView];
|
||||
|
||||
// Make sure the selected overlay is in front of all the other subviews except the toolbar, which should always stay on top.
|
||||
[self.view bringSubviewToFront:self.selectedViewOverlay];
|
||||
[self.view bringSubviewToFront:self.explorerToolbar];
|
||||
} else {
|
||||
[self.selectedViewOverlay removeFromSuperview];
|
||||
self.selectedViewOverlay = nil;
|
||||
}
|
||||
|
||||
// Some of the button states depend on whether we have a selected view.
|
||||
[self updateButtonStates];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setViewsAtTapPoint:(NSArray *)viewsAtTapPoint
|
||||
{
|
||||
if (![_viewsAtTapPoint isEqual:viewsAtTapPoint]) {
|
||||
for (UIView *view in _viewsAtTapPoint) {
|
||||
if (view != self.selectedView) {
|
||||
[self stopObservingView:view];
|
||||
}
|
||||
}
|
||||
|
||||
_viewsAtTapPoint = viewsAtTapPoint;
|
||||
|
||||
for (UIView *view in viewsAtTapPoint) {
|
||||
[self beginObservingView:view];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setCurrentMode:(FLEXExplorerMode)currentMode
|
||||
{
|
||||
if (_currentMode != currentMode) {
|
||||
_currentMode = currentMode;
|
||||
switch (currentMode) {
|
||||
case FLEXExplorerModeDefault:
|
||||
[self removeAndClearOutlineViews];
|
||||
self.viewsAtTapPoint = nil;
|
||||
self.selectedView = nil;
|
||||
break;
|
||||
|
||||
case FLEXExplorerModeSelect:
|
||||
// Make sure the outline views are unhidden in case we came from the move mode.
|
||||
for (id key in self.outlineViewsForVisibleViews) {
|
||||
UIView *outlineView = self.outlineViewsForVisibleViews[key];
|
||||
outlineView.hidden = NO;
|
||||
}
|
||||
break;
|
||||
|
||||
case FLEXExplorerModeMove:
|
||||
// Hide all the outline views to focus on the selected view, which is the only one that will move.
|
||||
for (id key in self.outlineViewsForVisibleViews) {
|
||||
UIView *outlineView = self.outlineViewsForVisibleViews[key];
|
||||
outlineView.hidden = YES;
|
||||
}
|
||||
break;
|
||||
}
|
||||
self.movePanGR.enabled = currentMode == FLEXExplorerModeMove;
|
||||
[self updateButtonStates];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - View Tracking
|
||||
|
||||
- (void)beginObservingView:(UIView *)view
|
||||
{
|
||||
// Bail if we're already observing this view or if there's nothing to observe.
|
||||
if (!view || [self.observedViews containsObject:view]) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (NSString *keyPath in [[self class] viewKeyPathsToTrack]) {
|
||||
[view addObserver:self forKeyPath:keyPath options:0 context:NULL];
|
||||
}
|
||||
|
||||
[self.observedViews addObject:view];
|
||||
}
|
||||
|
||||
- (void)stopObservingView:(UIView *)view
|
||||
{
|
||||
if (!view) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (NSString *keyPath in [[self class] viewKeyPathsToTrack]) {
|
||||
[view removeObserver:self forKeyPath:keyPath];
|
||||
}
|
||||
|
||||
[self.observedViews removeObject:view];
|
||||
}
|
||||
|
||||
+ (NSArray *)viewKeyPathsToTrack
|
||||
{
|
||||
static NSArray *trackedViewKeyPaths = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
NSString *frameKeyPath = NSStringFromSelector(@selector(frame));
|
||||
trackedViewKeyPaths = @[frameKeyPath];
|
||||
});
|
||||
return trackedViewKeyPaths;
|
||||
}
|
||||
|
||||
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
|
||||
{
|
||||
[self updateOverlayAndDescriptionForObjectIfNeeded:object];
|
||||
}
|
||||
|
||||
- (void)updateOverlayAndDescriptionForObjectIfNeeded:(id)object
|
||||
{
|
||||
NSUInteger indexOfView = [self.viewsAtTapPoint indexOfObject:object];
|
||||
if (indexOfView != NSNotFound) {
|
||||
UIView *view = [self.viewsAtTapPoint objectAtIndex:indexOfView];
|
||||
NSValue *key = [NSValue valueWithNonretainedObject:view];
|
||||
UIView *outline = [self.outlineViewsForVisibleViews objectForKey:key];
|
||||
if (outline) {
|
||||
outline.frame = [self frameInLocalCoordinatesForView:view];
|
||||
}
|
||||
}
|
||||
if (object == self.selectedView) {
|
||||
// Update the selected view description since we show the frame value there.
|
||||
self.explorerToolbar.selectedViewDescription = [FLEXUtility descriptionForView:self.selectedView includingFrame:YES];
|
||||
CGRect selectedViewOutlineFrame = [self frameInLocalCoordinatesForView:self.selectedView];
|
||||
self.selectedViewOverlay.frame = selectedViewOutlineFrame;
|
||||
}
|
||||
}
|
||||
|
||||
- (CGRect)frameInLocalCoordinatesForView:(UIView *)view
|
||||
{
|
||||
// First convert to window coordinates since the view may be in a different window than our view.
|
||||
CGRect frameInWindow = [view convertRect:view.bounds toView:nil];
|
||||
// Then convert from the window to our view's coordinate space.
|
||||
return [self.view convertRect:frameInWindow fromView:nil];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Toolbar Buttons
|
||||
|
||||
- (void)setupToolbarActions
|
||||
{
|
||||
[self.explorerToolbar.selectItem addTarget:self action:@selector(selectButtonTapped:) forControlEvents:UIControlEventTouchUpInside];
|
||||
[self.explorerToolbar.hierarchyItem addTarget:self action:@selector(hierarchyButtonTapped:) forControlEvents:UIControlEventTouchUpInside];
|
||||
[self.explorerToolbar.moveItem addTarget:self action:@selector(moveButtonTapped:) forControlEvents:UIControlEventTouchUpInside];
|
||||
[self.explorerToolbar.globalsItem addTarget:self action:@selector(globalsButtonTapped:) forControlEvents:UIControlEventTouchUpInside];
|
||||
[self.explorerToolbar.closeItem addTarget:self action:@selector(closeButtonTapped:) forControlEvents:UIControlEventTouchUpInside];
|
||||
}
|
||||
|
||||
- (void)selectButtonTapped:(FLEXToolbarItem *)sender
|
||||
{
|
||||
if (self.currentMode == FLEXExplorerModeSelect) {
|
||||
self.currentMode = FLEXExplorerModeDefault;
|
||||
} else {
|
||||
self.currentMode = FLEXExplorerModeSelect;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)hierarchyButtonTapped:(FLEXToolbarItem *)sender
|
||||
{
|
||||
NSArray *allViews = [self allViewsInHierarchy];
|
||||
NSDictionary *depthsForViews = [self hierarchyDepthsForViews:allViews];
|
||||
FLEXHierarchyTableViewController *hierarchyTVC = [[FLEXHierarchyTableViewController alloc] initWithViews:allViews viewsAtTap:self.viewsAtTapPoint selectedView:self.selectedView depths:depthsForViews];
|
||||
hierarchyTVC.delegate = self;
|
||||
UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:hierarchyTVC];
|
||||
[self makeKeyAndPresentViewController:navigationController animated:YES completion:nil];
|
||||
}
|
||||
|
||||
- (NSArray *)allViewsInHierarchy
|
||||
{
|
||||
NSMutableArray *allViews = [NSMutableArray array];
|
||||
NSArray *windows = [self allWindows];
|
||||
for (UIWindow *window in windows) {
|
||||
if (window != self.view.window) {
|
||||
[allViews addObject:window];
|
||||
[allViews addObjectsFromArray:[self allRecursiveSubviewsInView:window]];
|
||||
}
|
||||
}
|
||||
return allViews;
|
||||
}
|
||||
|
||||
- (NSArray *)allWindows
|
||||
{
|
||||
NSMutableArray *windows = [[[UIApplication sharedApplication] windows] mutableCopy];
|
||||
UIWindow *statusWindow = [self statusWindow];
|
||||
if (statusWindow) {
|
||||
// The windows are ordered back to front, so default to inserting the status bar at the end.
|
||||
// However, it there are windows at status bar level, insert the status bar before them.
|
||||
NSInteger insertionIndex = [windows count];
|
||||
for (UIWindow *window in windows) {
|
||||
if (window.windowLevel >= UIWindowLevelStatusBar) {
|
||||
insertionIndex = [windows indexOfObject:window];
|
||||
break;
|
||||
}
|
||||
}
|
||||
[windows insertObject:statusWindow atIndex:insertionIndex];
|
||||
}
|
||||
return windows;
|
||||
}
|
||||
|
||||
- (UIWindow *)statusWindow
|
||||
{
|
||||
NSString *statusBarString = [NSString stringWithFormat:@"%@arWindow", @"_statusB"];
|
||||
return [[UIApplication sharedApplication] valueForKey:statusBarString];
|
||||
}
|
||||
|
||||
- (void)moveButtonTapped:(FLEXToolbarItem *)sender
|
||||
{
|
||||
if (self.currentMode == FLEXExplorerModeMove) {
|
||||
self.currentMode = FLEXExplorerModeDefault;
|
||||
} else {
|
||||
self.currentMode = FLEXExplorerModeMove;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)globalsButtonTapped:(FLEXToolbarItem *)sender
|
||||
{
|
||||
FLEXGlobalsTableViewController *globalsViewController = [[FLEXGlobalsTableViewController alloc] init];
|
||||
globalsViewController.delegate = self;
|
||||
[FLEXGlobalsTableViewController setApplicationWindow:[[UIApplication sharedApplication] keyWindow]];
|
||||
UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:globalsViewController];
|
||||
[self makeKeyAndPresentViewController:navigationController animated:YES completion:nil];
|
||||
}
|
||||
|
||||
- (void)closeButtonTapped:(FLEXToolbarItem *)sender
|
||||
{
|
||||
self.currentMode = FLEXExplorerModeDefault;
|
||||
[self.delegate explorerViewControllerDidFinish:self];
|
||||
}
|
||||
|
||||
- (void)updateButtonStates
|
||||
{
|
||||
// Move and details only active when an object is selected.
|
||||
BOOL hasSelectedObject = self.selectedView != nil;
|
||||
self.explorerToolbar.moveItem.enabled = hasSelectedObject;
|
||||
self.explorerToolbar.selectItem.selected = self.currentMode == FLEXExplorerModeSelect;
|
||||
self.explorerToolbar.moveItem.selected = self.currentMode == FLEXExplorerModeMove;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Toolbar Dragging
|
||||
|
||||
- (void)setupToolbarGestures
|
||||
{
|
||||
// Pan gesture for dragging.
|
||||
UIPanGestureRecognizer *panGR = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handleToolbarPanGesture:)];
|
||||
[self.explorerToolbar.dragHandle addGestureRecognizer:panGR];
|
||||
|
||||
// Tap gesture for hinting.
|
||||
UITapGestureRecognizer *hintTapGR = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleToolbarHintTapGesture:)];
|
||||
[self.explorerToolbar.dragHandle addGestureRecognizer:hintTapGR];
|
||||
|
||||
// Tap gesture for showing additional details
|
||||
self.detailsTapGR = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleToolbarDetailsTapGesture:)];
|
||||
[self.explorerToolbar.selectedViewDescriptionContainer addGestureRecognizer:self.detailsTapGR];
|
||||
}
|
||||
|
||||
- (void)handleToolbarPanGesture:(UIPanGestureRecognizer *)panGR
|
||||
{
|
||||
switch (panGR.state) {
|
||||
case UIGestureRecognizerStateBegan:
|
||||
self.toolbarFrameBeforeDragging = self.explorerToolbar.frame;
|
||||
[self updateToolbarPostionWithDragGesture:panGR];
|
||||
break;
|
||||
|
||||
case UIGestureRecognizerStateChanged:
|
||||
case UIGestureRecognizerStateEnded:
|
||||
[self updateToolbarPostionWithDragGesture:panGR];
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)updateToolbarPostionWithDragGesture:(UIPanGestureRecognizer *)panGR
|
||||
{
|
||||
CGPoint translation = [panGR translationInView:self.view];
|
||||
CGRect newToolbarFrame = self.toolbarFrameBeforeDragging;
|
||||
newToolbarFrame.origin.y += translation.y;
|
||||
|
||||
CGFloat maxY = CGRectGetMaxY(self.view.bounds) - newToolbarFrame.size.height;
|
||||
if (newToolbarFrame.origin.y < 0.0) {
|
||||
newToolbarFrame.origin.y = 0.0;
|
||||
} else if (newToolbarFrame.origin.y > maxY) {
|
||||
newToolbarFrame.origin.y = maxY;
|
||||
}
|
||||
|
||||
self.explorerToolbar.frame = newToolbarFrame;
|
||||
}
|
||||
|
||||
- (void)handleToolbarHintTapGesture:(UITapGestureRecognizer *)tapGR
|
||||
{
|
||||
// Bounce the toolbar to indicate that it is draggable.
|
||||
// TODO: make it bouncier.
|
||||
if (tapGR.state == UIGestureRecognizerStateRecognized) {
|
||||
CGRect originalToolbarFrame = self.explorerToolbar.frame;
|
||||
const NSTimeInterval kHalfwayDuration = 0.2;
|
||||
const CGFloat kVerticalOffset = 30.0;
|
||||
[UIView animateWithDuration:kHalfwayDuration delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{
|
||||
CGRect newToolbarFrame = self.explorerToolbar.frame;
|
||||
newToolbarFrame.origin.y += kVerticalOffset;
|
||||
self.explorerToolbar.frame = newToolbarFrame;
|
||||
} completion:^(BOOL finished) {
|
||||
[UIView animateWithDuration:kHalfwayDuration delay:0 options:UIViewAnimationOptionCurveEaseIn animations:^{
|
||||
self.explorerToolbar.frame = originalToolbarFrame;
|
||||
} completion:nil];
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)handleToolbarDetailsTapGesture:(UITapGestureRecognizer *)tapGR
|
||||
{
|
||||
if (tapGR.state == UIGestureRecognizerStateRecognized && self.selectedView) {
|
||||
FLEXObjectExplorerViewController *selectedViewExplorer = [FLEXObjectExplorerFactory explorerViewControllerForObject:self.selectedView];
|
||||
selectedViewExplorer.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(selectedViewExplorerFinished:)];
|
||||
UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:selectedViewExplorer];
|
||||
[self makeKeyAndPresentViewController:navigationController animated:YES completion:nil];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - View Selection
|
||||
|
||||
- (void)handleSelectionTap:(UITapGestureRecognizer *)tapGR
|
||||
{
|
||||
// Only if we're in selection mode
|
||||
if (self.currentMode == FLEXExplorerModeSelect && tapGR.state == UIGestureRecognizerStateRecognized) {
|
||||
[self updateOutlineViewsForSelectionPoint:[tapGR locationInView:nil]];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)updateOutlineViewsForSelectionPoint:(CGPoint)selectionPointInWindow
|
||||
{
|
||||
[self removeAndClearOutlineViews];
|
||||
|
||||
// Include hidden views in the "viewsAtTapPoint" array so we can show them in the hierarchy list.
|
||||
self.viewsAtTapPoint = [self viewsAtPoint:selectionPointInWindow skipHiddenViews:NO];
|
||||
|
||||
// For outlined views and the selected view, only use visible views.
|
||||
// Outlining hidden views adds clutter and makes the selection behavior confusing.
|
||||
NSArray *visibleViewsAtTapPoint = [self viewsAtPoint:selectionPointInWindow skipHiddenViews:YES];
|
||||
NSMutableDictionary *newOutlineViewsForVisibleViews = [NSMutableDictionary dictionary];
|
||||
for (UIView *view in visibleViewsAtTapPoint) {
|
||||
UIView *outlineView = [self outlineViewForView:view];
|
||||
[self.view addSubview:outlineView];
|
||||
NSValue *key = [NSValue valueWithNonretainedObject:view];
|
||||
[newOutlineViewsForVisibleViews setObject:outlineView forKey:key];
|
||||
}
|
||||
self.outlineViewsForVisibleViews = newOutlineViewsForVisibleViews;
|
||||
self.selectedView = [self viewForSelectionAtPoint:selectionPointInWindow];
|
||||
|
||||
// Make sure the explorer toolbar doesn't end up behind the newly added outline views.
|
||||
[self.view bringSubviewToFront:self.explorerToolbar];
|
||||
|
||||
[self updateButtonStates];
|
||||
}
|
||||
|
||||
- (UIView *)outlineViewForView:(UIView *)view
|
||||
{
|
||||
CGRect outlineFrame = [self frameInLocalCoordinatesForView:view];
|
||||
UIView *outlineView = [[UIView alloc] initWithFrame:outlineFrame];
|
||||
outlineView.backgroundColor = [UIColor clearColor];
|
||||
outlineView.layer.borderColor = [[FLEXUtility consistentRandomColorForObject:view] CGColor];
|
||||
outlineView.layer.borderWidth = 1.0;
|
||||
return outlineView;
|
||||
}
|
||||
|
||||
- (void)removeAndClearOutlineViews
|
||||
{
|
||||
for (id key in self.outlineViewsForVisibleViews) {
|
||||
UIView *outlineView = self.outlineViewsForVisibleViews[key];
|
||||
[outlineView removeFromSuperview];
|
||||
}
|
||||
self.outlineViewsForVisibleViews = nil;
|
||||
}
|
||||
|
||||
- (NSArray *)viewsAtPoint:(CGPoint)tapPointInWindow skipHiddenViews:(BOOL)skipHidden
|
||||
{
|
||||
NSMutableArray *views = [NSMutableArray array];
|
||||
for (UIWindow *window in [self allWindows]) {
|
||||
// Don't include the explorer's own window or subviews.
|
||||
if (window != self.view.window && [window pointInside:tapPointInWindow withEvent:nil]) {
|
||||
[views addObject:window];
|
||||
[views addObjectsFromArray:[self recursiveSubviewsAtPoint:tapPointInWindow inView:window skipHiddenViews:skipHidden]];
|
||||
}
|
||||
}
|
||||
return views;
|
||||
}
|
||||
|
||||
- (UIView *)viewForSelectionAtPoint:(CGPoint)tapPointInWindow
|
||||
{
|
||||
// Select in the window that would handle the touch, but don't just use the result of hitTest:withEvent: so we can still select views with interaction disabled.
|
||||
// Default to the the application's key window if none of the windows want the touch.
|
||||
UIWindow *windowForSelection = [[UIApplication sharedApplication] keyWindow];
|
||||
for (UIWindow *window in [[self allWindows] reverseObjectEnumerator]) {
|
||||
// Ignore the explorer's own window.
|
||||
if (window != self.view.window) {
|
||||
if ([window hitTest:tapPointInWindow withEvent:nil]) {
|
||||
windowForSelection = window;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Select the deepest visible view at the tap point. This generally corresponds to what the user wants to select.
|
||||
return [[self recursiveSubviewsAtPoint:tapPointInWindow inView:windowForSelection skipHiddenViews:YES] lastObject];
|
||||
}
|
||||
|
||||
- (NSArray *)recursiveSubviewsAtPoint:(CGPoint)pointInView inView:(UIView *)view skipHiddenViews:(BOOL)skipHidden
|
||||
{
|
||||
NSMutableArray *subviewsAtPoint = [NSMutableArray array];
|
||||
for (UIView *subview in view.subviews) {
|
||||
BOOL isHidden = subview.hidden || subview.alpha < 0.01;
|
||||
if (skipHidden && isHidden) {
|
||||
continue;
|
||||
}
|
||||
|
||||
BOOL subviewContainsPoint = CGRectContainsPoint(subview.frame, pointInView);
|
||||
if (subviewContainsPoint) {
|
||||
[subviewsAtPoint addObject:subview];
|
||||
}
|
||||
|
||||
// If this view doesn't clip to its bounds, we need to check its subviews even if it doesn't contain the selection point.
|
||||
// They may be visible and contain the selection point.
|
||||
if (subviewContainsPoint || !subview.clipsToBounds) {
|
||||
CGPoint pointInSubview = [view convertPoint:pointInView toView:subview];
|
||||
[subviewsAtPoint addObjectsFromArray:[self recursiveSubviewsAtPoint:pointInSubview inView:subview skipHiddenViews:skipHidden]];
|
||||
}
|
||||
}
|
||||
return subviewsAtPoint;
|
||||
}
|
||||
|
||||
- (NSArray *)allRecursiveSubviewsInView:(UIView *)view
|
||||
{
|
||||
NSMutableArray *subviews = [NSMutableArray array];
|
||||
for (UIView *subview in view.subviews) {
|
||||
[subviews addObject:subview];
|
||||
[subviews addObjectsFromArray:[self allRecursiveSubviewsInView:subview]];
|
||||
}
|
||||
return subviews;
|
||||
}
|
||||
|
||||
- (NSDictionary *)hierarchyDepthsForViews:(NSArray *)views
|
||||
{
|
||||
NSMutableDictionary *hierarchyDepths = [NSMutableDictionary dictionary];
|
||||
for (UIView *view in views) {
|
||||
NSInteger depth = 0;
|
||||
UIView *tryView = view;
|
||||
while (tryView.superview) {
|
||||
tryView = tryView.superview;
|
||||
depth++;
|
||||
}
|
||||
[hierarchyDepths setObject:@(depth) forKey:[NSValue valueWithNonretainedObject:view]];
|
||||
}
|
||||
return hierarchyDepths;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Selected View Moving
|
||||
|
||||
- (void)handleMovePan:(UIPanGestureRecognizer *)movePanGR
|
||||
{
|
||||
switch (movePanGR.state) {
|
||||
case UIGestureRecognizerStateBegan:
|
||||
self.selectedViewFrameBeforeDragging = self.selectedView.frame;
|
||||
[self updateSelectedViewPositionWithDragGesture:movePanGR];
|
||||
break;
|
||||
|
||||
case UIGestureRecognizerStateChanged:
|
||||
case UIGestureRecognizerStateEnded:
|
||||
[self updateSelectedViewPositionWithDragGesture:movePanGR];
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)updateSelectedViewPositionWithDragGesture:(UIPanGestureRecognizer *)movePanGR
|
||||
{
|
||||
CGPoint translation = [movePanGR translationInView:self.selectedView.superview];
|
||||
CGRect newSelectedViewFrame = self.selectedViewFrameBeforeDragging;
|
||||
newSelectedViewFrame.origin.x = FLEXFloor(newSelectedViewFrame.origin.x + translation.x);
|
||||
newSelectedViewFrame.origin.y = FLEXFloor(newSelectedViewFrame.origin.y + translation.y);
|
||||
self.selectedView.frame = newSelectedViewFrame;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Touch Handling
|
||||
|
||||
- (BOOL)shouldReceiveTouchAtWindowPoint:(CGPoint)pointInWindowCoordinates
|
||||
{
|
||||
BOOL shouldReceiveTouch = NO;
|
||||
|
||||
CGPoint pointInLocalCoordinates = [self.view convertPoint:pointInWindowCoordinates fromView:nil];
|
||||
|
||||
// Always if it's on the toolbar
|
||||
if (CGRectContainsPoint(self.explorerToolbar.frame, pointInLocalCoordinates)) {
|
||||
shouldReceiveTouch = YES;
|
||||
}
|
||||
|
||||
// Always if we're in selection mode
|
||||
if (!shouldReceiveTouch && self.currentMode == FLEXExplorerModeSelect) {
|
||||
shouldReceiveTouch = YES;
|
||||
}
|
||||
|
||||
// Always in move mode too
|
||||
if (!shouldReceiveTouch && self.currentMode == FLEXExplorerModeMove) {
|
||||
shouldReceiveTouch = YES;
|
||||
}
|
||||
|
||||
// Always if we have a modal presented
|
||||
if (!shouldReceiveTouch && self.presentedViewController) {
|
||||
shouldReceiveTouch = YES;
|
||||
}
|
||||
|
||||
return shouldReceiveTouch;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - FLEXHierarchyTableViewControllerDelegate
|
||||
|
||||
- (void)hierarchyViewController:(FLEXHierarchyTableViewController *)hierarchyViewController didFinishWithSelectedView:(UIView *)selectedView
|
||||
{
|
||||
// Note that we need to wait until the view controller is dismissed to calculated the frame of the outline view.
|
||||
// Otherwise the coordinate conversion doesn't give the correct result.
|
||||
[self resignKeyAndDismissViewControllerAnimated:YES completion:^{
|
||||
// If the selected view is outside of the tap point array (selected from "Full Hierarchy"),
|
||||
// then clear out the tap point array and remove all the outline views.
|
||||
if (![self.viewsAtTapPoint containsObject:selectedView]) {
|
||||
self.viewsAtTapPoint = nil;
|
||||
[self removeAndClearOutlineViews];
|
||||
}
|
||||
|
||||
// If we now have a selected view and we didn't have one previously, go to "select" mode.
|
||||
if (self.currentMode == FLEXExplorerModeDefault && selectedView) {
|
||||
self.currentMode = FLEXExplorerModeSelect;
|
||||
}
|
||||
|
||||
// The selected view setter will also update the selected view overlay appropriately.
|
||||
self.selectedView = selectedView;
|
||||
}];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - FLEXGlobalsViewControllerDelegate
|
||||
|
||||
- (void)globalsViewControllerDidFinish:(FLEXGlobalsTableViewController *)globalsViewController
|
||||
{
|
||||
[self resignKeyAndDismissViewControllerAnimated:YES completion:nil];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - FLEXObjectExplorerViewController Done Action
|
||||
|
||||
- (void)selectedViewExplorerFinished:(id)sender
|
||||
{
|
||||
[self resignKeyAndDismissViewControllerAnimated:YES completion:nil];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Modal Presentation and Window Management
|
||||
|
||||
- (void)makeKeyAndPresentViewController:(UIViewController *)viewController animated:(BOOL)animated completion:(void (^)(void))completion
|
||||
{
|
||||
// Save the current key window so we can restore it following dismissal.
|
||||
self.previousKeyWindow = [[UIApplication sharedApplication] keyWindow];
|
||||
|
||||
// Make our window key to correctly handle input.
|
||||
[self.view.window makeKeyWindow];
|
||||
|
||||
// Move the status bar on top of FLEX so we can get scroll to top behavior for taps.
|
||||
[[self statusWindow] setWindowLevel:self.view.window.windowLevel + 1.0];
|
||||
|
||||
// If this app doesn't use view controller based status bar management and we're on iOS 7+,
|
||||
// make sure the status bar style is UIStatusBarStyleDefault. We don't actully have to check
|
||||
// for view controller based management because the global methods no-op if that is turned on.
|
||||
// Only for iOS 7+
|
||||
if (NSFoundationVersionNumber > NSFoundationVersionNumber_iOS_6_1) {
|
||||
self.previousStatusBarStyle = [[UIApplication sharedApplication] statusBarStyle];
|
||||
[[UIApplication sharedApplication] setStatusBarStyle:UIStatusBarStyleDefault];
|
||||
}
|
||||
|
||||
// Show the view controller.
|
||||
[self presentViewController:viewController animated:animated completion:completion];
|
||||
}
|
||||
|
||||
- (void)resignKeyAndDismissViewControllerAnimated:(BOOL)animated completion:(void (^)(void))completion
|
||||
{
|
||||
[self.previousKeyWindow makeKeyWindow];
|
||||
|
||||
self.previousKeyWindow = nil;
|
||||
|
||||
// Restore the status bar window's normal window level.
|
||||
// We want it above FLEX while a modal is presented for scroll to top, but below FLEX otherwise for exploration.
|
||||
[[self statusWindow] setWindowLevel:UIWindowLevelStatusBar];
|
||||
|
||||
// Restore the stauts bar style if the app is using global status bar management.
|
||||
// Only for iOS 7+
|
||||
if (NSFoundationVersionNumber > NSFoundationVersionNumber_iOS_6_1) {
|
||||
[[UIApplication sharedApplication] setStatusBarStyle:self.previousStatusBarStyle];
|
||||
}
|
||||
|
||||
[self dismissViewControllerAnimated:animated completion:completion];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,16 +0,0 @@
|
||||
//
|
||||
// FLEXManager+Private.h
|
||||
// PebbleApp
|
||||
//
|
||||
// Created by Javier Soto on 7/26/14.
|
||||
// Copyright (c) 2014 Pebble Technology. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXManager.h"
|
||||
|
||||
@interface FLEXManager ()
|
||||
|
||||
/// An array of FLEXGlobalsTableViewControllerEntry objects that have been registered by the user.
|
||||
@property (nonatomic, readonly, strong) NSArray *userGlobalEntries;
|
||||
|
||||
@end
|
||||
@@ -1,31 +0,0 @@
|
||||
//
|
||||
// FLEXManager.h
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 4/4/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@interface FLEXManager : NSObject
|
||||
|
||||
+ (instancetype)sharedManager;
|
||||
|
||||
@property (nonatomic, readonly) BOOL isHidden;
|
||||
|
||||
- (void)showExplorer;
|
||||
- (void)hideExplorer;
|
||||
|
||||
#pragma mark - Extensions
|
||||
|
||||
/// Adds an entry at the bottom of the list of Global State items. Call this method before this view controller is displayed.
|
||||
/// @param entryName The string to be displayed in the cell.
|
||||
/// @param objectFutureBlock When you tap on the row, information about the object returned by this block will be displayed.
|
||||
/// Passing a block that returns an object allows you to display information about an object whose actual pointer may change at runtime (e.g. +currentUser)
|
||||
/// @note This method must be called from the main thread.
|
||||
/// The objectFutureBlock will be invoked from the main thread and may return nil.
|
||||
/// @note The passed block will be copied and retain for the duration of the application, you may want to use __weak references.
|
||||
- (void)registerGlobalEntryWithName:(NSString *)entryName objectFutureBlock:(id(^)(void))objectFutureBlock;
|
||||
|
||||
@end
|
||||
@@ -1,122 +0,0 @@
|
||||
//
|
||||
// FLEXManager.m
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 4/4/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXManager.h"
|
||||
#import "FLEXExplorerViewController.h"
|
||||
#import "FLEXWindow.h"
|
||||
#import "FLEXGlobalsTableViewControllerEntry.h"
|
||||
#import "FLEXObjectExplorerFactory.h"
|
||||
#import "FLEXObjectExplorerViewController.h"
|
||||
|
||||
@interface FLEXManager () <FLEXWindowEventDelegate, FLEXExplorerViewControllerDelegate>
|
||||
|
||||
@property (nonatomic, strong) FLEXWindow *explorerWindow;
|
||||
@property (nonatomic, strong) FLEXExplorerViewController *explorerViewController;
|
||||
|
||||
@property (nonatomic, readonly, strong) NSMutableArray *userGlobalEntries;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXManager
|
||||
|
||||
+ (instancetype)sharedManager
|
||||
{
|
||||
static FLEXManager *sharedManager = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
sharedManager = [[[self class] alloc] init];
|
||||
});
|
||||
return sharedManager;
|
||||
}
|
||||
|
||||
- (instancetype)init
|
||||
{
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_userGlobalEntries = [[NSMutableArray alloc] init];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (FLEXWindow *)explorerWindow
|
||||
{
|
||||
NSAssert([NSThread isMainThread], @"You must use %@ from the main thread only.", NSStringFromClass([self class]));
|
||||
|
||||
if (!_explorerWindow) {
|
||||
_explorerWindow = [[FLEXWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
|
||||
_explorerWindow.eventDelegate = self;
|
||||
|
||||
_explorerWindow.rootViewController = self.explorerViewController;
|
||||
[_explorerWindow addSubview:self.explorerViewController.view];
|
||||
}
|
||||
|
||||
return _explorerWindow;
|
||||
}
|
||||
|
||||
- (FLEXExplorerViewController *)explorerViewController
|
||||
{
|
||||
if (!_explorerViewController) {
|
||||
_explorerViewController = [[FLEXExplorerViewController alloc] init];
|
||||
_explorerViewController.delegate = self;
|
||||
}
|
||||
|
||||
return _explorerViewController;
|
||||
}
|
||||
|
||||
- (void)showExplorer
|
||||
{
|
||||
self.explorerWindow.hidden = NO;
|
||||
}
|
||||
|
||||
- (void)hideExplorer
|
||||
{
|
||||
self.explorerWindow.hidden = YES;
|
||||
}
|
||||
|
||||
- (BOOL)isHidden
|
||||
{
|
||||
return self.explorerWindow.isHidden;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - FLEXWindowEventDelegate
|
||||
|
||||
- (BOOL)shouldHandleTouchAtPoint:(CGPoint)pointInWindow
|
||||
{
|
||||
// Ask the explorer view controller
|
||||
return [self.explorerViewController shouldReceiveTouchAtWindowPoint:pointInWindow];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - FLEXExplorerViewControllerDelegate
|
||||
|
||||
- (void)explorerViewControllerDidFinish:(FLEXExplorerViewController *)explorerViewController
|
||||
{
|
||||
[self hideExplorer];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Extensions
|
||||
|
||||
- (void)registerGlobalEntryWithName:(NSString *)entryName objectFutureBlock:(id (^)(void))objectFutureBlock
|
||||
{
|
||||
NSParameterAssert(entryName);
|
||||
NSParameterAssert(objectFutureBlock);
|
||||
NSAssert([NSThread isMainThread], @"This method must be called from the main thread.");
|
||||
|
||||
entryName = entryName.copy;
|
||||
FLEXGlobalsTableViewControllerEntry *entry = [FLEXGlobalsTableViewControllerEntry entryWithNameFuture:^NSString *{
|
||||
return entryName;
|
||||
} viewControllerFuture:^UIViewController *{
|
||||
return [FLEXObjectExplorerFactory explorerViewControllerForObject:objectFutureBlock()];
|
||||
}];
|
||||
|
||||
[self.userGlobalEntries addObject:entry];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,17 +0,0 @@
|
||||
//
|
||||
// FLEXToolbarItem.h
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 4/4/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface FLEXToolbarItem : UIButton
|
||||
|
||||
+ (instancetype)toolbarItemWithTitle:(NSString *)title image:(UIImage *)image;
|
||||
|
||||
+ (UIColor *)defaultBackgroundColor;
|
||||
|
||||
@end
|
||||
@@ -1,133 +0,0 @@
|
||||
//
|
||||
// FLEXToolbarItem.m
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 4/4/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXToolbarItem.h"
|
||||
#import "FLEXUtility.h"
|
||||
|
||||
@interface FLEXToolbarItem ()
|
||||
|
||||
@property (nonatomic, copy) NSAttributedString *attributedTitle;
|
||||
@property (nonatomic, strong) UIImage *image;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXToolbarItem
|
||||
|
||||
- (id)initWithFrame:(CGRect)frame
|
||||
{
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
self.backgroundColor = [[self class] defaultBackgroundColor];
|
||||
[self setTitleColor:[[self class] defaultTitleColor] forState:UIControlStateNormal];
|
||||
[self setTitleColor:[[self class] disabledTitleColor] forState:UIControlStateDisabled];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
+ (instancetype)toolbarItemWithTitle:(NSString *)title image:(UIImage *)image
|
||||
{
|
||||
FLEXToolbarItem *toolbarItem = [self buttonWithType:UIButtonTypeCustom];
|
||||
NSAttributedString *attributedTitle = [[NSAttributedString alloc] initWithString:title attributes:[self titleAttributes]];
|
||||
toolbarItem.attributedTitle = attributedTitle;
|
||||
toolbarItem.image = image;
|
||||
[toolbarItem setAttributedTitle:attributedTitle forState:UIControlStateNormal];
|
||||
[toolbarItem setImage:image forState:UIControlStateNormal];
|
||||
return toolbarItem;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Display Defaults
|
||||
|
||||
+ (NSDictionary *)titleAttributes
|
||||
{
|
||||
return @{NSFontAttributeName : [FLEXUtility defaultFontOfSize:12.0]};
|
||||
}
|
||||
|
||||
+ (UIColor *)defaultTitleColor
|
||||
{
|
||||
return [UIColor blackColor];
|
||||
}
|
||||
|
||||
+ (UIColor *)disabledTitleColor
|
||||
{
|
||||
return [UIColor colorWithWhite:121.0/255.0 alpha:1.0];
|
||||
}
|
||||
|
||||
+ (UIColor *)highlightedBackgroundColor
|
||||
{
|
||||
return [UIColor colorWithWhite:0.9 alpha:1.0];
|
||||
}
|
||||
|
||||
+ (UIColor *)selectedBackgroundColor
|
||||
{
|
||||
return [UIColor colorWithRed:199.0/255.0 green:199.0/255.0 blue:255.0/255.0 alpha:1.0];
|
||||
}
|
||||
|
||||
+ (UIColor *)defaultBackgroundColor
|
||||
{
|
||||
return [UIColor colorWithWhite:1.0 alpha:0.95];
|
||||
}
|
||||
|
||||
+ (CGFloat)topMargin
|
||||
{
|
||||
return 2.0;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - State Changes
|
||||
|
||||
- (void)setHighlighted:(BOOL)highlighted
|
||||
{
|
||||
[super setHighlighted:highlighted];
|
||||
[self updateBackgroundColor];
|
||||
}
|
||||
|
||||
- (void)setSelected:(BOOL)selected
|
||||
{
|
||||
[super setSelected:selected];
|
||||
[self updateBackgroundColor];
|
||||
}
|
||||
|
||||
- (void)updateBackgroundColor
|
||||
{
|
||||
if (self.highlighted) {
|
||||
self.backgroundColor = [[self class] highlightedBackgroundColor];
|
||||
} else if (self.selected) {
|
||||
self.backgroundColor = [[self class] selectedBackgroundColor];
|
||||
} else {
|
||||
self.backgroundColor = [[self class] defaultBackgroundColor];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - UIButton Layout Overrides
|
||||
|
||||
- (CGRect)titleRectForContentRect:(CGRect)contentRect
|
||||
{
|
||||
// Bottom aligned and centered.
|
||||
CGRect titleRect = CGRectZero;
|
||||
CGSize titleSize = [self.attributedTitle boundingRectWithSize:contentRect.size options:0 context:nil].size;
|
||||
titleSize = CGSizeMake(ceil(titleSize.width), ceil(titleSize.height));
|
||||
titleRect.size = titleSize;
|
||||
titleRect.origin.y = contentRect.origin.y + CGRectGetMaxY(contentRect) - titleSize.height;
|
||||
titleRect.origin.x = contentRect.origin.x + FLEXFloor((contentRect.size.width - titleSize.width) / 2.0);
|
||||
return titleRect;
|
||||
}
|
||||
|
||||
- (CGRect)imageRectForContentRect:(CGRect)contentRect
|
||||
{
|
||||
CGSize imageSize = self.image.size;
|
||||
CGRect titleRect = [self titleRectForContentRect:contentRect];
|
||||
CGFloat availableHeight = contentRect.size.height - titleRect.size.height - [[self class] topMargin];
|
||||
CGFloat originY = [[self class] topMargin] + FLEXFloor((availableHeight - imageSize.height) / 2.0);
|
||||
CGFloat originX = FLEXFloor((contentRect.size.width - imageSize.width) / 2.0);
|
||||
CGRect imageRect = CGRectMake(originX, originY, imageSize.width, imageSize.height);
|
||||
return imageRect;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,23 +0,0 @@
|
||||
//
|
||||
// FLEXWindow.h
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 4/13/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@protocol FLEXWindowEventDelegate;
|
||||
|
||||
@interface FLEXWindow : UIWindow
|
||||
|
||||
@property (nonatomic, weak) id <FLEXWindowEventDelegate> eventDelegate;
|
||||
|
||||
@end
|
||||
|
||||
@protocol FLEXWindowEventDelegate <NSObject>
|
||||
|
||||
- (BOOL)shouldHandleTouchAtPoint:(CGPoint)pointInWindow;
|
||||
|
||||
@end
|
||||
@@ -1,36 +0,0 @@
|
||||
//
|
||||
// FLEXWindow.m
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 4/13/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXWindow.h"
|
||||
|
||||
@implementation FLEXWindow
|
||||
|
||||
- (id)initWithFrame:(CGRect)frame
|
||||
{
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
self.backgroundColor = [UIColor clearColor];
|
||||
// Some apps have windows at UIWindowLevelStatusBar + n.
|
||||
// If we make the window level too high, we block out UIAlertViews.
|
||||
// There's a balance between staying above the app's windows and staying below alerts.
|
||||
// UIWindowLevelStatusBar + 100 seems to hit that balance.
|
||||
self.windowLevel = UIWindowLevelStatusBar + 100.0;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
|
||||
{
|
||||
BOOL pointInside = NO;
|
||||
if ([self.eventDelegate shouldHandleTouchAtPoint:point]) {
|
||||
pointInside = [super pointInside:point withEvent:event];
|
||||
}
|
||||
return pointInside;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,19 @@
|
||||
//
|
||||
// FLEXBookmarkManager.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 2/6/20.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface FLEXBookmarkManager : NSObject
|
||||
|
||||
@property (nonatomic, readonly, class) NSMutableArray *bookmarks;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,25 @@
|
||||
//
|
||||
// FLEXBookmarkManager.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 2/6/20.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXBookmarkManager.h"
|
||||
|
||||
static NSMutableArray *kFLEXBookmarkManagerBookmarks = nil;
|
||||
|
||||
@implementation FLEXBookmarkManager
|
||||
|
||||
+ (void)initialize {
|
||||
if (self == [FLEXBookmarkManager class]) {
|
||||
kFLEXBookmarkManagerBookmarks = [NSMutableArray new];
|
||||
}
|
||||
}
|
||||
|
||||
+ (NSMutableArray *)bookmarks {
|
||||
return kFLEXBookmarkManagerBookmarks;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// FLEXBookmarksViewController.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 2/6/20.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXTableViewController.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface FLEXBookmarksViewController : FLEXTableViewController
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,235 @@
|
||||
//
|
||||
// FLEXBookmarksViewController.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 2/6/20.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXBookmarksViewController.h"
|
||||
#import "FLEXExplorerViewController.h"
|
||||
#import "FLEXNavigationController.h"
|
||||
#import "FLEXObjectExplorerFactory.h"
|
||||
#import "FLEXBookmarkManager.h"
|
||||
#import "UIBarButtonItem+FLEX.h"
|
||||
#import "FLEXColor.h"
|
||||
#import "FLEXUtility.h"
|
||||
#import "FLEXRuntimeUtility.h"
|
||||
#import "FLEXTableView.h"
|
||||
|
||||
@interface FLEXBookmarksViewController ()
|
||||
@property (nonatomic, copy) NSArray *bookmarks;
|
||||
@property (nonatomic, readonly) FLEXExplorerViewController *corePresenter;
|
||||
@end
|
||||
|
||||
@implementation FLEXBookmarksViewController
|
||||
|
||||
#pragma mark - Initialization
|
||||
|
||||
- (id)init {
|
||||
return [self initWithStyle:UITableViewStylePlain];
|
||||
}
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
|
||||
self.navigationController.hidesBarsOnSwipe = NO;
|
||||
self.tableView.allowsMultipleSelectionDuringEditing = YES;
|
||||
|
||||
[self reloadData];
|
||||
}
|
||||
|
||||
- (void)viewWillAppear:(BOOL)animated {
|
||||
[super viewWillAppear:animated];
|
||||
[self setupDefaultBarItems];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Private
|
||||
|
||||
- (void)reloadData {
|
||||
// We assume the bookmarks aren't going to change out from under us, since
|
||||
// presenting any other tool via keyboard shortcuts should dismiss us first
|
||||
self.bookmarks = FLEXBookmarkManager.bookmarks;
|
||||
self.title = [NSString stringWithFormat:@"Bookmarks (%@)", @(self.bookmarks.count)];
|
||||
}
|
||||
|
||||
- (void)setupDefaultBarItems {
|
||||
self.navigationItem.rightBarButtonItem = FLEXBarButtonItemSystem(Done, self, @selector(dismissAnimated));
|
||||
self.toolbarItems = @[
|
||||
UIBarButtonItem.flex_flexibleSpace,
|
||||
FLEXBarButtonItemSystem(Edit, self, @selector(toggleEditing)),
|
||||
];
|
||||
|
||||
// Disable editing if no bookmarks available
|
||||
self.toolbarItems.lastObject.enabled = self.bookmarks.count > 0;
|
||||
}
|
||||
|
||||
- (void)setupEditingBarItems {
|
||||
self.navigationItem.rightBarButtonItem = nil;
|
||||
self.toolbarItems = @[
|
||||
[UIBarButtonItem flex_itemWithTitle:@"Close All" target:self action:@selector(closeAllButtonPressed:)],
|
||||
UIBarButtonItem.flex_flexibleSpace,
|
||||
// We use a non-system done item because we change its title dynamically
|
||||
[UIBarButtonItem flex_doneStyleitemWithTitle:@"Done" target:self action:@selector(toggleEditing)]
|
||||
];
|
||||
|
||||
self.toolbarItems.firstObject.tintColor = FLEXColor.destructiveColor;
|
||||
}
|
||||
|
||||
- (FLEXExplorerViewController *)corePresenter {
|
||||
// We must be presented by a FLEXExplorerViewController, or presented
|
||||
// by another view controller that was presented by FLEXExplorerViewController
|
||||
FLEXExplorerViewController *presenter = (id)self.presentingViewController;
|
||||
presenter = (id)presenter.presentingViewController ?: presenter;
|
||||
presenter = (id)presenter.presentingViewController ?: presenter;
|
||||
NSAssert(
|
||||
[presenter isKindOfClass:[FLEXExplorerViewController class]],
|
||||
@"The bookmarks view controller expects to be presented by the explorer controller"
|
||||
);
|
||||
return presenter;
|
||||
}
|
||||
|
||||
#pragma mark Button Actions
|
||||
|
||||
- (void)dismissAnimated {
|
||||
[self dismissAnimated:nil];
|
||||
}
|
||||
|
||||
- (void)dismissAnimated:(id)selectedObject {
|
||||
if (selectedObject) {
|
||||
UIViewController *explorer = [FLEXObjectExplorerFactory
|
||||
explorerViewControllerForObject:selectedObject
|
||||
];
|
||||
if ([self.presentingViewController isKindOfClass:[FLEXNavigationController class]]) {
|
||||
// I am presented on an existing navigation stack, so
|
||||
// dismiss myself and push the bookmark there
|
||||
UINavigationController *presenter = (id)self.presentingViewController;
|
||||
[presenter dismissViewControllerAnimated:YES completion:^{
|
||||
[presenter pushViewController:explorer animated:YES];
|
||||
}];
|
||||
} else {
|
||||
// Dismiss myself and present explorer
|
||||
UIViewController *presenter = self.corePresenter;
|
||||
[presenter dismissViewControllerAnimated:YES completion:^{
|
||||
[presenter presentViewController:[FLEXNavigationController
|
||||
withRootViewController:explorer
|
||||
] animated:YES completion:nil];
|
||||
}];
|
||||
}
|
||||
} else {
|
||||
// Just dismiss myself
|
||||
[self dismissViewControllerAnimated:YES completion:nil];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)toggleEditing {
|
||||
NSArray<NSIndexPath *> *selected = self.tableView.indexPathsForSelectedRows;
|
||||
self.editing = !self.editing;
|
||||
|
||||
if (self.isEditing) {
|
||||
[self setupEditingBarItems];
|
||||
} else {
|
||||
[self setupDefaultBarItems];
|
||||
|
||||
// Get index set of bookmarks to close
|
||||
NSMutableIndexSet *indexes = [NSMutableIndexSet new];
|
||||
for (NSIndexPath *ip in selected) {
|
||||
[indexes addIndex:ip.row];
|
||||
}
|
||||
|
||||
if (selected.count) {
|
||||
// Close bookmarks and update data source
|
||||
[FLEXBookmarkManager.bookmarks removeObjectsAtIndexes:indexes];
|
||||
[self reloadData];
|
||||
|
||||
// Remove deleted rows
|
||||
[self.tableView deleteRowsAtIndexPaths:selected withRowAnimation:UITableViewRowAnimationAutomatic];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)closeAllButtonPressed:(UIBarButtonItem *)sender {
|
||||
[FLEXAlert makeSheet:^(FLEXAlert *make) {
|
||||
NSInteger count = self.bookmarks.count;
|
||||
NSString *title = FLEXPluralFormatString(count, @"Remove %@ bookmarks", @"Remove %@ bookmark");
|
||||
make.button(title).destructiveStyle().handler(^(NSArray<NSString *> *strings) {
|
||||
[self closeAll];
|
||||
[self toggleEditing];
|
||||
});
|
||||
make.button(@"Cancel").cancelStyle();
|
||||
} showFrom:self source:sender];
|
||||
}
|
||||
|
||||
- (void)closeAll {
|
||||
NSInteger rowCount = self.bookmarks.count;
|
||||
|
||||
// Close bookmarks and update data source
|
||||
[FLEXBookmarkManager.bookmarks removeAllObjects];
|
||||
[self reloadData];
|
||||
|
||||
// Delete rows from table view
|
||||
NSArray<NSIndexPath *> *allRows = [NSArray flex_forEachUpTo:rowCount map:^id(NSUInteger row) {
|
||||
return [NSIndexPath indexPathForRow:row inSection:0];
|
||||
}];
|
||||
[self.tableView deleteRowsAtIndexPaths:allRows withRowAnimation:UITableViewRowAnimationAutomatic];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Table View Data Source
|
||||
|
||||
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
|
||||
return self.bookmarks.count;
|
||||
}
|
||||
|
||||
- (UITableViewCell *)tableView:(FLEXTableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kFLEXDetailCell forIndexPath:indexPath];
|
||||
|
||||
id object = self.bookmarks[indexPath.row];
|
||||
cell.textLabel.text = [FLEXRuntimeUtility safeDescriptionForObject:object];
|
||||
cell.detailTextLabel.text = [NSString stringWithFormat:@"%@ — %p", [object class], object];
|
||||
|
||||
return cell;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Table View Delegate
|
||||
|
||||
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
if (self.editing) {
|
||||
// Case: editing with multi-select
|
||||
self.toolbarItems.lastObject.title = @"Remove Selected";
|
||||
self.toolbarItems.lastObject.tintColor = FLEXColor.destructiveColor;
|
||||
} else {
|
||||
// Case: selected a bookmark
|
||||
[self dismissAnimated:self.bookmarks[indexPath.row]];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)tableView:(UITableView *)tableView didDeselectRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
NSParameterAssert(self.editing);
|
||||
|
||||
if (tableView.indexPathsForSelectedRows.count == 0) {
|
||||
self.toolbarItems.lastObject.title = @"Done";
|
||||
self.toolbarItems.lastObject.tintColor = self.view.tintColor;
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)tableView:(UITableView *)table
|
||||
commitEditingStyle:(UITableViewCellEditingStyle)edit
|
||||
forRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
NSParameterAssert(edit == UITableViewCellEditingStyleDelete);
|
||||
|
||||
// Remove bookmark and update data source
|
||||
[FLEXBookmarkManager.bookmarks removeObjectAtIndex:indexPath.row];
|
||||
[self reloadData];
|
||||
|
||||
// Delete row from table view
|
||||
[table deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,61 @@
|
||||
//
|
||||
// FLEXExplorerViewController.h
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 4/4/14.
|
||||
// Copyright (c) 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXExplorerToolbar.h"
|
||||
|
||||
@class FLEXWindow;
|
||||
@protocol FLEXExplorerViewControllerDelegate;
|
||||
|
||||
/// A view controller that manages the FLEX toolbar.
|
||||
@interface FLEXExplorerViewController : UIViewController
|
||||
|
||||
@property (nonatomic, weak) id <FLEXExplorerViewControllerDelegate> delegate;
|
||||
@property (nonatomic, readonly) BOOL wantsWindowToBecomeKey;
|
||||
|
||||
@property (nonatomic, readonly) FLEXExplorerToolbar *explorerToolbar;
|
||||
|
||||
- (BOOL)shouldReceiveTouchAtWindowPoint:(CGPoint)pointInWindowCoordinates;
|
||||
|
||||
/// @brief Used to present (or dismiss) a modal view controller ("tool"),
|
||||
/// typically triggered by pressing a button in the toolbar.
|
||||
///
|
||||
/// If a tool is already presented, this method simply dismisses it and calls the completion block.
|
||||
/// If no tool is presented, @code future() @endcode is presented and the completion block is called.
|
||||
- (void)toggleToolWithViewControllerProvider:(UINavigationController *(^)(void))future
|
||||
completion:(void (^)(void))completion;
|
||||
|
||||
/// @brief Used to present (or dismiss) a modal view controller ("tool"),
|
||||
/// typically triggered by pressing a button in the toolbar.
|
||||
///
|
||||
/// If a tool is already presented, this method dismisses it and presents the given tool.
|
||||
/// The completion block is called once the tool has been presented.
|
||||
- (void)presentTool:(UINavigationController *(^)(void))future
|
||||
completion:(void (^)(void))completion;
|
||||
|
||||
// Keyboard shortcut helpers
|
||||
|
||||
- (void)toggleSelectTool;
|
||||
- (void)toggleMoveTool;
|
||||
- (void)toggleViewsTool;
|
||||
- (void)toggleMenuTool;
|
||||
|
||||
/// @return YES if the explorer used the key press to perform an action, NO otherwise
|
||||
- (BOOL)handleDownArrowKeyPressed;
|
||||
/// @return YES if the explorer used the key press to perform an action, NO otherwise
|
||||
- (BOOL)handleUpArrowKeyPressed;
|
||||
/// @return YES if the explorer used the key press to perform an action, NO otherwise
|
||||
- (BOOL)handleRightArrowKeyPressed;
|
||||
/// @return YES if the explorer used the key press to perform an action, NO otherwise
|
||||
- (BOOL)handleLeftArrowKeyPressed;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
@protocol FLEXExplorerViewControllerDelegate <NSObject>
|
||||
- (void)explorerViewControllerDidFinish:(FLEXExplorerViewController *)explorerViewController;
|
||||
@end
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user