Compare commits
1353 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 44c88877c5 | |||
| 6f2ac3f671 | |||
| 007583929c | |||
| c97d6bd78c | |||
| 6089b09963 | |||
| 87a7a6a718 | |||
| c1493b9c5c | |||
| d47e835e8e | |||
| 4b456ad2bb | |||
| 125ff93795 | |||
| 2a1bbeefe8 | |||
| ddc023fd4c | |||
| 160bb111f0 | |||
| 30742a5043 | |||
| ead0650dfb | |||
| 4f3d0f33df | |||
| 608d954197 | |||
| 74326baef4 | |||
| 5b1abe95c5 | |||
| fe9c9e1fc5 | |||
| b8b7b80f09 | |||
| 1bcc12bd80 | |||
| eb9464b6ce | |||
| c476ad838e | |||
| c167e4d84c | |||
| 77eb12636a | |||
| 27f4197374 | |||
| 34f473d8e5 | |||
| 42b9907579 | |||
| 67a04562d6 | |||
| f86668e8cb | |||
| 33ca348856 | |||
| bccef20ce5 | |||
| 0f92862e52 | |||
| c7a8cdd2c0 | |||
| 75142028b0 | |||
| d618d45635 | |||
| 7a33c6b4a7 | |||
| add20301a1 | |||
| fbc4dde858 | |||
| 18b6557cf3 | |||
| c195cf1a16 | |||
| 6679fb0922 | |||
| 740051812a | |||
| 5c602d7850 | |||
| 0b4b2ae6e5 | |||
| 0efee740e4 | |||
| 856d544fad | |||
| c257751d1d | |||
| 81e879e8f6 | |||
| ddeed599df | |||
| 65f525ff38 | |||
| 60bfacf507 | |||
| 0eae7e87f7 | |||
| 26597eb334 | |||
| 44c5535a8d | |||
| d06f070e83 | |||
| 633d31be97 | |||
| 6936eb34ac | |||
| 40891abb63 | |||
| f9b8e39876 | |||
| e14d327e3b | |||
| b9bcd6713c | |||
| bc2801bb9e | |||
| ba78feb4dd | |||
| 56a8bc1db3 | |||
| e86ef34a75 | |||
| 912a8112fb | |||
| 27644a292c | |||
| 3f2cb32b23 | |||
| d84944ac6a | |||
| ebfbfcb900 | |||
| 3bd27d6ebe | |||
| e11440b036 | |||
| fdedb11fea | |||
| 72a665b961 | |||
| cfe04441ad | |||
| 7fe3e227ad | |||
| 4d8f4f3bb1 | |||
| e29e7f2b73 | |||
| f2e133676b | |||
| 0c898b4abb | |||
| c1fa1ea154 | |||
| f659531d77 | |||
| 6e22f94cf5 | |||
| 88c7c87f28 | |||
| 6a9da73a8a | |||
| 0f0448bff7 | |||
| fd2c914b68 | |||
| 5122948de0 | |||
| 19d3624658 | |||
| 4f68c2bf4e | |||
| 9e1ed9e070 | |||
| 9fb7562ce5 | |||
| b1b25f9ac4 | |||
| a5b58c4c8e | |||
| 66c7b86484 | |||
| 7b850951ef | |||
| bd5f5bb231 | |||
| 2526450391 | |||
| 289a45c3dd | |||
| 931c7d7728 | |||
| 6d52269995 | |||
| f69401cae9 | |||
| 3d57bf65b8 | |||
| e7b75b5f88 | |||
| 590bb25559 | |||
| f7345070a9 | |||
| 558b5391a3 | |||
| 4ea6fc7d46 | |||
| 2289b01f7c | |||
| 4899548438 | |||
| bcc85b9a2c | |||
| 613d3a676c | |||
| e5fa2374ab | |||
| 859bbb4e17 | |||
| 9fe59ca7a8 | |||
| b3d72ce28a | |||
| e421214098 | |||
| 53e19548c4 | |||
| f3fdb66cf8 | |||
| ead682b402 | |||
| aeb3849fd7 | |||
| b9af6508ac | |||
| 2ff97e4558 | |||
| 26da4d8e34 | |||
| 6ee2b47dc1 | |||
| 84f806ee11 | |||
| a24dc1ac56 | |||
| c3618bb104 | |||
| 119b5f19d2 | |||
| 5195ed44f1 | |||
| 755db8b700 | |||
| 3d95111ace | |||
| 859fc28d8a | |||
| ebc4ccacc0 | |||
| 1c3aab92c1 | |||
| 70093149b5 | |||
| d595a0b199 | |||
| 764b5f8691 | |||
| 395eb936d5 | |||
| dcab7194c1 | |||
| 9c271d87f6 | |||
| d9ebc6d253 | |||
| 2e555356a6 | |||
| 69c830b8cd | |||
| a5aa647ed0 | |||
| d7d923e609 | |||
| 0cbb21d71d | |||
| f5fe7a47f3 | |||
| cbea809430 | |||
| a65d5da62f | |||
| 49f2914063 | |||
| f50340c7e1 | |||
| dfcc02d75f | |||
| 6ad71820d8 | |||
| 275cc3ae17 | |||
| 5f643fdcde | |||
| e444c32eb5 | |||
| 7c7dfc4861 | |||
| 9b9e941299 | |||
| 27a0755414 | |||
| 0dfb3455f7 | |||
| 47d6b5e0f9 | |||
| dbb0fcf2b7 | |||
| 170ca3e0b6 | |||
| 2c868e3bb4 | |||
| 1abd8bf548 | |||
| 5db0f6422e | |||
| 47f1a3bfd1 | |||
| 231ed3f90c | |||
| dd465a00e7 | |||
| 05325d6070 | |||
| ee446da653 | |||
| b39c2ea10d | |||
| 106b2d969f | |||
| cd02eefff7 | |||
| 55b97af36a | |||
| fa50f74e9c | |||
| 903e1cbffe | |||
| 4cda98e896 | |||
| 525d5b2350 | |||
| 191c17930d | |||
| cfe39f8201 | |||
| 4b35370e80 | |||
| 2404b20433 | |||
| a20f953467 | |||
| f41480f5bb | |||
| ddef74e9cc | |||
| a50f7cbc2f | |||
| 5715ea13c5 | |||
| 41575b830c | |||
| c6907b1e9e | |||
| 286f49bb5d | |||
| 59ac9c3213 | |||
| 0cfce94dd6 | |||
| cb4ff44859 | |||
| 0375b70146 | |||
| e9e2434d5f | |||
| 72fb6d5138 | |||
| 60aa464b02 | |||
| 8474d39cdf | |||
| 3f258d2c6c | |||
| aad49eac86 | |||
| 4b610ee085 | |||
| 78d59f7ed8 | |||
| a147b2e6e3 | |||
| 78bc96956c | |||
| 0e9ce26b82 | |||
| e2f284a4cb | |||
| 90af9211eb | |||
| 1107bd9dba | |||
| 4bcc83ecd3 | |||
| aa86bf3bed | |||
| 5de1888375 | |||
| 01eab7bd0c | |||
| 1da1d632b6 | |||
| 072d19c84b | |||
| 11effe846e | |||
| c185fce465 | |||
| 4863368e23 | |||
| 1ce947395d | |||
| c15b98e41a | |||
| 9341e85030 | |||
| 1934f143a5 | |||
| 9b1efb8641 | |||
| c1f30947be | |||
| dc22fe8191 | |||
| 4259a1cc92 | |||
| 70d4532d0e | |||
| 8e2313118f | |||
| 354cee7e7a | |||
| 7cfb8960c9 | |||
| cabaecb5d1 | |||
| 99bd214f23 | |||
| ea7c941424 | |||
| 75e880e1d9 | |||
| a788e35d25 | |||
| e9d7b847da | |||
| b0f5e2cbb7 | |||
| 2c4ef0f73e | |||
| 23900bed0e | |||
| 8c8a1adc47 | |||
| 4ada9cf5ff | |||
| 7b23d8b21e | |||
| d2f26a5ab9 | |||
| c170a8c38a | |||
| 934aa3283b | |||
| 639f75eaa6 | |||
| 5f969da0d7 | |||
| 080c69bd8b | |||
| 42c12ca73f | |||
| 025581ad55 | |||
| 65aee797ce | |||
| f7ea0167b1 | |||
| a5a8531fdf | |||
| e60db320ed | |||
| 8a6895d762 | |||
| 2ac579a610 | |||
| 4b9ae8e404 | |||
| f7d1591c19 | |||
| 9b539d7faf | |||
| e515278b4c | |||
| e3b2d91a8b | |||
| da41b8a43b | |||
| 86273181ac | |||
| 930d94f804 | |||
| d783c0b2b6 | |||
| 92cc2c0ef9 | |||
| 275b7b5ff9 | |||
| 642f87576a | |||
| 4735bc5d47 | |||
| 0e29f50f77 | |||
| daac3505b4 | |||
| 89838d63bd | |||
| 1337dbf851 | |||
| 0c60532068 | |||
| ea0fb64319 | |||
| 14bb563d26 | |||
| 09b7adc981 | |||
| 412f30652f | |||
| 9ef8b4a980 | |||
| ce365bbe10 | |||
| 778bcf71e0 | |||
| bf94333d32 | |||
| 9f5caad71d | |||
| 14fce4445f | |||
| 1984b46efb | |||
| 02a822baef | |||
| cdaa4d4c91 | |||
| 6e4a4560d8 | |||
| 657bcab2f7 | |||
| b8a999995f | |||
| 28ee60beb7 | |||
| e305e3c4b2 | |||
| 90f5a2cc62 | |||
| 75c75901a3 | |||
| c5396e8ae6 | |||
| 942a092bd1 | |||
| 1cb09d68d3 | |||
| fd11ee1e89 | |||
| 878acecad6 | |||
| 5d1fe51b42 | |||
| 077dfcc4ab | |||
| f24c371541 | |||
| 7a0101c12b | |||
| 887f8dd6bd | |||
| f6c8bc939b | |||
| b6cc2f3add | |||
| 24929ca8b1 | |||
| 8c270dca89 | |||
| 129ee6baef | |||
| a2e4b5aa1c | |||
| 75047cabb2 | |||
| 8a3fea6488 | |||
| 57cbebae64 | |||
| b13a470348 | |||
| 2ea828e29e | |||
| 7efb9ecb36 | |||
| ea777873af | |||
| fc7c55c3ab | |||
| df05877256 | |||
| 9025045a4c | |||
| 0571b6e960 | |||
| d1fbc178a1 | |||
| 6a2f18af4d | |||
| cc352493f9 | |||
| 43a4379c8e | |||
| 5c3db570f7 | |||
| 0980410b95 | |||
| 2e5d784608 | |||
| 288d710e51 | |||
| cc46125c69 | |||
| ca94aa7468 | |||
| d0293a0ee7 | |||
| b8c80c70c7 | |||
| 40e328e29e | |||
| 3f89fba2cf | |||
| bece29b351 | |||
| 4d1601a787 | |||
| 80d7685ab3 | |||
| 6e1334a2fc | |||
| 3c550b54b4 | |||
| 7ca7079522 | |||
| e4af7fe7fe | |||
| 8c3b5ce81f | |||
| 783ad4c6b4 | |||
| b85182b372 | |||
| 6e075cbfe3 | |||
| 410affbf5d | |||
| 182960a263 | |||
| 0971ef71f4 | |||
| 92ca8f86a5 | |||
| 24c46898e8 | |||
| 065a02eff1 | |||
| 2db99d6152 | |||
| f6cb8b09f0 | |||
| f9d49594fe | |||
| 62a31b087a | |||
| 70fd57488d | |||
| 1f457e1d54 | |||
| 729247cfcd | |||
| 218b8af66a | |||
| 069c388cb3 | |||
| e199db13b4 | |||
| a96c8b9540 | |||
| 865275d3aa | |||
| a1d29fe225 | |||
| 67bcc93947 | |||
| 8e8fabf79f | |||
| 292727ad91 | |||
| 2f1e58e234 | |||
| 86226e9ba8 | |||
| d40e8193d5 | |||
| 525f2c6efb | |||
| ad52da287c | |||
| 8d0fd95ad0 | |||
| 46c78c08a4 | |||
| 54927408bf | |||
| f325310d77 | |||
| dd4e4fdbce | |||
| 063cde7656 | |||
| fb1ef39165 | |||
| c570771b4d | |||
| 00ff2f3402 | |||
| 407de8ed11 | |||
| 6474ebfff7 | |||
| 340ba7210d | |||
| 8c86b8d80d | |||
| 21c89256fa | |||
| 62ae57bb11 | |||
| fbc4191081 | |||
| 429c3c9886 | |||
| 4228cf46c6 | |||
| 674e06cebc | |||
| ce31ebd366 | |||
| 135ec4a3f2 | |||
| a30119aa0d | |||
| 31526c6664 | |||
| e5ac62f298 | |||
| ade7f99010 | |||
| bb1be965a2 | |||
| 3fb266139e | |||
| 0b2216c6cf | |||
| c90d8c7fbd | |||
| 3cb705c123 | |||
| 94fbd3a6d1 | |||
| f24a74006a | |||
| 040b9d2220 | |||
| 2e1749a14d | |||
| 1817398f61 | |||
| b542c46189 | |||
| b6cc636975 | |||
| 1844a0774c | |||
| 6a1343622d | |||
| a3b45b6268 | |||
| 204d5dbf40 | |||
| f076b69a98 | |||
| 908b9cb713 | |||
| ff2c5ad055 | |||
| 39e17e7867 | |||
| 66fa232ca7 | |||
| 75ad7512b8 | |||
| 8b521477dd | |||
| d60c81b004 | |||
| a44f2db8fd | |||
| 663426250f | |||
| 58d377d9ab | |||
| e56bf7d217 | |||
| 28ff1415dd | |||
| 8cbe836bb2 | |||
| 2289abf03a | |||
| 44ee48fa54 | |||
| b704be75b5 | |||
| 4048e34530 | |||
| b34fb01350 | |||
| 1b266c3652 | |||
| 8ab052577d | |||
| 1e34cf6d35 | |||
| d39a72e535 | |||
| 135e2209c8 | |||
| b865da3a75 | |||
| 40111a1431 | |||
| cc45d92266 | |||
| 197eb8422a | |||
| fe10cd8a71 | |||
| 63dfad2b0d | |||
| 886605e013 | |||
| 1619428bb0 | |||
| 3ae773049a | |||
| 76eb4f97d9 | |||
| 4c0a4a1e51 | |||
| 12326634db | |||
| 36b90b813c | |||
| 22b769b786 | |||
| 0afc2027d7 | |||
| aa2b1f9e61 | |||
| a78878c712 | |||
| f6fd29087b | |||
| 4517cd445e | |||
| d151e11004 | |||
| fed9fb9ce6 | |||
| 6a786cfb37 | |||
| 39d76c8ed6 | |||
| ec9b1c9108 | |||
| 7059bd39a6 | |||
| dddc633e3a | |||
| 9e358e7fc5 | |||
| 0aacde2057 | |||
| 5bdd973315 | |||
| d7bf7a6ec3 | |||
| 4342caae71 | |||
| cb4514328e | |||
| 84710f773f | |||
| 42005af39c | |||
| f7d7355c49 | |||
| 9c847f8fe6 | |||
| aac584b2f1 | |||
| 4be283e888 | |||
| 4d37142873 | |||
| 08b48589da | |||
| 6ba85a1504 | |||
| 5fc4316e22 | |||
| 287dcd979c | |||
| 29f2d16676 | |||
| b022ae3589 | |||
| 76bbbbe604 | |||
| a322f0b17f | |||
| f11d17752f | |||
| 14a815d63e | |||
| 1b4b0166b8 | |||
| 669269fd45 | |||
| 3f7c1d15d5 | |||
| ca136a9ce1 | |||
| fe2ea1e3d8 | |||
| eedcff0ab0 | |||
| 00bd267c1b | |||
| d9c005aa18 | |||
| be02e1a22a | |||
| b474f6f12d | |||
| bc263d92af | |||
| 0575f21c3e | |||
| 88543a14d6 | |||
| cd8a801738 | |||
| 30af7f46f7 | |||
| 1e889ad7dc | |||
| 839c8eca53 | |||
| 48ed4b3771 | |||
| e6f4c1deb8 | |||
| ae2bff43fa | |||
| 14260e0df1 | |||
| 4153702c2d | |||
| a9494ce5d8 | |||
| 0b06d4c18a | |||
| 2b6d1fcab9 | |||
| 610ce5aefd | |||
| bd52a14196 | |||
| eb52acefa9 | |||
| 423a89c879 | |||
| e444086399 | |||
| 82de8783bd | |||
| 0dea3dd673 | |||
| 6f4c072f93 | |||
| 0ed1df3945 | |||
| fee0d2d7b6 | |||
| d71ab9538c | |||
| 7caaf525e3 | |||
| 610a1fd93f | |||
| 36bda05556 | |||
| 069ea16731 | |||
| a1ed3ea711 | |||
| d910062bb9 | |||
| ba5d80c6c1 | |||
| 21ada70ae4 | |||
| 9be3f057be | |||
| 51c23ccf17 | |||
| 113b7422a0 | |||
| c852c62262 | |||
| c27b784870 | |||
| c3102d6183 | |||
| 2510bc7f97 | |||
| de7860b60a | |||
| 37c65bdc55 | |||
| 4fe13d0b27 | |||
| 853d4384fd | |||
| f9669508dc | |||
| e1ea72b861 | |||
| 1cf445af09 | |||
| fc1c328b68 | |||
| 7b7a851cca | |||
| 37ac134b5e | |||
| 067f8f8e3d | |||
| 7d30dd09f3 | |||
| 73495244be | |||
| 85dd559217 | |||
| 81c8c10ebc | |||
| 19921baf55 | |||
| 63a7e30049 | |||
| 8fa839a66b | |||
| ffd8552aa8 | |||
| 140e1ebae0 | |||
| 9338ea8b59 | |||
| 63d56e44f8 | |||
| 3daf9484c4 | |||
| d87372923b | |||
| 3dccc6d438 | |||
| f8a77a1b1e | |||
| ddc0016bd1 | |||
| 50cf8171f5 | |||
| d2c51201ef | |||
| 8a320f0b72 | |||
| 8b81a8c5f1 | |||
| ff673f0ad4 | |||
| 116a8e08ad | |||
| 2988cb0fa6 | |||
| 59788e53ec | |||
| 09e717c69f | |||
| 9b1bdd390c | |||
| af823d1f63 | |||
| 75ac5d3366 | |||
| b113ef0ee3 | |||
| 8e49cbc733 | |||
| 37cfadf81a | |||
| 932024c4af | |||
| 26cf7e4d64 | |||
| fa2b18da42 | |||
| fb778eefcf | |||
| f384f9137d | |||
| 2f19a3e089 | |||
| b112945365 | |||
| 58c12a025b | |||
| 304d93a79a | |||
| a57a8d8370 | |||
| 2aa09084c8 | |||
| 4d16e39f73 | |||
| d44676d816 | |||
| 18d50721cc | |||
| 967518a576 | |||
| ef8c1e1b3a | |||
| 6563ebdca4 | |||
| f85faecb49 | |||
| 0341e511ab | |||
| 59d2ed7a39 | |||
| 8f9ab33897 | |||
| e5a4d01039 | |||
| e7aac14571 | |||
| 372fbaaaa4 | |||
| a3c28f666c | |||
| 4c34d1e8a3 | |||
| 50c0d4b3c0 | |||
| 746b0866b0 | |||
| 4b269b8c21 | |||
| 143b474aff | |||
| ca11a8def0 | |||
| 09cb204a24 | |||
| 20204946bc | |||
| 74dc33a654 | |||
| 11a29435cd | |||
| f738f18570 | |||
| 7af35c7c35 | |||
| ca2f7848f8 | |||
| 85097a5e20 | |||
| 60af4ce1e5 | |||
| 8c10abfd74 | |||
| 0f97c0704a | |||
| a9ea108054 | |||
| ac0a9a25d6 | |||
| 4ca0197532 | |||
| e021f815ca | |||
| 1492ab82c1 | |||
| 61408fc5f6 | |||
| 0459818613 | |||
| 99bdc3cbbc | |||
| c0b751d05a | |||
| d3ed706680 | |||
| d5175083ef | |||
| 721572a533 | |||
| 2494895cf9 | |||
| 00886afe69 | |||
| fdd608fee4 | |||
| b630dff27e | |||
| c41fac8023 | |||
| 160b7c5f71 | |||
| 1b5ea88e0f | |||
| dd42f4740e | |||
| 1f26d5c5a1 | |||
| 0a07f24bea | |||
| 224146f7fd | |||
| 5e7768483e | |||
| 2819b7714d | |||
| 81ec1f0056 | |||
| 36f710739d | |||
| 66884c045b | |||
| 04f5a9e9f6 | |||
| e7ca832c3d | |||
| af3285f5f0 | |||
| 50cd9055ac | |||
| d0226f29f7 | |||
| eceeab053d | |||
| aa2e83e2b5 | |||
| de0019054a | |||
| cbf9d7284f | |||
| 00183879ce | |||
| 58453f5df9 | |||
| 9c65780911 | |||
| f7f8e12d9d | |||
| ee494c5ed1 | |||
| 12a2d556c9 | |||
| ce6d4fe3ac | |||
| 05f2fe9db4 | |||
| 7e8175b8a2 | |||
| c3460d8aaa | |||
| 886bca74bf | |||
| 79deaaa4b2 | |||
| 53bb638c69 | |||
| 3d40007df9 | |||
| 5070517ba3 | |||
| 5bca8eb66d | |||
| 4dff0189df | |||
| 9e1e855b70 | |||
| 4e7f24f1c2 | |||
| 3229df2a48 | |||
| 7bc407ee90 | |||
| 96e94155d7 | |||
| b8b5648c71 | |||
| 0471bf61d4 | |||
| a0ca62ec23 | |||
| a581d58243 | |||
| 718a4d3ed7 | |||
| 9241848bc3 | |||
| 913be47d6d | |||
| 88a94e106b | |||
| 2469bfe5a3 | |||
| 6ec5df751d | |||
| aac14a3a1a | |||
| 696758bdc2 | |||
| 43b6585bfa | |||
| f47f9d824c | |||
| 7ef55e55e7 | |||
| 9b4152ec6e | |||
| b58ae6139b | |||
| 7ba97f4089 | |||
| a2bfb9a15d | |||
| 17b7d7d4f9 | |||
| d9e2f54c41 | |||
| 9347ef6573 | |||
| ed4a50dacc | |||
| d8d3882a1c | |||
| ec8308b8bd | |||
| 9296fe90bc | |||
| df9170f9cf | |||
| 0deb8ba55e | |||
| b24310adbc | |||
| c138f6aa02 | |||
| 49e541d1b7 | |||
| f22676a8e7 | |||
| d34749e1ef | |||
| 0e4251d511 | |||
| 06ff988941 | |||
| 3b3c907d39 | |||
| b1515e0fc2 | |||
| f9924ddbc0 | |||
| bbde9865ee | |||
| cfa9e90d76 | |||
| 851bd4e2c5 | |||
| 9fbabf8f3b | |||
| 536a22f733 | |||
| e51c9b708a | |||
| b45425ba72 | |||
| 181651b365 | |||
| b8d2c6bf86 | |||
| feb5f11e3e | |||
| 8d4b73ae8f | |||
| 81e67dc6fa | |||
| e873af81d6 | |||
| 59681fbab5 | |||
| a3a53b7c68 | |||
| a28e2596bd | |||
| f5e8b40f6d | |||
| 55bc242896 | |||
| d3dee01678 | |||
| a6445546fd | |||
| 44a786d51d | |||
| 5954a6b254 | |||
| 06eecb054c | |||
| c38da58f6b | |||
| 17c35e6765 | |||
| 1074eefbbe | |||
| 6c54eea404 | |||
| daa267b8f0 | |||
| 7f66623c59 | |||
| 7854c619a3 | |||
| deef811bc9 | |||
| b9fff0e4c0 | |||
| f35e84189c | |||
| 5ff0f9324a | |||
| 35d33709b2 | |||
| 0d9c98dd56 | |||
| 2ac5e30d85 | |||
| d1e246bba4 | |||
| 0dc31e4593 | |||
| 521dba3d4e | |||
| 07e05298a0 | |||
| 9214c48d41 | |||
| ebdc7a218a | |||
| 3d9cc15c0b | |||
| 41d6a415ad | |||
| f45722366c | |||
| 10152cfb79 | |||
| 038794df0d | |||
| d74bfe956f | |||
| a3b2d68b7a | |||
| 87560025c8 | |||
| af9588fd93 | |||
| 80ee2b8792 | |||
| 8dffe81ea7 | |||
| 95099e34de | |||
| 9dc22ff97e | |||
| df7a5d2ae4 | |||
| 475d84de73 | |||
| cc6380af67 | |||
| ebf3a5474d | |||
| f8a94a0f66 | |||
| 1afb67bf9e | |||
| 258afcb7df | |||
| 8a3882d85c | |||
| 2bd667bc6f | |||
| 7a4715dd72 | |||
| 8f7047220f | |||
| fda01e8dd0 | |||
| 8042df3127 | |||
| 153726633f | |||
| 4040d962a0 | |||
| 861211fa3b | |||
| a8f6be61f3 | |||
| 7c01903108 | |||
| 848800bcd6 | |||
| caa324ec79 | |||
| 27bf948f6d | |||
| ed60c84a2d | |||
| e70d56142d | |||
| a4c9286f29 | |||
| 97c5ada28f | |||
| fd0d4af750 | |||
| dc81d8af3a | |||
| deac6b5de0 | |||
| 2c504f4b01 | |||
| db4b19b229 | |||
| 129d2ac3df | |||
| 0790e52a5a | |||
| 9784c1425a | |||
| df13dca4d9 | |||
| acfce230b4 | |||
| bfac86d706 | |||
| c2e2951d6f | |||
| 36a578549b | |||
| 165e107ddc | |||
| e32efc4ec2 | |||
| 3479c35fb3 | |||
| 74fbc9af77 | |||
| ec91b7b139 | |||
| d4403704ce | |||
| 4919e0fc0c | |||
| dc3c6e011c | |||
| 4c49283504 | |||
| 26a094e05f | |||
| d6504c68ef | |||
| f90af1ce01 | |||
| ae6fcf4ce4 | |||
| 8730a26be1 | |||
| 20d6defb6f | |||
| cf917dc208 | |||
| 47ad36d906 | |||
| e69228445a | |||
| 90619ac2af | |||
| 43c4b892b2 | |||
| 1c2028ce5d | |||
| 29942a01d5 | |||
| c159d6db61 | |||
| 7635528b2f | |||
| d5b6b0f87a | |||
| 328b8421a3 | |||
| b4a2cbebd0 | |||
| 9a749c5489 | |||
| 09272a78df | |||
| 92ae7cf071 | |||
| b5568e278b | |||
| 33864f1012 | |||
| 2534e6b20e | |||
| 56b50e9296 | |||
| 8eb756b96c | |||
| 52d5fcabe7 | |||
| bf07984430 | |||
| 0554cbd6f1 | |||
| b34d1ecd29 | |||
| f3c7c53e21 | |||
| 59045b27a5 | |||
| 14ea5cc282 | |||
| ec04a5c633 | |||
| f1e4941f3f | |||
| bb01f0bb4d | |||
| 7ebc03c93f | |||
| 413bc6682a | |||
| 9ccf4fb743 | |||
| 723075a61f | |||
| c659539a45 | |||
| 48661fe670 | |||
| b9fadc1b87 | |||
| 11dc10e66e | |||
| 6911fe614f | |||
| 452665de14 | |||
| 1f352c39d2 | |||
| e42c92004a | |||
| 17ac3f5177 | |||
| 400da43f42 | |||
| c0aed6d641 | |||
| f7e9c672e2 | |||
| 7f106898e2 | |||
| d3da59a4f6 | |||
| 8a78e95ebb | |||
| c113deb1a3 | |||
| 3097232bd5 | |||
| 04a55343bc | |||
| 485e6035e0 | |||
| 785ea40fc4 | |||
| 69319a3c5e | |||
| c10ab44036 | |||
| 3fdbad88b2 | |||
| 2287b99b86 | |||
| 3d6a7fa481 | |||
| 0759764889 | |||
| e3ab9d75ef | |||
| f401dcf94d | |||
| b941379779 | |||
| f2d13d1305 | |||
| 96e5f26f7d | |||
| cecef562a9 | |||
| 7d2d04239f | |||
| 2b649f9e12 | |||
| a9478c3cb0 | |||
| 9b64b9c0f0 | |||
| ac0f3c542c | |||
| 2e356cac03 | |||
| 91d4fa7890 | |||
| 18504e9ffe | |||
| d80ca89094 | |||
| 706900ff07 | |||
| 20cfa9f588 | |||
| 76aef1a069 | |||
| 4afe6dcf77 | |||
| 4d82dcb8f3 | |||
| d2c381fefb | |||
| fbfeb233fa | |||
| 2a4b1b370a | |||
| 52ef002a67 | |||
| 65e2946cba | |||
| 52fbba25b8 | |||
| 73c59a40b9 | |||
| 87c6feceaa | |||
| 0180500607 | |||
| 42e6c97ee2 | |||
| 4bfc412626 | |||
| 8621b38a89 | |||
| 8517354463 | |||
| 250d016306 | |||
| a2a73b1661 | |||
| 46e61011a2 | |||
| 13ab769dd8 | |||
| 30f3900307 | |||
| f85a4c0c06 | |||
| 32fc46f896 | |||
| f15940f83e | |||
| 310080fc8e | |||
| b41e0be80f | |||
| c7d1d99b32 | |||
| 684dd82c15 | |||
| c0ff5bc6e0 | |||
| d2381657fa | |||
| 3463f1a16f | |||
| 9878b34c2a | |||
| 81bc7221d1 | |||
| a5bb7b5506 | |||
| 921b280a3b | |||
| d313ab3a45 | |||
| 73657854d4 | |||
| 7dc91f531d | |||
| 294ec34d47 | |||
| ce6c79a60f | |||
| 86bcdaa3ee | |||
| bcfb0b4dbb | |||
| 82c325d6c7 | |||
| 6d2c1400b1 | |||
| 8fa8dbae9e | |||
| c7bd1f5c41 | |||
| bdb6849ab5 | |||
| 6d1d22d9fa | |||
| 393a2bf404 | |||
| f828fbd8c4 | |||
| 27193727e9 | |||
| a7173cc746 | |||
| b0977b55e0 | |||
| 6d792cfd83 | |||
| fb6b1ab2c5 | |||
| ab729432b4 | |||
| 5b5e53bdc3 | |||
| 2b6c69e01d | |||
| fb1c5e9b9a | |||
| f4b4d2aee1 | |||
| 96478c583d | |||
| a796e6cc1e | |||
| 8599553a79 | |||
| 8197c147e1 | |||
| 8bdb32fc7a | |||
| 16f9b1dbe1 | |||
| 9459e330d5 | |||
| 78be958aea | |||
| 3c9291ed24 | |||
| 5454cf83e1 | |||
| b4e8a91474 | |||
| 829f93523a | |||
| 98b0acaeb7 | |||
| 91a9acb9ef | |||
| 71bdaab7be | |||
| 3e18a08690 | |||
| 63b8bea246 | |||
| 7a1a1c6cdd | |||
| 2b7f05fd96 | |||
| d26712e5a1 | |||
| 0eba3970b5 | |||
| 20e9cb24d4 | |||
| 234f40306a | |||
| 66218e4e28 | |||
| 130898cafd | |||
| 7db4bf50c8 | |||
| feb26fa506 | |||
| c317b30e82 | |||
| af6b027b7c | |||
| 3c715cc211 | |||
| c7bddcf4d5 | |||
| ad9eefb19a | |||
| 537e67af78 | |||
| 1c16398bc7 | |||
| ca292c1987 | |||
| 7a9009759e | |||
| 2040bbd095 | |||
| 111ac5627a | |||
| 57f911a4bb | |||
| 17776d04a4 | |||
| 66b7806bce | |||
| c79b228fba | |||
| 0398fb3767 | |||
| f168e02e9e | |||
| 5d1ed481e3 | |||
| 7cd75c4d40 | |||
| aa58b643a0 | |||
| 3b0ce6478b | |||
| c545aa8cbb | |||
| 74ff026b92 | |||
| 257ee610b7 | |||
| 79b9351433 | |||
| 359f8f3387 | |||
| 1f7c856fdb | |||
| 58a5a2c3b2 | |||
| 54f064247e | |||
| 3f35a49f50 | |||
| c16c929054 | |||
| be6a922554 | |||
| 3711ab342c | |||
| 908f7b0f5f | |||
| 1be6949c2d | |||
| fc4e8a9915 | |||
| 413e607258 | |||
| 7847c813a5 | |||
| 956549c320 | |||
| 709e4b818c | |||
| 8a6f0a35bd | |||
| b218db0f63 | |||
| 8fe483a5e0 | |||
| 3a555592ff | |||
| 4fb39abac9 | |||
| fbaf85ae9b | |||
| 11a0427883 | |||
| d23e188d64 | |||
| d70ee216c8 | |||
| 0cd42556f5 | |||
| 3daec83a7f | |||
| ed44fd84d9 | |||
| be9d2e3486 | |||
| dc78de8fdd | |||
| 21f1dbb5af | |||
| 7bc31391fa | |||
| e932d118b9 | |||
| 874386987a | |||
| 2b27a9d038 | |||
| 293e0ed23c | |||
| c322e0455d | |||
| 9960b4376f | |||
| 1d3adbba45 | |||
| 07d1aba216 | |||
| b69d40a86b | |||
| 633f9eaaef | |||
| 2edf7bc481 | |||
| 92620b782b | |||
| 06ca255bb1 | |||
| 5baa3a423a | |||
| b56e622ea0 | |||
| 9fad01dd0c | |||
| f2c751a71f | |||
| 06d9244c80 | |||
| 6004058c50 | |||
| 29f883b04b | |||
| cc1e1ae435 | |||
| be6d351bb3 | |||
| eac89e0119 | |||
| e035236314 | |||
| 870a9f6ac5 | |||
| ea0c6a1cf8 | |||
| 48cc58f04c | |||
| 30a346e94d | |||
| 6c97c550ed | |||
| 50b3f1d170 | |||
| 601b16a985 | |||
| ecb0fde9c5 | |||
| 81be7627eb | |||
| f94929daae | |||
| 8d7ab7861c | |||
| 1354181f84 | |||
| 395d2727c6 | |||
| 7f67d9c4da | |||
| 2c5b5cda7d | |||
| 4fd95efe57 | |||
| fb223bb1bb | |||
| 831f136cd1 | |||
| 6ad9ad52af | |||
| b28d5e6456 | |||
| d7f9987ede | |||
| 86aaeae857 | |||
| 642bc3db80 | |||
| 7361442a04 | |||
| 398ae8029f | |||
| af0ca497d5 | |||
| 75f62ee5f7 | |||
| bd63d1576b | |||
| a4c521203c | |||
| b8aba5459e | |||
| f0507b973f | |||
| 58a9366bf5 | |||
| 5b298d17a7 | |||
| bca3ceae78 | |||
| d8c0ecbd1d | |||
| 133b154023 | |||
| 95baa8b943 | |||
| 0e433640c4 | |||
| 1ae771ee80 | |||
| c5b14925bd | |||
| fa40e30ee3 | |||
| 8736dd5165 | |||
| 4917b34291 | |||
| 29d3e0da89 | |||
| 9b68f5188f | |||
| ed3503370d | |||
| 43480170aa | |||
| fdaf18198f | |||
| aee5c48875 | |||
| 521715f7dd | |||
| 68572aefe2 | |||
| 731b65d65c | |||
| b584258946 | |||
| d46ab216f4 | |||
| b6bee7e9de | |||
| 7272291eb7 | |||
| 57be36482c | |||
| c7d180b3ad | |||
| ed6913e2ad | |||
| 9f79e144ce | |||
| 185789dfb3 | |||
| 5445b67b41 | |||
| 66286a80c9 | |||
| d9addcf469 | |||
| 7ba00fb853 | |||
| e4e2626823 | |||
| 48178b01c2 | |||
| 8b847e4de0 | |||
| 2a76ff2a2e | |||
| caa24b9ae6 | |||
| 0a3d938f8a | |||
| 1ea4960c29 | |||
| 7013e954bc | |||
| ec8c36260c | |||
| cfeb39ef5c | |||
| b56bda5aaa | |||
| a9857da213 | |||
| 3b3ec14acd | |||
| c499309897 | |||
| b813690c67 | |||
| 714f29e185 | |||
| d752d4d926 | |||
| 050cea0c51 | |||
| 5a20aeab03 | |||
| 748d13e56e | |||
| e9f3bc919b | |||
| b17dda9697 | |||
| 729ef4fa58 | |||
| e9cc5ea800 | |||
| 090b976083 | |||
| bd32b0ad24 | |||
| d86edbc3f2 | |||
| a7c7aa7466 | |||
| 72da218778 | |||
| 89571ef876 | |||
| dc6bb27f57 | |||
| 1c200251e8 | |||
| bdad58b3a7 | |||
| c79b0eebe9 | |||
| de05c7a632 | |||
| 0d07828edc | |||
| 527609982a | |||
| 01cb0e77be | |||
| 1fdd29c308 | |||
| b6e185284b | |||
| ade8879d54 | |||
| 015be2da3a | |||
| 2650360476 | |||
| d42e7eca38 | |||
| 074a00e2ae | |||
| f7b8901fd2 | |||
| c01d530cde | |||
| ceec6adf1a | |||
| dffbaeee6b | |||
| c45d2cb856 | |||
| 7e5b5b45ec | |||
| 739d22b5d3 | |||
| 070f449c57 | |||
| f76c960846 | |||
| a5640cb4b1 | |||
| 1c0ac116d1 | |||
| 222c017e0d | |||
| 90c9e1de94 | |||
| e8bd259952 | |||
| 07736ac5f3 | |||
| 21bb2884b5 | |||
| b323bd8980 | |||
| f5e2f54979 | |||
| 39ef6a21f0 | |||
| 215e4d6008 | |||
| 674a131060 | |||
| 778e8571d2 | |||
| e71356805c | |||
| 25612dc872 | |||
| 8087810ac2 | |||
| 3e99ad04c7 | |||
| 0a1eb9c8a2 | |||
| c11e70585a | |||
| 045abdd919 | |||
| f9ffd5e5f1 | |||
| 407a3da80e | |||
| c786ed06e1 | |||
| 2765fa5575 | |||
| 835df17c5a | |||
| 68e0c685db | |||
| 352fa46668 | |||
| 87d8db3218 | |||
| 458f290956 | |||
| f8c492aadd | |||
| 22bcc07047 | |||
| ca7479609c | |||
| d1adbf1f50 | |||
| 321b19adbe | |||
| d86fba5c56 | |||
| 9614d50af9 | |||
| 42f9f6f03d | |||
| 43e4ae11b2 | |||
| 848c4dee8b | |||
| a750b2718d | |||
| 071242f217 | |||
| 52aed1a5b6 | |||
| 2e736a7306 | |||
| 217f40e6ec | |||
| 3353ff3dd0 | |||
| 9e45652356 | |||
| d83e859d84 | |||
| 4195dfe755 | |||
| 55aa48b544 | |||
| 5d3c846ed7 | |||
| 17c82175c2 | |||
| ddeea33a02 | |||
| f4d8224164 | |||
| aa5e5b8394 | |||
| f914263d0e | |||
| d3634439af | |||
| b509908b2a | |||
| 6490a7a8c3 | |||
| 162764e508 | |||
| 5d99a54c58 | |||
| 7252b83ef2 | |||
| 98b6269f11 | |||
| b8ee219a5f | |||
| 01e8c561bb | |||
| 71a0646065 | |||
| c8f5a1ccb6 | |||
| 2d1c697402 | |||
| c96a94c164 | |||
| 41d346853e | |||
| ad919639c7 | |||
| 5ebcc6d6b5 | |||
| 27c084707e | |||
| eeae8f3c1d | |||
| 36f86eb607 | |||
| e69460acfe | |||
| f7be4f6ae2 | |||
| d7975ec9f2 | |||
| 0405f813a2 | |||
| 8492334353 | |||
| 20e5e6db03 | |||
| ca442e748d | |||
| 4e6e3c9ad3 | |||
| eeee427c97 | |||
| 2bc714f05f | |||
| a07aa08fd4 | |||
| 00ca93e14d | |||
| a5e641185b | |||
| ba7433dade | |||
| 99f4263417 | |||
| a5c16f9d0b | |||
| 2d82a2a904 | |||
| 286366333f | |||
| a90abe74ae | |||
| 9e72e3aaeb | |||
| 0af4b33056 | |||
| 62fc9cc6ea | |||
| 2b54f90b97 | |||
| cfd1724cb0 | |||
| f6d9feda07 | |||
| be44b0c39a | |||
| 5e3f4acd78 | |||
| fd4557a8e6 | |||
| 28f4205bcd | |||
| 7b70f5c362 | |||
| 7f25099acd | |||
| fd84d8d893 | |||
| 49bee20f8e | |||
| 49cbb27222 | |||
| 90b4c7b1b6 | |||
| aa57b8223c | |||
| db93a6ddc8 | |||
| 41457846e2 | |||
| 08f1b8e3d9 | |||
| dc5460c120 | |||
| 6a6cb929e1 | |||
| fb56d6b64b | |||
| 2ed0111866 | |||
| ccc615cea6 | |||
| 97be632ed8 | |||
| 588d9b6528 | |||
| 4d791e96e3 | |||
| deaa85ad09 | |||
| 415a937a7e | |||
| e15906aeb8 | |||
| 853adebbcd | |||
| 0c39747462 | |||
| 7d9e48a8a6 | |||
| e75f76a1e6 | |||
| e5ac86f029 | |||
| 1ef8b48d2a | |||
| b19f2e06a8 | |||
| 8912749a75 | |||
| 033719036f | |||
| 4bde825ba0 | |||
| ad5aa9912b | |||
| 43c6e8b72b | |||
| 650c9154b5 | |||
| deeec06e03 | |||
| 2737e60305 | |||
| 662e2f746b | |||
| 43488a1145 | |||
| 6d0bd6a66d | |||
| 36d57c9419 | |||
| 43c75d6d76 | |||
| d146c50cf0 | |||
| bd6d36fa31 | |||
| 27a777a3e7 | |||
| fdedacaa51 | |||
| 1ecf1d7027 | |||
| 38707d4f67 | |||
| b5b3c61be0 | |||
| 64f702b826 | |||
| fdef4d29bd | |||
| e6a27ba94a | |||
| ad72ce7347 | |||
| 6b0559b13b | |||
| 93bb589eb7 | |||
| f923bd7fe0 | |||
| af1ed4425c |
+7
-1
@@ -75,13 +75,19 @@ tests, add UI to at least one test application so that the new
|
||||
features can be reviewed and tested. Consider also whether to add new
|
||||
code to other existing demo apps to exercise your feature.
|
||||
|
||||
When adding UI driven components, make sure that they are accessible.
|
||||
Follow the steps outlined in the [Best Practices](../../wiki/best-practices)
|
||||
section under Accessibility. Before submitting the pull request, you should
|
||||
audit your components with Voice Over (or other relevant assistive technologies)
|
||||
enabled.
|
||||
|
||||
Keep changes that fix different issues separate. For bug fixes,
|
||||
separate bugs should be submitted as separate pull requests. A good
|
||||
way to do this is to create a new branch in your fork for each new
|
||||
bug work on.
|
||||
|
||||
Any new user-visible strings should be included in the English
|
||||
`Localizable.strings` table so that they can be picked up and
|
||||
`ResearchKit.strings` table so that they can be picked up and
|
||||
localized in the next release cycle.
|
||||
|
||||
|
||||
|
||||
@@ -1,51 +1,63 @@
|
||||
ResearchKit Framework
|
||||
===========
|
||||
|
||||
The ResearchKit™ framework is an open source software framework that makes it easy to
|
||||
create apps for medical research or for other research projects.
|
||||
The *ResearchKit™ framework* is an open source software framework that makes it easy to create apps
|
||||
for medical research or for other research projects.
|
||||
|
||||
* Getting Started: [Getting Started](#gettingstarted)
|
||||
* Documentation: ([Programming Guide](http://researchkit.github.io/docs/docs/Overview/GuideOverview.html)) ([API](http://researchkit.github.io/docs/index.html))
|
||||
* Best practices: [Best Practices](../../wiki/best-practices)
|
||||
* Contributing to ResearchKit: [Contributing](CONTRIBUTING.md)
|
||||
* Website and blog: ([researchkit.org](http://researchkit.github.io/index.html)) ([Blog](http://researchkit.github.io/blog.html))
|
||||
* ResearchKit BSD License: [License](#license)
|
||||
* [Getting Started](#gettingstarted)
|
||||
* Documentation:
|
||||
* [Programming Guide](http://researchkit.org/docs/docs/Overview/GuideOverview.html)
|
||||
* [Framework Reference](http://researchkit.org/docs/index.html)
|
||||
* [Best Practices](../../wiki/best-practices)
|
||||
* [Contributing to ResearchKit](CONTRIBUTING.md)
|
||||
* [Website](http://researchkit.org) and [Blog](http://researchkit.org/blog.html)
|
||||
* [ResearchKit BSD License](#license)
|
||||
|
||||
Getting More Information
|
||||
========================
|
||||
|
||||
* Join [researchkit-users](https://lists.apple.com/mailman/listinfo/researchkit-users) for discussing uses of the ResearchKit framework and related projects.
|
||||
* Join [researchkit-dev](https://lists.apple.com/mailman/listinfo/researchkit-dev) for discussing ongoing work to improve and expand the framework.
|
||||
* Or [contact us](https://developer.apple.com/contact/researchkit/)
|
||||
* Join the [*ResearchKit* Forum](https://forums.developer.apple.com/community/researchkit) for discussing uses of the *ResearchKit framework and* related projects.
|
||||
|
||||
Use Cases
|
||||
===========
|
||||
|
||||
A task in the ResearchKit framework contains a set of steps to present to a
|
||||
user. Everything, whether it’s a survey, the consent process, or active tasks,
|
||||
is represented as a task that can be presented with a task view controller.
|
||||
A task in the *ResearchKit framework* contains a set of steps to present to a user. Everything,
|
||||
whether it’s a *survey*, the *consent process*, or *active tasks*, is represented as a task that can
|
||||
be presented with a task view controller.
|
||||
|
||||
Surveys
|
||||
-------
|
||||
|
||||
The ResearchKit framework provides a pre-built user interface for surveys, which can be
|
||||
presented modally on an iPhone, iPod Touch, or iPad. [Surveys](http://researchkit.github.io/docs/docs/Survey/CreatingSurveys.html)
|
||||
The *ResearchKit framework* provides a pre-built user interface for surveys, which can be presented
|
||||
modally on an *iPhone*, *iPod Touch*, or *iPad*. See
|
||||
*[Creating Surveys](http://researchkit.org/docs/docs/Survey/CreatingSurveys.html)* for more
|
||||
information.
|
||||
|
||||
|
||||
Consent
|
||||
----------------
|
||||
|
||||
The ResearchKit framework provides visual consent templates that you can customize to
|
||||
explain the details of your research study and obtain a signature if needed. [Consent](http://researchkit.github.io/docs/docs/InformedConsent/InformedConsent.html)
|
||||
The *ResearchKit framework* provides visual consent templates that you can customize to explain the
|
||||
details of your research study and obtain a signature if needed.
|
||||
See *[Obtaining Consent](http://researchkit.org/docs/docs/InformedConsent/InformedConsent.html)* for
|
||||
more information.
|
||||
|
||||
|
||||
Active Tasks
|
||||
------------
|
||||
|
||||
Some studies may need data beyond survey questions or the passive data collection
|
||||
capabilities available through use of the HealthKit and CoreMotion APIs if you are
|
||||
programming for iOS. ResearchKit's active tasks invite users to perform activities
|
||||
under semi-controlled conditions, while iPhone sensors actively collect data. [Active Tasks](http://researchkit.github.io/docs/docs/ActiveTasks/ActiveTasks.html)
|
||||
Some studies may need data beyond survey questions or the passive data collection capabilities
|
||||
available through use of the *HealthKit* and *CoreMotion* APIs if you are programming for *iOS*.
|
||||
*ResearchKit*'s active tasks invite users to perform activities under semi-controlled conditions,
|
||||
while *iPhone* sensors actively collect data. See
|
||||
*[Active Tasks](http://researchkit.org/docs/docs/ActiveTasks/ActiveTasks.html)* for more
|
||||
information.
|
||||
|
||||
Charts
|
||||
------------
|
||||
*ResearchKit* includes a *Charts module*. It features three chart types: a *pie chart* (`ORKPieChartView`), a *line graph chart* (`ORKLineGraphChartView`), and a *discrete graph chart* (`ORKDiscreteGraphChartView`).
|
||||
|
||||
The views in the *Charts module* can be used independently of the rest of *ResearchKit*. They don't automatically connect with any other part of *ResearchKit*: the developer has to supply the data to be displayed through the views' `dataSources`, which allows for maximum flexibility.
|
||||
|
||||
|
||||
Getting Started<a name="gettingstarted"></a>
|
||||
@@ -55,15 +67,15 @@ Getting Started<a name="gettingstarted"></a>
|
||||
Requirements
|
||||
------------
|
||||
|
||||
The primary ResearchKit framework codebase supports iOS and requires Xcode 7.0
|
||||
or newer. The ResearchKit framework has a Base SDK version of 8.0, meaning that apps
|
||||
using the ResearchKit framework can run on devices with iOS 8.0 or newer.
|
||||
The primary *ResearchKit framework* codebase supports *iOS* and requires *Xcode 8.0* or newer. The
|
||||
*ResearchKit framework* has a *Base SDK* version of *8.0*, meaning that apps using the *ResearchKit
|
||||
framework* can run on devices with *iOS 8.0* or newer.
|
||||
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
The latest stable version of ResearchKit framework can be cloned with
|
||||
The latest stable version of *ResearchKit framework* can be cloned with
|
||||
|
||||
```
|
||||
git clone -b stable https://github.com/ResearchKit/ResearchKit.git
|
||||
@@ -78,53 +90,54 @@ git clone https://github.com/ResearchKit/ResearchKit.git
|
||||
Building
|
||||
--------
|
||||
|
||||
Build the ResearchKit framework by opening `ResearchKit.xcodeproj` and running the
|
||||
`ResearchKit` framework target. Optionally, run the unit tests too.
|
||||
Build the *ResearchKit framework* by opening `ResearchKit.xcodeproj` and running the `ResearchKit`
|
||||
framework target. Optionally, run the unit tests too.
|
||||
|
||||
|
||||
Adding the ResearchKit framework to your App
|
||||
------------------------------
|
||||
|
||||
This walk-through shows how to embed the ResearchKit framework in your app as a
|
||||
dynamic framework, and present a simple task view controller.
|
||||
This walk-through shows how to embed the *ResearchKit framework* in your app as a dynamic framework,
|
||||
and present a simple task view controller.
|
||||
|
||||
###1. Add the ResearchKit framework to Your Project
|
||||
### 1. Add the ResearchKit framework to Your Project
|
||||
|
||||
To get started, drag `ResearchKit.xcodeproj` from your checkout into
|
||||
your iOS app project in Xcode:
|
||||
To get started, drag `ResearchKit.xcodeproj` from your checkout into your *iOS* app project
|
||||
in *Xcode*:
|
||||
|
||||
<center>
|
||||
<figure>
|
||||
<img src="../../wiki/AddingResearchKitXcode.png" alt="Adding the ResearchKit framework to your project" align="middle"/>
|
||||
<img src="../../wiki/AddingResearchKitXcode.png" alt="Adding the ResearchKit framework to your
|
||||
project" align="middle"/>
|
||||
</figure>
|
||||
</center>
|
||||
|
||||
Then, embed the ResearchKit framework as a dynamic framework in your app, by adding
|
||||
it to the Embedded Binaries section of the General pane for your
|
||||
target as shown in the figure below.
|
||||
Then, embed the *ResearchKit framework* as a dynamic framework in your app, by adding it to the
|
||||
*Embedded Binaries* section of the *General* pane for your target as shown in the figure below.
|
||||
|
||||
<center>
|
||||
<figure>
|
||||
<img src="../../wiki/AddedBinaries.png" width="100%" alt="Adding the ResearchKit framework to Embedded Binaries" align="middle"/>
|
||||
<img src="../../wiki/AddedBinaries.png" width="100%" alt="Adding the ResearchKit framework to
|
||||
Embedded Binaries" align="middle"/>
|
||||
<figcaption><center>Adding the ResearchKit framework to Embedded Binaries</center></figcaption>
|
||||
</figure>
|
||||
</center>
|
||||
|
||||
Note: You can also import ResearchKit into your project using a [dependency manager](./docs-standalone/dependency-management.md) such as CocoaPods or Carthage.
|
||||
Note: You can also import *ResearchKit* into your project using a
|
||||
[dependency manager](./docs-standalone/dependency-management.md) such as *CocoaPods* or *Carthage*.
|
||||
|
||||
###2. Create a Step
|
||||
### 2. Create a Step
|
||||
|
||||
In this walk-through, we will use the ResearchKit framework to modally present a
|
||||
simple single-step task showing a single instruction.
|
||||
In this walk-through, we will use the *ResearchKit framework* to modally present a simple
|
||||
single-step task showing a single instruction.
|
||||
|
||||
Create a step for your task by adding some code, perhaps in
|
||||
`viewDidAppear:` of an existing view controller. To keep things
|
||||
simple, we'll use an instruction step (`ORKInstructionStep`) and name
|
||||
Create a step for your task by adding some code, perhaps in `viewDidAppear:` of an existing view
|
||||
controller. To keep things simple, we'll use an instruction step (`ORKInstructionStep`) and name
|
||||
the step `myStep`.
|
||||
|
||||
*Objective-C*
|
||||
|
||||
```objc
|
||||
```objc
|
||||
ORKInstructionStep *myStep =
|
||||
[[ORKInstructionStep alloc] initWithIdentifier:@"intro"];
|
||||
myStep.title = @"Welcome to ResearchKit";
|
||||
@@ -137,12 +150,11 @@ let myStep = ORKInstructionStep(identifier: "intro")
|
||||
myStep.title = "Welcome to ResearchKit"
|
||||
```
|
||||
|
||||
###3. Create a Task
|
||||
### 3. Create a Task
|
||||
|
||||
Use the ordered task class (`ORKOrderedTask`) to create a task that
|
||||
contains `myStep`. An ordered task is just a task where the order and
|
||||
selection of later steps does not depend on the results of earlier
|
||||
ones. Name your task `task` and initialize it with `myStep`.
|
||||
Use the ordered task class (`ORKOrderedTask`) to create a task that contains `myStep`. An ordered
|
||||
task is just a task where the order and selection of later steps does not depend on the results of
|
||||
earlier ones. Name your task `task` and initialize it with `myStep`.
|
||||
|
||||
*Objective-C*
|
||||
|
||||
@@ -157,11 +169,10 @@ ORKOrderedTask *task =
|
||||
let task = ORKOrderedTask(identifier: "task", steps: [myStep])
|
||||
```
|
||||
|
||||
###4. Present the Task
|
||||
### 4. Present the Task
|
||||
|
||||
Create a task view controller (`ORKTaskViewController`) and initialize
|
||||
it with your `task`. A task view controller manages a task and collects the
|
||||
results of each step. In this case, your task view
|
||||
Create a task view controller (`ORKTaskViewController`) and initialize it with your `task`. A task
|
||||
view controller manages a task and collects the results of each step. In this case, your task view
|
||||
controller simply displays your instruction step.
|
||||
|
||||
*Objective-C*
|
||||
@@ -181,9 +192,9 @@ taskViewController.delegate = self
|
||||
presentViewController(taskViewController, animated: true, completion: nil)
|
||||
```
|
||||
|
||||
The above snippet assumes that your class implements the
|
||||
`ORKTaskViewControllerDelegate` protocol. This has just one required method,
|
||||
which you must implement in order to handle the completion of the task:
|
||||
The above snippet assumes that your class implements the `ORKTaskViewControllerDelegate` protocol.
|
||||
This has just one required method, which you must implement in order to handle the completion of
|
||||
the task:
|
||||
|
||||
*Objective-C*
|
||||
|
||||
@@ -203,20 +214,19 @@ which you must implement in order to handle the completion of the task:
|
||||
*Swift*
|
||||
|
||||
```swift
|
||||
func taskViewController(taskViewController: ORKTaskViewController,
|
||||
didFinishWithReason reason: ORKTaskViewControllerFinishReason,
|
||||
error: NSError?) {
|
||||
let taskResult = taskViewController.result
|
||||
// You could do something with the result here.
|
||||
func taskViewController(_ taskViewController: ORKTaskViewController,
|
||||
didFinishWith reason: ORKTaskViewControllerFinishReason,
|
||||
error: Error?) {
|
||||
let taskResult = taskViewController.result
|
||||
// You could do something with the result here.
|
||||
|
||||
// Then, dismiss the task view controller.
|
||||
dismissViewControllerAnimated(true, completion: nil)
|
||||
// Then, dismiss the task view controller.
|
||||
dismiss(true, completion: nil)
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
If you now run your app, you should see your first ResearchKit framework
|
||||
instruction step:
|
||||
|
||||
If you now run your app, you should see your first *ResearchKit framework* instruction step:
|
||||
|
||||
<center>
|
||||
<figure>
|
||||
@@ -229,38 +239,37 @@ instruction step:
|
||||
What else can the ResearchKit framework do?
|
||||
-----------------------------
|
||||
|
||||
The ResearchKit [`ORKCatalog`](samples/ORKCatalog) sample app is a
|
||||
good place to start. Find the project in ResearchKit's
|
||||
[`samples`](samples) directory. This project includes a list of all
|
||||
the types of steps supported by the ResearchKit framework in one tab, and displays a
|
||||
browser for the results of the last completed task in the other tab.
|
||||
The *ResearchKit* [`ORKCatalog`](samples/ORKCatalog) sample app is a good place to start. Find the
|
||||
project in ResearchKit's [`samples`](samples) directory. This project includes a list of all the
|
||||
types of steps supported by the *ResearchKit framework* in the first tab, and displays a browser for the
|
||||
results of the last completed task in the second tab. The third tab shows some examples from the *Charts module*.
|
||||
|
||||
|
||||
|
||||
License<a name="license"></a>
|
||||
=======
|
||||
|
||||
The source in the ResearchKit repository is made available under the
|
||||
following license unless another license is explicitly identified:
|
||||
The source in the *ResearchKit* repository is made available under the following license unless
|
||||
another license is explicitly identified:
|
||||
|
||||
```
|
||||
Copyright (c) 2015, Apple Inc. All rights reserved.
|
||||
|
||||
Copyright (c) 2015 - 2017, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
||||
@@ -0,0 +1,346 @@
|
||||
# ResearchKit Release Notes
|
||||
|
||||
## ResearchKit 1.5 Release Notes
|
||||
|
||||
*ResearchKit 1.5* supports *iOS* and requires *Xcode 8.0* or newer. The minimum supported *Base SDK* is *8.0*.
|
||||
|
||||
In addition to general stabiltiy and performance improvements, *ResearchKit 1.5* includes the following new features and enhancements.
|
||||
|
||||
- **New Active Tasks**
|
||||
|
||||
- **Stroop Test**
|
||||
|
||||
*Contributed by [Apple Inc](https://github.com/researchkit).*
|
||||
|
||||
The *Stroop Test* shows the participant different combinations of text and tint colors on the screen.
|
||||
|
||||
Users must ignore the text and instead select the button that reflects the first letter of the tint color.
|
||||
|
||||
- **Trail Making Test**
|
||||
|
||||
*Contributed by Faraz Hussain.*
|
||||
|
||||
The *Trail Making Test* instructs participants to connect a series of labelled circles and the time to complete the test is recorded.
|
||||
|
||||
- **Range of Motion Test**
|
||||
|
||||
*Contributed by Daren Levy, Dr. Raj Karia, John Guydo.*
|
||||
|
||||
Participants are instructed to follow a series of steps while accelerometer and gyroscope data is captured to measure flexed and extended positions for both the shoulder and knee.
|
||||
|
||||
- **Touch Anywhere Active Task**
|
||||
|
||||
*Contributed by Daren Levy, Dr. Raj Karia, John Guydo*
|
||||
|
||||
Allows the user to get their device in the proper position and then tap the screen to indicate they are ready to begin the next step.
|
||||
|
||||
- **New Steps**
|
||||
|
||||
- **Video Instruction Step**
|
||||
|
||||
*Contributed by [Oliver Schäfer](https://github.com/oliverschaefer).*
|
||||
|
||||
The *Video Instruction Step* provides a step to be used to display a video.
|
||||
|
||||
This step can be used to display videos to users from either a local or remote source.
|
||||
|
||||
- **Other Improvements**
|
||||
|
||||
- **Tone Audiometry Test**
|
||||
|
||||
*Contributed by [Apple Inc](https://github.com/researchkit).*
|
||||
|
||||
Updated to include both a left and right button.
|
||||
|
||||
- **Digital Object Identifier**
|
||||
|
||||
*Contributed by [Apple Inc](https://github.com/researchkit).*
|
||||
|
||||
Assigns a Digital Object Identifier to the ResearchKit repository on GitHub to use when referencing the framework.
|
||||
|
||||
|
||||
## ResearchKit 1.4 Release Notes
|
||||
|
||||
*ResearchKit 1.4* supports *iOS* and requires *Xcode 8.0* or newer. The minimum supported *Base SDK* is *8.0*.
|
||||
|
||||
In addition to general stabiltiy and performance improvements, *ResearchKit 1.4* includes the following new features and enhancements.
|
||||
|
||||
- **New Active Task**
|
||||
|
||||
- **Hand Tremor Task**
|
||||
|
||||
*Contributed by [Shannon Young](https://github.com/syoung-smallwisdom).*
|
||||
|
||||
The *Hand Tremor Task* asks the participant to hold the device with their most affected hand in various positions while accelerometer and motion data is captured.
|
||||
|
||||
- **Walk Back and Forth Task**
|
||||
|
||||
*Contributed by [Shannon Young](https://github.com/syoung-smallwisdom).*
|
||||
|
||||
The *Walk Back and Forth Task* addresses the concern of researchers/participants who have difficulty locating an unobstructed path for 20 steps.
|
||||
|
||||
Instructs users to walk and turn in a full circle, allowing the tests to be conducted in a smaller space.
|
||||
|
||||
- **New Steps**
|
||||
|
||||
- **Video Capture Step**
|
||||
|
||||
*Contributed by [Apple Inc](https://github.com/researchkit).*
|
||||
|
||||
The *Video Capture Step* provides a step to be used to record video.
|
||||
|
||||
The step can be used as part of a survey to capture video respones as well.
|
||||
|
||||
- **Review Step**
|
||||
|
||||
*Contributed by [Oliver Schäfer](https://github.com/oliverschaefer).*
|
||||
|
||||
The *Review Step* allows a participant to review and modify their answers to a survey.
|
||||
|
||||
The step can be used in the middle of a survey, at the end of a survey, or a standalone module.
|
||||
|
||||
- **Signature Step**
|
||||
|
||||
*Contributed by [Oliver Schäfer](https://github.com/oliverschaefer).*
|
||||
|
||||
The *Signature Step* provides an interface for a participant to sign their name.
|
||||
|
||||
The step can be used for handwriting detection or simply to sign a document.
|
||||
|
||||
- **Table Step**
|
||||
|
||||
*Contributed by [Shannon Young](https://github.com/syoung-smallwisdom).*
|
||||
|
||||
The *Table Step* provides a way to neatly display data in a table.
|
||||
|
||||
- **Other Improvements**
|
||||
|
||||
- **Data Collection Module**
|
||||
|
||||
*Contributed by [Apple Inc](https://github.com/researchkit).*
|
||||
|
||||
The *Data Collection Module* makes it even easier to aggregate data from HealthKit and device sensors.
|
||||
|
||||
- **Tapping Test**
|
||||
|
||||
*Contributed by [Michał Zaborowski](https://github.com/m1entus).*
|
||||
|
||||
The *Tapping Test* is updated to include tap duration as part of the result.
|
||||
|
||||
|
||||
## ResearchKit 1.3 Release Notes
|
||||
|
||||
*ResearchKit 1.3* supports *iOS* and requires *Xcode 7.2* or newer. The minimum supported *Base SDK* is *8.0*.
|
||||
|
||||
In addition to general stability and performance improvements, *ResearchKit 1.3* includes the following new features and enhancements.
|
||||
|
||||
- **New Active Task**
|
||||
|
||||
- **9-Hole Peg Test**
|
||||
|
||||
*Contributed by [Julien Therier](https://github.com/julientherier).*
|
||||
|
||||
The *9-Hole Peg Test task* is used to test upper extremity functionality.
|
||||
|
||||
The test involves putting a variable number of pegs in a hole and subsequently removing them.
|
||||
|
||||
The test is documented in the scientific literature to measure the *[MSFC score in Multiple Sclerosis](http://www.nationalmssociety.org/For-Professionals/Researchers/Resources-for-Researchers/Clinical-Study-Measures/9-Hole-Peg-Test-(9-HPT))* or *[Parkinson's Disease](http://www.ncbi.nlm.nih.gov/pubmed/22020457)*.
|
||||
|
||||
- **Sample App**
|
||||
|
||||
*Contributed by [Apple Inc](https://github.com/researchkit).*
|
||||
|
||||
The *Sample App* (`ORKSample` project on *ResearchKit*'s workspace) serves as a template application that combines different modules from the *ResearchKit framework*.
|
||||
|
||||
- **Account Module**
|
||||
|
||||
*Contributed by [Apple Inc](https://github.com/researchkit).*
|
||||
|
||||
The *Account Module* provides steps to facilitate account creation and login.
|
||||
|
||||
The module includes the following steps:
|
||||
|
||||
1. *Registration*, used to allow the participant to create a new account.
|
||||
2. *Verification*, used to confirm if the participant has verified the provided email address.
|
||||
3. *Login*, used to allow registered users to login.
|
||||
|
||||
- **Passcode with Touch ID**
|
||||
|
||||
*Contributed by [Apple Inc](https://github.com/researchkit).*
|
||||
|
||||
The *Passcode with Touch ID module* provides the ability to secure any *ResearchKit* application with a numeric passcode.
|
||||
|
||||
This module includes a *Keychain Wrapper* that stores the passcode on the device, as well as the option to use *Touch ID* on compatible devices. The passcode module supports 4-digit and 6-digit numeric codes.
|
||||
|
||||
The passcode module provides the following components:
|
||||
|
||||
1. *Passcode creation step*, which can be used as part of onboarding to create a passcode and store it in the keychain.
|
||||
2. *Passcode authentication view controller*, which can be modally presented when appropriate.
|
||||
3. *Passcode modification view controller*, which allows the participant to change their passcode.
|
||||
|
||||
- **Other Improvements**
|
||||
|
||||
- **Optional Form Items**
|
||||
|
||||
*Contributed by [Ricardo Sánchez-Sáez](https://github.com/rsanchezsaez).*
|
||||
|
||||
Adds the `optional` property to `ORKFormItem`.
|
||||
|
||||
The *Continue/Done* button of form steps is enabled when all of the following conditions are met:
|
||||
|
||||
- At least one form item has an answer.
|
||||
- All the non-optional form items have answers.
|
||||
- All answered form items have valid answers.
|
||||
|
||||
- **Location Question**
|
||||
|
||||
*Contributed by [Quintiles](https://github.com/QuintilesRK).*
|
||||
|
||||
A *Location Question* can be used to request details about the participant's current location or about a specific address.
|
||||
|
||||
The question uses *MapKit* to provide a visual representation for the specified address.
|
||||
|
||||
- **Wait Step**
|
||||
|
||||
*Contributed by [Quintiles](https://github.com/QuintilesRK).*
|
||||
|
||||
The *Wait Step* provides a step to be used in-between steps when additional data processing is required.
|
||||
|
||||
The step supports both indeterminate and determinate progress views, as well as the ability to show text status updates.
|
||||
|
||||
- **Validated Text Answer Format**
|
||||
|
||||
*Contributed by [Quintiles](https://github.com/QuintilesRK).*
|
||||
|
||||
The *Validated Text Answer Format* enhances the existing *Text Answer Format* by providing input validation using a regular expression.
|
||||
|
||||
A valid *NSRegularExpression* object and an *error message* string are required to properly use this answer format.
|
||||
|
||||
|
||||
## ResearchKit 1.2 Release Notes
|
||||
|
||||
*ResearchKit 1.2* supports *iOS* and requires *Xcode 7.0* or newer. The minimum supported *Base SDK* is *8.0*.
|
||||
|
||||
In addition to general stability and performance improvements, *ResearchKit 1.2* includes the following new features and enhancements.
|
||||
|
||||
- **New Active Tasks**
|
||||
|
||||
- **Tower of Hanoi Task**
|
||||
|
||||
*Contributed by [coxy1989](https://github.com/coxy1989).*
|
||||
|
||||
The *[Tower of Hanoi](https://en.wikipedia.org/wiki/Tower_of_Hanoi#Applications) task* is frequently used in psychological research on problem solving.
|
||||
|
||||
It is a mathematical puzzle consisting of three rods and a number of disks of different sizes which can slide onto any rod. The puzzle starts with the disks in a stack in ascending order of size on one rod (the smallest at the top).
|
||||
|
||||
The objective of the puzzle is to move the entire stack to another rod, obeying the following rules:
|
||||
|
||||
1. Only one disk can be moved at a time.
|
||||
2. Each move consists of taking the upper disk from one of the stacks and placing it on top of another stack.
|
||||
3. No disk may be placed on top of a smaller disk.
|
||||
|
||||
- **Paced Serial Addition Test Task**
|
||||
|
||||
*Contributed by [Julien Therier](https://github.com/julientherier).*
|
||||
|
||||
The *Paced Serial Addition Test task* provides adaptations of both the *Paced Auditory Serial Addition Test (PASAT)* and the *Paced Visual Serial Addition Test (PVSAT)*.
|
||||
|
||||
The *[PASAT](https://en.wikipedia.org/wiki/Paced_Auditory_Serial_Addition_Test)* is a neuropsychological test used to assess capacity and rate of information processing and sustained and divided attention.
|
||||
|
||||
Both tests are documented in the scientific literature ([Fos et al., 2000](http://www.ncbi.nlm.nih.gov/pubmed/11125707); [Nagels et al., 2005](http://www.ncbi.nlm.nih.gov/pubmed/15823678)) as a measure of the [*Multiple Sclerosis Functional Score*](http://www.nationalmssociety.org/For-Professionals/Researchers/Resources-for-Researchers/Clinical-Study-Measures/Multiple-Sclerosis-Functional-Composite-%28MSFC%29).
|
||||
|
||||
This task generates a series of single digits (for example, 60 of them), at the specific frequency (for example, one new digit every 2 or 3 seconds). The user must add the newly presented digit to the one prior to it.
|
||||
|
||||
- **Timed Walk Task**
|
||||
|
||||
*Contributed by [Julien Therier](https://github.com/julientherier).*
|
||||
|
||||
The *Timed Walk task* measures gait speed and is an adaptation of the [*Timed 25-Foot Walk*](http://www.nationalmssociety.org/For-Professionals/Researchers/Resources-for-Researchers/Clinical-Study-Measures/Timed-25-Foot-Walk-%28T25-FW%29) in the context of *multiple sclerosis*.
|
||||
|
||||
Gait speed has been demonstrated to be a useful and reliable functional measure of walking ability. When administering the *Timed Walk Task*, patients are allowed to use assistive devices (canes, crutches, walkers).
|
||||
|
||||
- **Charts Module**
|
||||
|
||||
*Contributed by [coxy1989](https://github.com/coxy1989) and [Ricardo Sánchez-Sáez](https://github.com/rsanchezsaez).*
|
||||
|
||||
A *Charts module* has been implemented. It features three chart types: a *pie chart* (`ORKPieChartView`), a *line graph chart* (`ORKLineGraphChartView`), and a *discrete graph chart* (`ORKDiscreteGraphChartView`).
|
||||
|
||||
The views in the *Charts module* can be used independently of the rest of *ResearchKit*. It doesn't automatically connect with any other *ResearchKit* module: the developer has to supply the data to be displayed through the views' `dataSources`, which allows for maximum flexibility.
|
||||
|
||||
- **Other Improvements**
|
||||
|
||||
- **Scale Answer Format**
|
||||
|
||||
*Contributed by [Apple Inc](https://github.com/researchkit).*
|
||||
|
||||
*Discrete scales* now support *text choice* labels, and all *scales* support images in place of the minimum and maximum range labels.
|
||||
|
||||
- **Result Predicates**
|
||||
|
||||
*Contributed by [Ricardo Sánchez-Sáez](https://github.com/rsanchezsaez).*
|
||||
|
||||
The predicate-building methods in `ORKResultPredicate` now use the new `ORKResultSelector` class for unequivocally identifying a *question step result* or a *form item result*.
|
||||
|
||||
This eliminates ambiguity when matching results with the same inner scope identifier. For example, a *form item result* can have the same identifier as a *question step result* or as another *form item result* in a different *form step*, and you can now match them separately.
|
||||
|
||||
|
||||
## ResearchKit 1.1 Release Notes
|
||||
|
||||
*ResearchKit 1.1* supports *iOS* and requires *Xcode 6.3* or newer. The minimum supported *Base SDK* is *8.0*.
|
||||
|
||||
In addition to general stability and performance improvements, *ResearchKit 1.1* includes the following new features and enhancements.
|
||||
|
||||
- **Navigable Ordered Task**
|
||||
|
||||
*Contributed by [Ricardo Sánchez-Sáez](https://github.com/rsanchezsaez).*
|
||||
|
||||
A new type of *conditional ordered task* (`ORKNavigableOrderedTask`) has been implemented.
|
||||
|
||||
The developer can use the `ORKStepNavigationRule` subclasses to dynamically navigate between the task steps:
|
||||
- `ORKPredicateStepNavigationRule` allows to make conditional jumps by matching previous results (either those of the ongoing task, or those of any previously stored task result tree). You typically use the class methods in the `ORKResultPredicate` class to match answers in the most commonly used result types.
|
||||
- `ORKDirectStepNavigationRule` provides support for unconditional jumps.
|
||||
|
||||
- **New Active Tasks**
|
||||
- **Reaction Time Task**
|
||||
|
||||
*Contributed by [coxy1989](https://github.com/coxy1989).*
|
||||
|
||||
The *Reaction Time Task* is an adaptation of the [*Simple Reaction Time test (SRT)*](http://www.cambridgecognition.com/tests/simple-reaction-time-srt). *SRT* measures reaction time through delivery of a known stimulus to a known location to elicit a known response.
|
||||
|
||||
This test is deployed in a range of research questions across fields including medicine, sports science and psychology.
|
||||
|
||||
Although it classically involves pressing the space bar or clicking a mouse in response to an event on screen, the *ResearchKit* implementation relies on the study participant shaking the device when she sees a blue circle on the screen, which we think is more correlatable to a true stimulus reaction test.
|
||||
|
||||
- **Tone Audiometry Task**
|
||||
|
||||
*Contributed by [Vincent Tourraine](https://github.com/vtourraine).*
|
||||
|
||||
The *Tone Audiometry Task* is an adaptation of the [*Pure Tone Audiometry test (PTA)*](https://en.wikipedia.org/wiki/Pure_tone_audiometry). *PTA* is a key hearing test used to identify hearing threshold levels of an individual, enabling determination of the degree, type and configuration of a hearing loss.
|
||||
|
||||
The *ResearchKit* implementation generates a series of pure sinusoid sounds, with different frequencies and on different channels (left or right). The test starts at the minimum volume and is gradually increased until the participant perceives it and taps a button. At that time, the current sound amplitude, frequency and channel are recorded.
|
||||
|
||||
- **Scale Answer Format Enhancements**
|
||||
|
||||
*Contributed by [Ricardo Sánchez-Sáez](https://github.com/rsanchezsaez) and [Bruce Duncan](https://github.com/brucehappy).*
|
||||
|
||||
Support for discrete and continuous *vertical scales* has been added. Some questions, like mood measurement or symptom severity measurement queries may be more naturally presented using a *vertical scale*.
|
||||
|
||||
The *Scale Answer Format* has also been improved by making it usable within forms.
|
||||
|
||||
- **Image Capture Step**
|
||||
|
||||
*Contributed by [Bruce Duncan](https://github.com/brucehappy).*
|
||||
|
||||
An *Image Capture Step* has been added. The researcher can ask the participant to take pictures of relevant body parts. The researcher can provide a body part image template to facilitate the scale and orientation of the taken pictures.
|
||||
|
||||
- **iPad Support**
|
||||
|
||||
*Contributed by [Ricardo Sánchez-Sáez](https://github.com/rsanchezsaez) and [Apple Inc](https://github.com/researchkit).*
|
||||
|
||||
*iPad support* for all orientations has been implemented.
|
||||
|
||||
- **iPhone Landscape Support**
|
||||
|
||||
*Contributed by [Apple Inc.](https://github.com/researchkit) and [Ricardo Sánchez-Sáez](https://github.com/rsanchezsaez).*
|
||||
|
||||
*iPhone landscape orientation support* has been implemented.
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "group:ResearchKit.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Testing/ORKTest/ORKTest.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:samples/ORKCatalog/ORKCatalog.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:samples/ORKSample/ORKSample.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'ResearchKit'
|
||||
s.version = '1.2.1'
|
||||
s.version = '1.5.2'
|
||||
s.summary = 'ResearchKit is an open source software framework that makes it easy to create apps for medical research or for other research projects.'
|
||||
s.homepage = 'https://www.github.com/ResearchKit/ResearchKit'
|
||||
s.documentation_url = 'http://researchkit.github.io/docs/'
|
||||
@@ -8,8 +8,8 @@ Pod::Spec.new do |s|
|
||||
s.author = { 'researchkit.org' => 'http://researchkit.org' }
|
||||
s.source = { :git => 'https://github.com/ResearchKit/ResearchKit.git', :tag => s.version.to_s }
|
||||
s.public_header_files = `./scripts/find_headers.rb --public --private`.split("\n")
|
||||
s.source_files = 'ResearchKit/**/*.{h,m}'
|
||||
s.source_files = 'ResearchKit/**/*.{h,m,swift}'
|
||||
s.resources = 'ResearchKit/**/*.{fsh,vsh}', 'ResearchKit/Animations/**/*.m4v', 'ResearchKit/Artwork.xcassets', 'ResearchKit/Localized/*.lproj'
|
||||
s.platform = :ios, '8.0'
|
||||
s.platform = :ios, '8.2'
|
||||
s.requires_arc = true
|
||||
end
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0700"
|
||||
LastUpgradeVersion = "0900"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0700"
|
||||
LastUpgradeVersion = "0900"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
||||
@@ -30,6 +30,6 @@
|
||||
|
||||
|
||||
// Shared header for accessibility functionality.
|
||||
#import "UIView+ORKAccessibility.h"
|
||||
#import "ORKAccessibilityFunctions.h"
|
||||
#import "ORKLineGraphAccessibilityElement.h"
|
||||
#import "UIView+ORKAccessibility.h"
|
||||
|
||||
@@ -30,9 +30,11 @@
|
||||
|
||||
|
||||
#import "ORKDefines.h"
|
||||
#import "ORKHelpers.h"
|
||||
#import "ORKHelpers_Internal.h"
|
||||
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class ORKScaleSlider;
|
||||
|
||||
// Used to properly format values from the ORKScaleSlider.
|
||||
@@ -43,7 +45,7 @@ ORK_EXTERN NSString *ORKAccessibilityFormatContinuousScaleSliderValue(CGFloat va
|
||||
ORK_EXTERN void ORKAccessibilityPerformBlockAfterDelay(NSTimeInterval delay, void(^block)(void));
|
||||
|
||||
// Convenience for posting an accessibility notification after a delay.
|
||||
ORK_INLINE void ORKAccessibilityPostNotificationAfterDelay(UIAccessibilityNotifications notification, id argument, NSTimeInterval delay) {
|
||||
ORK_INLINE void ORKAccessibilityPostNotificationAfterDelay(UIAccessibilityNotifications notification, _Nullable id argument, NSTimeInterval delay) {
|
||||
ORKAccessibilityPerformBlockAfterDelay(delay, ^{
|
||||
UIAccessibilityPostNotification(notification, argument);
|
||||
});
|
||||
@@ -52,3 +54,5 @@ ORK_INLINE void ORKAccessibilityPostNotificationAfterDelay(UIAccessibilityNotifi
|
||||
// Creates a string suitable for Voice Over by joining the variables with ", " and avoiding nil and empty strings.
|
||||
#define ORKAccessibilityStringForVariables(...) _ORKAccessibilityStringForVariables(ORK_NARG(__VA_ARGS__), ##__VA_ARGS__)
|
||||
ORK_EXTERN NSString *_ORKAccessibilityStringForVariables(NSInteger numParameters, NSString *baseString, ...);
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
@@ -29,11 +29,14 @@
|
||||
*/
|
||||
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "ORKAccessibilityFunctions.h"
|
||||
#import "ORKAnswerFormat_Internal.h"
|
||||
@import UIKit;
|
||||
|
||||
#import "ORKScaleSlider.h"
|
||||
#import "ORKScaleSliderView.h"
|
||||
|
||||
#import "ORKAnswerFormat_Internal.h"
|
||||
|
||||
#import "ORKAccessibilityFunctions.h"
|
||||
#import "UIView+ORKAccessibility.h"
|
||||
|
||||
|
||||
|
||||
@@ -28,10 +28,16 @@
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@import UIKit;
|
||||
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface ORKLineGraphAccessibilityElement : UIAccessibilityElement
|
||||
|
||||
- (nonnull instancetype)initWithAccessibilityContainer:(nonnull UIView *)container index:(NSInteger)index maxIndex:(NSInteger)maxIndex;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
@@ -30,11 +30,15 @@
|
||||
|
||||
#import "ORKLineGraphAccessibilityElement.h"
|
||||
|
||||
|
||||
@interface ORKLineGraphAccessibilityElement()
|
||||
|
||||
@property (assign, nonatomic) NSInteger index;
|
||||
@property (assign, nonatomic) NSInteger maxIndex;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation ORKLineGraphAccessibilityElement
|
||||
|
||||
- (nonnull instancetype)initWithAccessibilityContainer:(nonnull UIView *)container index:(NSInteger)index maxIndex:(NSInteger)maxIndex {
|
||||
|
||||
@@ -29,8 +29,7 @@
|
||||
*/
|
||||
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <ResearchKit/ResearchKit.h>
|
||||
@import UIKit;
|
||||
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@@ -29,8 +29,7 @@
|
||||
*/
|
||||
|
||||
|
||||
#import <CoreLocation/CoreLocation.h>
|
||||
#import <ResearchKit/ResearchKit.h>
|
||||
@import CoreLocation;
|
||||
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@@ -30,7 +30,8 @@
|
||||
|
||||
|
||||
#import "CLLocation+ORKJSONDictionary.h"
|
||||
#import "ORKHelpers.h"
|
||||
|
||||
#import "ORKHelpers_Internal.h"
|
||||
|
||||
|
||||
@implementation CLLocation (ORKJSONDictionary)
|
||||
@@ -45,10 +46,11 @@
|
||||
NSDate *timestamp = self.timestamp;
|
||||
CLFloor *floor = self.floor;
|
||||
|
||||
NSMutableDictionary *dictionary = [@{@"timestamp" : ORKStringFromDateISO8601(timestamp)} mutableCopy];
|
||||
NSMutableDictionary *dictionary = [@{ @"timestamp": ORKStringFromDateISO8601(timestamp) } mutableCopy];
|
||||
|
||||
if (horizAccuracy >= 0) {
|
||||
dictionary[@"coordinate"] = @{ @"latitude" : [NSDecimalNumber numberWithDouble:coord.latitude], @"longitude" : [NSDecimalNumber numberWithDouble:coord.longitude]};
|
||||
dictionary[@"coordinate"] = @{ @"latitude": [NSDecimalNumber numberWithDouble:coord.latitude],
|
||||
@"longitude": [NSDecimalNumber numberWithDouble:coord.longitude]};
|
||||
dictionary[@"horizontalAccuracy"] = [NSDecimalNumber numberWithDouble:horizAccuracy];
|
||||
}
|
||||
if (vertAccuracy >= 0) {
|
||||
|
||||
@@ -29,8 +29,7 @@
|
||||
*/
|
||||
|
||||
|
||||
#import <CoreMotion/CoreMotion.h>
|
||||
#import <ResearchKit/ResearchKit.h>
|
||||
@import CoreMotion;
|
||||
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@@ -35,10 +35,10 @@
|
||||
@implementation CMAccelerometerData (ORKJSONDictionary)
|
||||
|
||||
- (NSDictionary *)ork_JSONDictionary {
|
||||
NSDictionary *dictionary = @{@"timestamp": [NSDecimalNumber numberWithDouble:self.timestamp],
|
||||
@"x" : [NSDecimalNumber numberWithDouble:self.acceleration.x],
|
||||
@"y" : [NSDecimalNumber numberWithDouble:self.acceleration.y],
|
||||
@"z" : [NSDecimalNumber numberWithDouble:self.acceleration.z]
|
||||
NSDictionary *dictionary = @{ @"timestamp": [NSDecimalNumber numberWithDouble:self.timestamp],
|
||||
@"x": [NSDecimalNumber numberWithDouble:self.acceleration.x],
|
||||
@"y": [NSDecimalNumber numberWithDouble:self.acceleration.y],
|
||||
@"z": [NSDecimalNumber numberWithDouble:self.acceleration.z]
|
||||
};
|
||||
return dictionary;
|
||||
}
|
||||
|
||||
@@ -29,8 +29,7 @@
|
||||
*/
|
||||
|
||||
|
||||
#import <CoreMotion/CoreMotion.h>
|
||||
#import <ResearchKit/ResearchKit.h>
|
||||
@import CoreMotion;
|
||||
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@@ -42,34 +42,34 @@
|
||||
CMCalibratedMagneticField field = self.magneticField;
|
||||
|
||||
NSDictionary *dictionary = @{@"timestamp": [NSDecimalNumber numberWithDouble:self.timestamp],
|
||||
@"attitude" : @{
|
||||
@"x" : [NSDecimalNumber numberWithDouble:attitude.x],
|
||||
@"y" : [NSDecimalNumber numberWithDouble:attitude.y],
|
||||
@"z" : [NSDecimalNumber numberWithDouble:attitude.z],
|
||||
@"w" : [NSDecimalNumber numberWithDouble:attitude.w]
|
||||
},
|
||||
@"rotationRate" : @{
|
||||
@"x" : [NSDecimalNumber numberWithDouble:rotationRate.x],
|
||||
@"y" : [NSDecimalNumber numberWithDouble:rotationRate.y],
|
||||
@"z" : [NSDecimalNumber numberWithDouble:rotationRate.z]
|
||||
},
|
||||
@"gravity" : @{
|
||||
@"x" : [NSDecimalNumber numberWithDouble:gravity.x],
|
||||
@"y" : [NSDecimalNumber numberWithDouble:gravity.y],
|
||||
@"z" : [NSDecimalNumber numberWithDouble:gravity.z]
|
||||
},
|
||||
@"userAcceleration" : @{
|
||||
@"x" : [NSDecimalNumber numberWithDouble:userAccel.x],
|
||||
@"y" : [NSDecimalNumber numberWithDouble:userAccel.y],
|
||||
@"z" : [NSDecimalNumber numberWithDouble:userAccel.z]
|
||||
},
|
||||
@"magneticField" : @{
|
||||
@"x" : [NSDecimalNumber numberWithDouble:field.field.x],
|
||||
@"y" : [NSDecimalNumber numberWithDouble:field.field.y],
|
||||
@"z" : [NSDecimalNumber numberWithDouble:field.field.z],
|
||||
@"accuracy" : [NSDecimalNumber numberWithDouble:field.accuracy]
|
||||
}
|
||||
};
|
||||
@"attitude": @{
|
||||
@"x": [NSDecimalNumber numberWithDouble:attitude.x],
|
||||
@"y": [NSDecimalNumber numberWithDouble:attitude.y],
|
||||
@"z": [NSDecimalNumber numberWithDouble:attitude.z],
|
||||
@"w": [NSDecimalNumber numberWithDouble:attitude.w]
|
||||
},
|
||||
@"rotationRate": @{
|
||||
@"x": [NSDecimalNumber numberWithDouble:rotationRate.x],
|
||||
@"y": [NSDecimalNumber numberWithDouble:rotationRate.y],
|
||||
@"z": [NSDecimalNumber numberWithDouble:rotationRate.z]
|
||||
},
|
||||
@"gravity": @{
|
||||
@"x": [NSDecimalNumber numberWithDouble:gravity.x],
|
||||
@"y": [NSDecimalNumber numberWithDouble:gravity.y],
|
||||
@"z": [NSDecimalNumber numberWithDouble:gravity.z]
|
||||
},
|
||||
@"userAcceleration": @{
|
||||
@"x": [NSDecimalNumber numberWithDouble:userAccel.x],
|
||||
@"y": [NSDecimalNumber numberWithDouble:userAccel.y],
|
||||
@"z": [NSDecimalNumber numberWithDouble:userAccel.z]
|
||||
},
|
||||
@"magneticField": @{
|
||||
@"x": [NSDecimalNumber numberWithDouble:field.field.x],
|
||||
@"y": [NSDecimalNumber numberWithDouble:field.field.y],
|
||||
@"z": [NSDecimalNumber numberWithDouble:field.field.z],
|
||||
@"accuracy": [NSDecimalNumber numberWithDouble:field.accuracy]
|
||||
}
|
||||
};
|
||||
return dictionary;
|
||||
}
|
||||
|
||||
|
||||
@@ -29,8 +29,7 @@
|
||||
*/
|
||||
|
||||
|
||||
#import <CoreMotion/CoreMotion.h>
|
||||
#import <ResearchKit/ResearchKit.h>
|
||||
@import CoreMotion;
|
||||
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@@ -30,54 +30,55 @@
|
||||
|
||||
|
||||
#import "CMMotionActivity+ORKJSONDictionary.h"
|
||||
#import "ORKHelpers.h"
|
||||
|
||||
#import "ORKHelpers_Internal.h"
|
||||
|
||||
|
||||
static NSString *const kActivityUnknown = @"unknown";
|
||||
static NSString *const kActivityStationary = @"stationary";
|
||||
static NSString *const kActivityWalking = @"walking";
|
||||
static NSString *const kActivityRunning = @"running";
|
||||
static NSString *const kActivityAutomotive = @"automotive";
|
||||
static NSString *const kStartDateKey = @"startDate";
|
||||
static NSString *const kEndDateKey = @"endDate";
|
||||
static NSString *const ActivityUnknown = @"unknown";
|
||||
static NSString *const ActivityStationary = @"stationary";
|
||||
static NSString *const ActivityWalking = @"walking";
|
||||
static NSString *const ActivityRunning = @"running";
|
||||
static NSString *const ActivityAutomotive = @"automotive";
|
||||
static NSString *const StartDateKey = @"startDate";
|
||||
static NSString *const EndDateKey = @"endDate";
|
||||
|
||||
static NSString *stringFromActivityConfidence(CMMotionActivityConfidence confidence) {
|
||||
NSDictionary *confidences = @{@(CMMotionActivityConfidenceHigh) : @"high",
|
||||
@(CMMotionActivityConfidenceMedium) : @"medium",
|
||||
@(CMMotionActivityConfidenceLow) : @"low"};
|
||||
NSDictionary *confidences = @{@(CMMotionActivityConfidenceHigh): @"high",
|
||||
@(CMMotionActivityConfidenceMedium): @"medium",
|
||||
@(CMMotionActivityConfidenceLow): @"low"};
|
||||
return confidences[@(confidence)];
|
||||
}
|
||||
|
||||
static NSArray *activityArray(CMMotionActivity *activity) {
|
||||
NSMutableArray *array = [NSMutableArray array];
|
||||
if (activity.unknown) {
|
||||
[array addObject:kActivityUnknown];
|
||||
[array addObject:ActivityUnknown];
|
||||
}
|
||||
if (activity.stationary) {
|
||||
[array addObject:kActivityStationary];
|
||||
[array addObject:ActivityStationary];
|
||||
}
|
||||
if (activity.walking) {
|
||||
[array addObject:kActivityWalking];
|
||||
[array addObject:ActivityWalking];
|
||||
}
|
||||
if (activity.running) {
|
||||
[array addObject:kActivityRunning];
|
||||
[array addObject:ActivityRunning];
|
||||
}
|
||||
if (activity.automotive) {
|
||||
[array addObject:kActivityAutomotive];
|
||||
[array addObject:ActivityAutomotive];
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
static NSString *const kActivityKey = @"activity";
|
||||
static NSString *const ActivityKey = @"activity";
|
||||
static NSString *const ConfidenceKey = @"confidence";
|
||||
|
||||
static NSString *const kConfidenceKey = @"confidence";
|
||||
|
||||
@implementation CMMotionActivity (ORKJSONDictionary)
|
||||
|
||||
- (NSDictionary *)ork_JSONDictionary {
|
||||
return @{kConfidenceKey : stringFromActivityConfidence(self.confidence),
|
||||
kActivityKey : activityArray(self),
|
||||
kStartDateKey : ORKStringFromDateISO8601(self.startDate)};
|
||||
return @{ConfidenceKey: stringFromActivityConfidence(self.confidence),
|
||||
ActivityKey: activityArray(self),
|
||||
StartDateKey: ORKStringFromDateISO8601(self.startDate)};
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -29,8 +29,7 @@
|
||||
*/
|
||||
|
||||
|
||||
#import <CoreMotion/CoreMotion.h>
|
||||
#import <ResearchKit/ResearchKit.h>
|
||||
@import CoreMotion;
|
||||
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@@ -30,17 +30,17 @@
|
||||
|
||||
|
||||
#import "CMPedometerData+ORKJSONDictionary.h"
|
||||
#import "ORKHelpers.h"
|
||||
#import <CoreMotion/CoreMotion.h>
|
||||
|
||||
#import "ORKHelpers_Internal.h"
|
||||
|
||||
@import CoreMotion;
|
||||
|
||||
|
||||
@implementation CMPedometerData (ORKJSONDictionary)
|
||||
|
||||
- (NSDictionary *)ork_JSONDictionary {
|
||||
NSMutableDictionary *dictionary = [@{@"startDate": ORKStringFromDateISO8601(self.startDate),
|
||||
@"endDate": ORKStringFromDateISO8601(self.endDate)
|
||||
} mutableCopy];
|
||||
for (NSString *key in @[@"numberOfSteps", @"distance", @"floorsAscended", @"floorsDescended"]) {
|
||||
NSMutableDictionary *dictionary = [@{ @"startDate": ORKStringFromDateISO8601(self.startDate), @"endDate": ORKStringFromDateISO8601(self.endDate) } mutableCopy];
|
||||
for (NSString *key in @[ @"numberOfSteps", @"distance", @"floorsAscended", @"floorsDescended" ]) {
|
||||
[dictionary setValue:[self valueForKey:key] forKey:key];
|
||||
}
|
||||
return dictionary;
|
||||
|
||||
@@ -29,8 +29,7 @@
|
||||
*/
|
||||
|
||||
|
||||
#import <HealthKit/HealthKit.h>
|
||||
#import <ResearchKit/ResearchKit.h>
|
||||
@import HealthKit;
|
||||
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@@ -30,19 +30,20 @@
|
||||
|
||||
|
||||
#import "HKSample+ORKJSONDictionary.h"
|
||||
#import "ORKHelpers.h"
|
||||
|
||||
#import "ORKHelpers_Internal.h"
|
||||
|
||||
|
||||
static NSString *const kHKSampleIdentifierKey = @"type"; // For compatibility with Health XML export
|
||||
static NSString *const kHKUUIDKey = @"uuid";
|
||||
static NSString *const kHKSampleStartDateKey = @"startDate";
|
||||
static NSString *const kHKSampleEndDateKey = @"endDate";
|
||||
static NSString *const kHKSampleValue = @"value";
|
||||
static NSString *const kHKMetadataKey = @"metadata";
|
||||
static NSString *const kHKSourceKey = @"source";
|
||||
static NSString *const kHKUnitKey = @"unit";
|
||||
static NSString *const kHKCorrelatedObjectsKey = @"objects";
|
||||
// static NSString *const kHKSourceIdentifierKey = @"sourceBundleIdentifier";
|
||||
static NSString *const HKSampleIdentifierKey = @"type"; // For compatibility with Health XML export
|
||||
static NSString *const HKUUIDKey = @"uuid";
|
||||
static NSString *const HKSampleStartDateKey = @"startDate";
|
||||
static NSString *const HKSampleEndDateKey = @"endDate";
|
||||
static NSString *const HKSampleValue = @"value";
|
||||
static NSString *const HKMetadataKey = @"metadata";
|
||||
static NSString *const HKSourceKey = @"source";
|
||||
static NSString *const HKUnitKey = @"unit";
|
||||
static NSString *const HKCorrelatedObjectsKey = @"objects";
|
||||
// static NSString *const HKSourceIdentifierKey = @"sourceBundleIdentifier";
|
||||
|
||||
|
||||
@implementation HKSample (ORKJSONDictionary)
|
||||
@@ -52,31 +53,31 @@ static NSString *const kHKCorrelatedObjectsKey = @"objects";
|
||||
|
||||
// Type identification
|
||||
HKSampleType *sampleType = [self sampleType];
|
||||
mutableDictionary[kHKSampleIdentifierKey] = [sampleType identifier];
|
||||
mutableDictionary[HKSampleIdentifierKey] = [sampleType identifier];
|
||||
|
||||
// consider adding @"class" : NSStringFromClass(sampleType) ?
|
||||
|
||||
// Start and end dates
|
||||
NSDate *startDate = [self startDate];
|
||||
if (startDate) {
|
||||
mutableDictionary[kHKSampleStartDateKey] = ORKStringFromDateISO8601(startDate);
|
||||
mutableDictionary[HKSampleStartDateKey] = ORKStringFromDateISO8601(startDate);
|
||||
}
|
||||
|
||||
NSDate *endDate = [self endDate];
|
||||
if (endDate) {
|
||||
mutableDictionary[kHKSampleEndDateKey] = ORKStringFromDateISO8601(endDate);
|
||||
mutableDictionary[HKSampleEndDateKey] = ORKStringFromDateISO8601(endDate);
|
||||
}
|
||||
if (unit) {
|
||||
mutableDictionary[kHKUnitKey] = [unit unitString];
|
||||
mutableDictionary[HKUnitKey] = [unit unitString];
|
||||
}
|
||||
if ((options & ORKSampleIncludeUUID)) {
|
||||
NSUUID *uuid = [self UUID];
|
||||
if (uuid) {
|
||||
mutableDictionary[kHKUUIDKey] = uuid.UUIDString;
|
||||
mutableDictionary[HKUUIDKey] = uuid.UUIDString;
|
||||
}
|
||||
}
|
||||
|
||||
if ( (options & ORKSampleIncludeMetadata) && [self.metadata count] > 0) {
|
||||
if ( (options & ORKSampleIncludeMetadata) && self.metadata.count > 0) {
|
||||
NSMutableDictionary *metadata = [self.metadata mutableCopy];
|
||||
for (NSString *k in metadata) {
|
||||
id obj = metadata[k];
|
||||
@@ -85,13 +86,13 @@ static NSString *const kHKCorrelatedObjectsKey = @"objects";
|
||||
}
|
||||
}
|
||||
|
||||
mutableDictionary[kHKMetadataKey] = metadata;
|
||||
mutableDictionary[HKMetadataKey] = metadata;
|
||||
}
|
||||
|
||||
if (options & ORKSampleIncludeSource) {
|
||||
HKSource *source = [self source];
|
||||
HKSource *source = [[self sourceRevision] source];
|
||||
if (source.name) {
|
||||
mutableDictionary[kHKSourceKey] = source.name;
|
||||
mutableDictionary[HKSourceKey] = source.name;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,8 +116,8 @@ static NSString *const kHKCorrelatedObjectsKey = @"objects";
|
||||
- (NSDictionary *)ork_JSONDictionaryWithOptions:(ORKSampleJSONOptions)options unit:(HKUnit *)unit {
|
||||
NSMutableDictionary *dictionary = [self ork_JSONMutableDictionaryWithOptions:options unit:unit];
|
||||
|
||||
NSInteger value = [self value];
|
||||
dictionary[kHKSampleValue] = @(value);
|
||||
NSInteger value = self.value;
|
||||
dictionary[HKSampleValue] = @(value);
|
||||
|
||||
return dictionary;
|
||||
}
|
||||
@@ -136,7 +137,7 @@ static NSString *const kHKCorrelatedObjectsKey = @"objects";
|
||||
|
||||
HKQuantity *quantity = [self quantity];
|
||||
double value = [quantity doubleValueForUnit:unit];
|
||||
dictionary[kHKSampleValue] = @(value);
|
||||
dictionary[HKSampleValue] = @(value);
|
||||
|
||||
|
||||
return dictionary;
|
||||
@@ -151,7 +152,7 @@ static NSString *const kHKCorrelatedObjectsKey = @"objects";
|
||||
NSMutableDictionary *mutableDictionary = [self ork_JSONMutableDictionaryWithOptions:options unit:nil];
|
||||
|
||||
// The correlated objects
|
||||
NSMutableArray *correlatedObjects = [NSMutableArray arrayWithCapacity:[sampleTypes count]];
|
||||
NSMutableArray *correlatedObjects = [NSMutableArray arrayWithCapacity:sampleTypes.count];
|
||||
for (HKSample *sample in self.objects) {
|
||||
NSUInteger idx = [sampleTypes indexOfObject:sample.sampleType];
|
||||
if (idx == NSNotFound) {
|
||||
@@ -160,7 +161,7 @@ static NSString *const kHKCorrelatedObjectsKey = @"objects";
|
||||
|
||||
[correlatedObjects addObject:[sample ork_JSONDictionaryWithOptions:options unit:units[idx]]];
|
||||
}
|
||||
mutableDictionary[kHKCorrelatedObjectsKey] = correlatedObjects;
|
||||
mutableDictionary[HKCorrelatedObjectsKey] = correlatedObjects;
|
||||
|
||||
return mutableDictionary;
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
*/
|
||||
|
||||
|
||||
@import UIKit;
|
||||
#import <ResearchKit/ORKRecorder.h>
|
||||
|
||||
|
||||
@@ -58,7 +59,7 @@ ORK_CLASS_AVAILABLE
|
||||
|
||||
@return An initialized accelerometer recorder.
|
||||
*/
|
||||
- (instancetype)initWithIdentifier:(NSString *)identifer
|
||||
- (instancetype)initWithIdentifier:(NSString *)identifier
|
||||
frequency:(double)frequency
|
||||
step:(nullable ORKStep *)step
|
||||
outputDirectory:(nullable NSURL *)outputDirectory;
|
||||
|
||||
@@ -30,12 +30,15 @@
|
||||
|
||||
|
||||
#import "ORKAccelerometerRecorder.h"
|
||||
|
||||
#import "ORKDataLogger.h"
|
||||
#import "CMAccelerometerData+ORKJSONDictionary.h"
|
||||
#import <CoreMotion/CoreMotion.h>
|
||||
|
||||
#import "ORKRecorder_Internal.h"
|
||||
#import "ORKRecorder_Private.h"
|
||||
#import "ORKHelpers.h"
|
||||
|
||||
#import "ORKHelpers_Internal.h"
|
||||
#import "CMAccelerometerData+ORKJSONDictionary.h"
|
||||
|
||||
@import CoreMotion;
|
||||
|
||||
|
||||
@interface ORKAccelerometerRecorder () {
|
||||
@@ -70,7 +73,7 @@
|
||||
}
|
||||
|
||||
- (void)setFrequency:(double)frequency {
|
||||
if (frequency <=0) {
|
||||
if (frequency <= 0) {
|
||||
_frequency = 1;
|
||||
} else {
|
||||
_frequency = frequency;
|
||||
@@ -86,24 +89,24 @@
|
||||
|
||||
self.motionManager = [self createMotionManager];
|
||||
|
||||
if (! _logger) {
|
||||
NSError *err = nil;
|
||||
_logger = [self makeJSONDataLoggerWithError:&err];
|
||||
if (! _logger) {
|
||||
[self finishRecordingWithError:err];
|
||||
if (!_logger) {
|
||||
NSError *error = nil;
|
||||
_logger = [self makeJSONDataLoggerWithError:&error];
|
||||
if (!_logger) {
|
||||
[self finishRecordingWithError:error];
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (! self.motionManager || ! self.motionManager.accelerometerAvailable) {
|
||||
if (!self.motionManager || !self.motionManager.accelerometerAvailable) {
|
||||
NSError *error = [NSError errorWithDomain:NSCocoaErrorDomain
|
||||
code:NSFeatureUnsupportedError
|
||||
userInfo:@{@"recorder" : self}];
|
||||
userInfo:@{@"recorder": self}];
|
||||
[self finishRecordingWithError:error];
|
||||
return;
|
||||
}
|
||||
|
||||
self.motionManager.accelerometerUpdateInterval = 1.0/_frequency;
|
||||
self.motionManager.accelerometerUpdateInterval = 1.0 / _frequency;
|
||||
|
||||
self.uptime = [NSProcessInfo processInfo].systemUptime;
|
||||
|
||||
@@ -124,7 +127,7 @@
|
||||
}
|
||||
|
||||
- (NSDictionary *)userInfo {
|
||||
return @{ @"frequency" : @(self.frequency) };
|
||||
return @{ @"frequency": @(self.frequency) };
|
||||
}
|
||||
|
||||
- (void)stop {
|
||||
@@ -172,11 +175,6 @@
|
||||
@end
|
||||
|
||||
|
||||
@interface ORKAccelerometerRecorderConfiguration ()
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation ORKAccelerometerRecorderConfiguration
|
||||
|
||||
#pragma clang diagnostic push
|
||||
|
||||
@@ -29,10 +29,10 @@
|
||||
*/
|
||||
|
||||
|
||||
#import <ResearchKit/ORKDefines.h>
|
||||
@import UIKit;
|
||||
@import HealthKit;
|
||||
#import <ResearchKit/ORKStep.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <HealthKit/HealthKit.h>
|
||||
|
||||
|
||||
@class ORKRecorderConfiguration;
|
||||
|
||||
@@ -103,6 +103,17 @@ automatically navigates forward when the timer expires.
|
||||
*/
|
||||
@property (nonatomic) BOOL shouldSpeakCountDown;
|
||||
|
||||
/**
|
||||
A Boolean value indicating whether to speak the halfway point in the count down of the
|
||||
duration of a timed step.
|
||||
|
||||
When the value of this property is `YES`, `AVSpeechSynthesizer` is used to synthesize the countdown. Note that this property is ignored if VoiceOver is enabled.
|
||||
|
||||
The default value of this property is `NO`.
|
||||
*/
|
||||
@property (nonatomic) BOOL shouldSpeakRemainingTimeAtHalfway;
|
||||
|
||||
|
||||
/**
|
||||
A Boolean value indicating whether to start the count down timer automatically when the step starts, or
|
||||
require the user to take some explicit action to start the step, such as tapping a button.
|
||||
@@ -171,6 +182,14 @@ The default value of this property is `NO`.
|
||||
*/
|
||||
@property (nonatomic, copy, nullable) NSString *spokenInstruction;
|
||||
|
||||
/**
|
||||
Localized text that represents an instructional voice prompt for when the step finishes.
|
||||
|
||||
Instructional speech begins when the step finishes. If VoiceOver is active,
|
||||
the instruction is spoken by VoiceOver.
|
||||
*/
|
||||
@property (nonatomic, copy, nullable) NSString *finishedSpokenInstruction;
|
||||
|
||||
/**
|
||||
An image to be displayed below the instructions for the step.
|
||||
|
||||
@@ -195,18 +214,6 @@ The default value of this property is `NO`.
|
||||
*/
|
||||
@property (nonatomic, copy, nullable) NSArray<ORKRecorderConfiguration *> *recorderConfigurations;
|
||||
|
||||
/**
|
||||
The set of HealthKit types the step requests for reading. (read-only)
|
||||
|
||||
The task view controller uses this set of types when constructing a list of
|
||||
all the HealthKit types required by all the steps in a task, so that it can
|
||||
present the HealthKit access dialog just once during that task.
|
||||
|
||||
By default, the property scans the recorders and collates the HealthKit
|
||||
types the recorders require. Subclasses may override this implementation.
|
||||
*/
|
||||
@property (nonatomic, readonly, nullable) NSSet<HKObjectType *> *requestedHealthKitTypesForReading;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
@@ -30,12 +30,15 @@
|
||||
|
||||
|
||||
#import "ORKActiveStep.h"
|
||||
#import "ORKHelpers.h"
|
||||
#import "ORKStep_Private.h"
|
||||
#import "ORKActiveStep_Internal.h"
|
||||
|
||||
#import "ORKActiveStepViewController.h"
|
||||
#import "ORKRecorder_Private.h"
|
||||
|
||||
#import "ORKStep_Private.h"
|
||||
|
||||
#import "ORKHelpers_Internal.h"
|
||||
|
||||
|
||||
@implementation ORKActiveStep
|
||||
|
||||
@@ -62,7 +65,9 @@
|
||||
}
|
||||
|
||||
- (BOOL)hasVoice {
|
||||
return (_spokenInstruction != nil && _spokenInstruction.length > 0);
|
||||
BOOL hasSpokenInstruction = (_spokenInstruction != nil && _spokenInstruction.length > 0);
|
||||
BOOL hasFinishedSpokenInstruction = (_finishedSpokenInstruction != nil && _finishedSpokenInstruction.length > 0);
|
||||
return (hasSpokenInstruction || hasFinishedSpokenInstruction);
|
||||
}
|
||||
|
||||
- (BOOL)isRestorable {
|
||||
@@ -86,6 +91,7 @@
|
||||
step.stepDuration = self.stepDuration;
|
||||
step.shouldStartTimerAutomatically = self.shouldStartTimerAutomatically;
|
||||
step.shouldSpeakCountDown = self.shouldSpeakCountDown;
|
||||
step.shouldSpeakRemainingTimeAtHalfway = self.shouldSpeakRemainingTimeAtHalfway;
|
||||
step.shouldShowDefaultTimer = self.shouldShowDefaultTimer;
|
||||
step.shouldPlaySoundOnStart = self.shouldPlaySoundOnStart;
|
||||
step.shouldPlaySoundOnFinish = self.shouldPlaySoundOnFinish;
|
||||
@@ -94,6 +100,7 @@
|
||||
step.shouldUseNextAsSkipButton = self.shouldUseNextAsSkipButton;
|
||||
step.shouldContinueOnFinish = self.shouldContinueOnFinish;
|
||||
step.spokenInstruction = self.spokenInstruction;
|
||||
step.finishedSpokenInstruction = self.finishedSpokenInstruction;
|
||||
step.recorderConfigurations = [self.recorderConfigurations copy];
|
||||
step.image = self.image;
|
||||
return step;
|
||||
@@ -105,6 +112,7 @@
|
||||
ORK_DECODE_DOUBLE(aDecoder, stepDuration);
|
||||
ORK_DECODE_BOOL(aDecoder, shouldStartTimerAutomatically);
|
||||
ORK_DECODE_BOOL(aDecoder, shouldSpeakCountDown);
|
||||
ORK_DECODE_BOOL(aDecoder, shouldSpeakRemainingTimeAtHalfway);
|
||||
ORK_DECODE_BOOL(aDecoder, shouldShowDefaultTimer);
|
||||
ORK_DECODE_BOOL(aDecoder, shouldPlaySoundOnStart);
|
||||
ORK_DECODE_BOOL(aDecoder, shouldPlaySoundOnFinish);
|
||||
@@ -113,6 +121,7 @@
|
||||
ORK_DECODE_BOOL(aDecoder, shouldUseNextAsSkipButton);
|
||||
ORK_DECODE_BOOL(aDecoder, shouldContinueOnFinish);
|
||||
ORK_DECODE_OBJ_CLASS(aDecoder, spokenInstruction, NSString);
|
||||
ORK_DECODE_OBJ_CLASS(aDecoder, finishedSpokenInstruction, NSString);
|
||||
ORK_DECODE_IMAGE(aDecoder, image);
|
||||
ORK_DECODE_OBJ_ARRAY(aDecoder, recorderConfigurations, ORKRecorderConfiguration);
|
||||
}
|
||||
@@ -124,6 +133,7 @@
|
||||
ORK_ENCODE_DOUBLE(aCoder, stepDuration);
|
||||
ORK_ENCODE_BOOL(aCoder, shouldStartTimerAutomatically);
|
||||
ORK_ENCODE_BOOL(aCoder, shouldSpeakCountDown);
|
||||
ORK_ENCODE_BOOL(aCoder, shouldSpeakRemainingTimeAtHalfway);
|
||||
ORK_ENCODE_BOOL(aCoder, shouldShowDefaultTimer);
|
||||
ORK_ENCODE_BOOL(aCoder, shouldPlaySoundOnStart);
|
||||
ORK_ENCODE_BOOL(aCoder, shouldPlaySoundOnFinish);
|
||||
@@ -133,6 +143,7 @@
|
||||
ORK_ENCODE_BOOL(aCoder, shouldContinueOnFinish);
|
||||
ORK_ENCODE_IMAGE(aCoder, image);
|
||||
ORK_ENCODE_OBJ(aCoder, spokenInstruction);
|
||||
ORK_ENCODE_OBJ(aCoder, finishedSpokenInstruction);
|
||||
ORK_ENCODE_OBJ(aCoder, recorderConfigurations);
|
||||
}
|
||||
|
||||
@@ -142,12 +153,14 @@
|
||||
__typeof(self) castObject = object;
|
||||
return (isParentSame &&
|
||||
ORKEqualObjects(self.spokenInstruction, castObject.spokenInstruction) &&
|
||||
ORKEqualObjects(self.finishedSpokenInstruction, castObject.finishedSpokenInstruction) &&
|
||||
ORKEqualObjects(self.recorderConfigurations, castObject.recorderConfigurations) &&
|
||||
ORKEqualObjects(self.image, castObject.image) &&
|
||||
(self.stepDuration == castObject.stepDuration) &&
|
||||
(self.shouldShowDefaultTimer == castObject.shouldShowDefaultTimer) &&
|
||||
(self.shouldStartTimerAutomatically == castObject.shouldStartTimerAutomatically) &&
|
||||
(self.shouldSpeakCountDown == castObject.shouldSpeakCountDown) &&
|
||||
(self.shouldSpeakRemainingTimeAtHalfway == castObject.shouldSpeakRemainingTimeAtHalfway) &&
|
||||
(self.shouldPlaySoundOnStart == castObject.shouldPlaySoundOnStart) &&
|
||||
(self.shouldPlaySoundOnFinish == castObject.shouldPlaySoundOnFinish) &&
|
||||
(self.shouldVibrateOnStart == castObject.shouldVibrateOnStart) &&
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
*/
|
||||
|
||||
|
||||
#import <ResearchKit/ResearchKit_Private.h>
|
||||
@import UIKit;
|
||||
#import "ORKLabel.h"
|
||||
|
||||
|
||||
|
||||
@@ -30,10 +30,12 @@
|
||||
|
||||
|
||||
#import "ORKActiveStepQuantityView.h"
|
||||
#import "ORKHelpers.h"
|
||||
#import "ORKSkin.h"
|
||||
#import "ORKTintedImageView.h"
|
||||
|
||||
#import "ORKSubheadlineLabel.h"
|
||||
#import "ORKTintedImageView.h"
|
||||
|
||||
#import "ORKHelpers_Internal.h"
|
||||
#import "ORKSkin.h"
|
||||
|
||||
|
||||
@implementation ORKQuantityLabel
|
||||
@@ -84,15 +86,14 @@
|
||||
view.isAccessibilityElement = NO;
|
||||
}
|
||||
|
||||
[self setupConstraints];
|
||||
[self setNeedsUpdateConstraints];
|
||||
[self setUpConstraints];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setEnabled:(BOOL)enabled {
|
||||
_enabled = enabled;
|
||||
self.hidden = ! enabled;
|
||||
self.hidden = !enabled;
|
||||
[self setNeedsUpdateConstraints];
|
||||
}
|
||||
|
||||
@@ -111,91 +112,89 @@
|
||||
_imageView.image = image;
|
||||
}
|
||||
|
||||
- (void)setupConstraints {
|
||||
- (void)setUpConstraints {
|
||||
|
||||
const CGFloat TitleBaselineToValueBaseline = 40;
|
||||
const CGFloat ValueBaselineToBottom = 36;
|
||||
|
||||
NSMutableArray *additionalConstraints = [NSMutableArray array];
|
||||
NSMutableArray *constraints = [NSMutableArray array];
|
||||
NSDictionary *views = NSDictionaryOfVariableBindings(_titleLabel, _valueLabel, _imageView);
|
||||
[additionalConstraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[_titleLabel]"
|
||||
[constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[_titleLabel]"
|
||||
options:(NSLayoutFormatOptions)0
|
||||
metrics:nil
|
||||
views:views]];
|
||||
[additionalConstraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[_titleLabel]|"
|
||||
[constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[_titleLabel]|"
|
||||
options:(NSLayoutFormatOptions)0
|
||||
metrics:nil
|
||||
views:views]];
|
||||
[additionalConstraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[_imageView]-10-[_valueLabel]|"
|
||||
[constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[_imageView]-10-[_valueLabel]|"
|
||||
options:NSLayoutFormatAlignAllCenterY
|
||||
metrics:nil
|
||||
views:views]];
|
||||
[additionalConstraints addObject:[NSLayoutConstraint constraintWithItem:_valueLabel
|
||||
[constraints addObject:[NSLayoutConstraint constraintWithItem:_valueLabel
|
||||
attribute:NSLayoutAttributeFirstBaseline
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:_titleLabel
|
||||
attribute:NSLayoutAttributeLastBaseline
|
||||
multiplier:1.0
|
||||
constant:TitleBaselineToValueBaseline]];
|
||||
[additionalConstraints addObject:[NSLayoutConstraint constraintWithItem:self
|
||||
[constraints addObject:[NSLayoutConstraint constraintWithItem:self
|
||||
attribute:NSLayoutAttributeBottom
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:_valueLabel
|
||||
attribute:NSLayoutAttributeLastBaseline
|
||||
multiplier:1.0
|
||||
constant:ValueBaselineToBottom]];
|
||||
[additionalConstraints addObject:[NSLayoutConstraint constraintWithItem:_valueHolder
|
||||
[constraints addObject:[NSLayoutConstraint constraintWithItem:_valueHolder
|
||||
attribute:NSLayoutAttributeCenterX
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:self
|
||||
attribute:NSLayoutAttributeCenterX
|
||||
multiplier:1.0
|
||||
constant:0.0]];
|
||||
[additionalConstraints addObject:[NSLayoutConstraint constraintWithItem:_valueLabel
|
||||
[constraints addObject:[NSLayoutConstraint constraintWithItem:_valueLabel
|
||||
attribute:NSLayoutAttributeTop
|
||||
relatedBy:NSLayoutRelationGreaterThanOrEqual
|
||||
toItem:_valueHolder
|
||||
attribute:NSLayoutAttributeTop
|
||||
multiplier:1.0
|
||||
constant:0.0]];
|
||||
[additionalConstraints addObject:[NSLayoutConstraint constraintWithItem:_valueLabel
|
||||
[constraints addObject:[NSLayoutConstraint constraintWithItem:_valueLabel
|
||||
attribute:NSLayoutAttributeBottom
|
||||
relatedBy:NSLayoutRelationLessThanOrEqual
|
||||
toItem:_valueHolder
|
||||
attribute:NSLayoutAttributeBottom
|
||||
multiplier:1.0
|
||||
constant:0.0]];
|
||||
[additionalConstraints addObject:[NSLayoutConstraint constraintWithItem:_valueHolder
|
||||
[constraints addObject:[NSLayoutConstraint constraintWithItem:_valueHolder
|
||||
attribute:NSLayoutAttributeLeft
|
||||
relatedBy:NSLayoutRelationGreaterThanOrEqual
|
||||
toItem:self
|
||||
attribute:NSLayoutAttributeLeft
|
||||
multiplier:1.0
|
||||
constant:0.0]];
|
||||
[additionalConstraints addObject:[NSLayoutConstraint constraintWithItem:_valueHolder
|
||||
[constraints addObject:[NSLayoutConstraint constraintWithItem:_valueHolder
|
||||
attribute:NSLayoutAttributeRight
|
||||
relatedBy:NSLayoutRelationLessThanOrEqual
|
||||
toItem:self
|
||||
attribute:NSLayoutAttributeRight
|
||||
multiplier:1.0
|
||||
constant:0.0]];
|
||||
for (NSLayoutConstraint *constraint in additionalConstraints) {
|
||||
constraint.priority = UILayoutPriorityRequired-2;
|
||||
for (NSLayoutConstraint *constraint in constraints) {
|
||||
constraint.priority = UILayoutPriorityRequired - 2;
|
||||
}
|
||||
|
||||
[NSLayoutConstraint activateConstraints:additionalConstraints];
|
||||
[NSLayoutConstraint activateConstraints:constraints];
|
||||
|
||||
|
||||
NSLayoutConstraint *zeroWidthConstraint = [NSLayoutConstraint constraintWithItem:self
|
||||
attribute:NSLayoutAttributeWidth
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:nil
|
||||
attribute:NSLayoutAttributeNotAnAttribute
|
||||
multiplier:1.0
|
||||
constant:0.0];
|
||||
zeroWidthConstraint.priority = UILayoutPriorityRequired-1;
|
||||
_zeroWidthConstraint = zeroWidthConstraint;
|
||||
_zeroWidthConstraint.active = !_enabled;
|
||||
_zeroWidthConstraint = [NSLayoutConstraint constraintWithItem:self
|
||||
attribute:NSLayoutAttributeWidth
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:nil
|
||||
attribute:NSLayoutAttributeNotAnAttribute
|
||||
multiplier:1.0
|
||||
constant:0.0];
|
||||
_zeroWidthConstraint.priority = UILayoutPriorityRequired - 1;
|
||||
[self setNeedsUpdateConstraints];
|
||||
}
|
||||
|
||||
- (void)updateConstraints {
|
||||
@@ -245,18 +244,18 @@
|
||||
[self addSubview:_leftView];
|
||||
[self addSubview:_rightView];
|
||||
[self addSubview:_metricKeyline];
|
||||
[self setupConstraints];
|
||||
[self setUpConstraints];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setupConstraints {
|
||||
- (void)setUpConstraints {
|
||||
|
||||
NSMutableArray *constraints = [NSMutableArray array];
|
||||
NSDictionary *views = NSDictionaryOfVariableBindings(_leftView, _rightView, _metricKeyline);
|
||||
|
||||
// Leave space for the keyline between these views, and then constrain it to be 1px wide and go from top to bottom baseline of metric views.
|
||||
CGFloat scale = [[UIScreen mainScreen] scale];
|
||||
CGFloat scale = [UIScreen mainScreen].scale;
|
||||
NSArray *vertConstraints = [NSLayoutConstraint constraintsWithVisualFormat:@"V:|[_leftView]|"
|
||||
options:(NSLayoutFormatOptions)0
|
||||
metrics:nil
|
||||
@@ -265,10 +264,10 @@
|
||||
|
||||
NSArray *horizConstraints = [NSLayoutConstraint constraintsWithVisualFormat:@"H:|-[_leftView]-s-[_rightView]-|"
|
||||
options:NSLayoutFormatAlignAllTop|NSLayoutFormatAlignAllBottom|NSLayoutFormatDirectionLeftToRight
|
||||
metrics:@{ @"s": @(1/scale) }
|
||||
metrics:@{ @"s": @(1.0 / scale) }
|
||||
views:views];
|
||||
for (NSLayoutConstraint *constraint in horizConstraints) {
|
||||
constraint.priority = UILayoutPriorityDefaultHigh+1;
|
||||
constraint.priority = UILayoutPriorityDefaultHigh + 1;
|
||||
}
|
||||
[constraints addObjectsFromArray:horizConstraints];
|
||||
|
||||
@@ -290,7 +289,7 @@
|
||||
|
||||
[constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"H:[_leftView][_metricKeyline(==s)]"
|
||||
options:NSLayoutFormatAlignAllTop|NSLayoutFormatDirectionLeftToRight
|
||||
metrics:@{ @"s": @(1/scale) }
|
||||
metrics:@{ @"s": @(1.0 / scale) }
|
||||
views:views]];
|
||||
NSLayoutConstraint *keylineBottom = [NSLayoutConstraint constraintWithItem:_metricKeyline
|
||||
attribute:NSLayoutAttributeBottom
|
||||
@@ -308,7 +307,7 @@
|
||||
attribute:NSLayoutAttributeNotAnAttribute
|
||||
multiplier:1.0
|
||||
constant:ORKScreenMetricMaxDimension];
|
||||
maxWidthConstraint.priority = UILayoutPriorityRequired-2;
|
||||
maxWidthConstraint.priority = UILayoutPriorityRequired - 2;
|
||||
[constraints addObject:maxWidthConstraint];
|
||||
|
||||
|
||||
|
||||
@@ -29,8 +29,7 @@
|
||||
*/
|
||||
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <ResearchKit/ResearchKit.h>
|
||||
@import Foundation;
|
||||
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@@ -30,9 +30,12 @@
|
||||
|
||||
|
||||
#import "ORKActiveStepTimer.h"
|
||||
|
||||
#import "ORKHelpers_Internal.h"
|
||||
|
||||
@import UIKit;
|
||||
#include <mach/mach.h>
|
||||
#include <mach/mach_time.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
|
||||
static NSTimeInterval timeIntervalFromMachTime(uint64_t delta) {
|
||||
@@ -44,6 +47,7 @@ static NSTimeInterval timeIntervalFromMachTime(uint64_t delta) {
|
||||
return elapsedNano * 1.0 / NSEC_PER_SEC;
|
||||
}
|
||||
|
||||
|
||||
@implementation ORKActiveStepTimer {
|
||||
uint64_t _startTime;
|
||||
NSTimeInterval _preExistingRuntime;
|
||||
@@ -56,7 +60,7 @@ static NSTimeInterval timeIntervalFromMachTime(uint64_t delta) {
|
||||
- (instancetype)initWithDuration:(NSTimeInterval)duration interval:(NSTimeInterval)interval runtime:(NSTimeInterval)runtime handler:(ORKActiveStepTimerHandler)handler {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
if (! handler) {
|
||||
if (!handler) {
|
||||
@throw [NSException exceptionWithName:NSInvalidArgumentException reason:@"Handler is required" userInfo:nil];
|
||||
}
|
||||
|
||||
@@ -197,9 +201,9 @@ static NSTimeInterval timeIntervalFromMachTime(uint64_t delta) {
|
||||
assert(0);
|
||||
return;
|
||||
}
|
||||
__weak typeof(self) weakSelf = self;
|
||||
ORKWeakTypeOf(self) weakSelf = self;
|
||||
dispatch_source_set_event_handler(_timer, ^{
|
||||
typeof(self) strongSelf = weakSelf;
|
||||
ORKStrongTypeOf(self) strongSelf = weakSelf;
|
||||
[strongSelf hiqueue_event];
|
||||
});
|
||||
|
||||
@@ -224,7 +228,7 @@ static NSTimeInterval timeIntervalFromMachTime(uint64_t delta) {
|
||||
_preExistingRuntime += timeIntervalFromMachTime(now - _startTime);
|
||||
_startTime = 0;
|
||||
|
||||
if (! atFinish) {
|
||||
if (!atFinish) {
|
||||
// If we are atFinish, the task will be released after the handler completes
|
||||
[self queue_releaseBackgroundTask];
|
||||
}
|
||||
|
||||
@@ -29,18 +29,20 @@
|
||||
*/
|
||||
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <ResearchKit/ResearchKit_Private.h>
|
||||
@import UIKit;
|
||||
#import "ORKCustomStepView_Internal.h"
|
||||
#import "ORKCountdownLabel.h"
|
||||
#import "ORKTextButton.h"
|
||||
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class ORKActiveStep;
|
||||
@class ORKCountdownLabel;
|
||||
@class ORKTextButton;
|
||||
|
||||
@interface ORKActiveStepTimerView : ORKActiveStepCustomView
|
||||
|
||||
@property (nonatomic, strong, nullable) ORKCountdownLabel *countDownLabel;
|
||||
|
||||
@property (nonatomic, strong, nullable) ORKTextButton *startTimerButton;
|
||||
|
||||
@property (nonatomic, strong, nullable) ORKActiveStep *step;
|
||||
|
||||
@@ -30,21 +30,30 @@
|
||||
|
||||
|
||||
#import "ORKActiveStepTimerView.h"
|
||||
|
||||
#import "ORKActiveStepTimer.h"
|
||||
#import "ORKHelpers.h"
|
||||
#import "ORKSkin.h"
|
||||
#import "ORKActiveStepViewController_Internal.h"
|
||||
#import "ORKVoiceEngine.h"
|
||||
#import "ORKCountdownLabel.h"
|
||||
#import "ORKSurveyAnswerCellForText.h"
|
||||
#import "ORKSurveyAnswerCellForNumber.h"
|
||||
#import "ORKActiveStep_Internal.h"
|
||||
#import "ORKTextButton.h"
|
||||
#import "ORKVoiceEngine.h"
|
||||
|
||||
#import "ORKActiveStepViewController_Internal.h"
|
||||
#import "ORKStepViewController_Internal.h"
|
||||
|
||||
#import "ORKActiveStep_Internal.h"
|
||||
|
||||
#import "ORKHelpers_Internal.h"
|
||||
#import "ORKSkin.h"
|
||||
|
||||
|
||||
@implementation ORKActiveStepTimerView {
|
||||
BOOL _started;
|
||||
BOOL _registeredForNotifications;
|
||||
|
||||
NSLayoutConstraint *_countDownLabelBottomToStartTimerButtonTopConstraint;
|
||||
NSLayoutConstraint *_countDownLabelZeroHeightConstraint;
|
||||
NSLayoutConstraint *_startTimerButtonZeroHeightConstraint;
|
||||
}
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
@@ -70,7 +79,8 @@
|
||||
|
||||
_countDownLabel.accessibilityTraits |= UIAccessibilityTraitUpdatesFrequently;
|
||||
|
||||
[self setTranslatesAutoresizingMaskIntoConstraints:NO];
|
||||
self.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
[self setUpConstraints];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@@ -88,11 +98,11 @@
|
||||
}
|
||||
|
||||
registered = _registeredForNotifications;
|
||||
NSNotificationCenter *nfc = [NSNotificationCenter defaultCenter];
|
||||
NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
|
||||
if (registered) {
|
||||
[nfc addObserver:self selector:@selector(applicationDidBecomeActive:) name:UIApplicationDidBecomeActiveNotification object:nil];
|
||||
[notificationCenter addObserver:self selector:@selector(applicationDidBecomeActive:) name:UIApplicationDidBecomeActiveNotification object:nil];
|
||||
} else {
|
||||
[nfc removeObserver:self name:UIApplicationDidBecomeActiveNotification object:nil];
|
||||
[notificationCenter removeObserver:self name:UIApplicationDidBecomeActiveNotification object:nil];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,56 +154,80 @@
|
||||
- (void)finishStep:(ORKActiveStepViewController *)viewController {
|
||||
}
|
||||
|
||||
- (void)setNeedsUpdateConstraints {
|
||||
[NSLayoutConstraint deactivateConstraints:[self constraints]];
|
||||
[super setNeedsUpdateConstraints];
|
||||
static const CGFloat CountDownLabelToButtonMargin = 2.0;
|
||||
|
||||
- (void)setUpConstraints {
|
||||
NSDictionary *views = NSDictionaryOfVariableBindings(_countDownLabel, _startTimerButton);
|
||||
ORKEnableAutoLayoutForViews(views.allValues);
|
||||
|
||||
NSMutableArray *constraints = [NSMutableArray new];
|
||||
|
||||
for (UIView *view in views.allValues) {
|
||||
[constraints addObject:[NSLayoutConstraint constraintWithItem:view
|
||||
attribute:NSLayoutAttributeWidth
|
||||
relatedBy:NSLayoutRelationLessThanOrEqual
|
||||
toItem:self
|
||||
attribute:NSLayoutAttributeWidth
|
||||
multiplier:1.0
|
||||
constant:0.0]];
|
||||
[constraints addObject:[NSLayoutConstraint constraintWithItem:view
|
||||
attribute:NSLayoutAttributeCenterX
|
||||
relatedBy:NSLayoutRelationLessThanOrEqual
|
||||
toItem:self
|
||||
attribute:NSLayoutAttributeCenterX
|
||||
multiplier:1.0
|
||||
constant:0.0]];
|
||||
}
|
||||
|
||||
[constraints addObject:[NSLayoutConstraint constraintWithItem:self
|
||||
attribute:NSLayoutAttributeTop
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:_countDownLabel
|
||||
attribute:NSLayoutAttributeTop
|
||||
multiplier:1.0
|
||||
constant:0.0]];
|
||||
|
||||
_countDownLabelBottomToStartTimerButtonTopConstraint = [NSLayoutConstraint constraintWithItem:_startTimerButton
|
||||
attribute:NSLayoutAttributeTop
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:_countDownLabel
|
||||
attribute:NSLayoutAttributeBottom
|
||||
multiplier:1.0
|
||||
constant:CountDownLabelToButtonMargin];
|
||||
[constraints addObject:_countDownLabelBottomToStartTimerButtonTopConstraint];
|
||||
|
||||
[constraints addObject:[NSLayoutConstraint constraintWithItem:_startTimerButton
|
||||
attribute:NSLayoutAttributeBottom
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:self
|
||||
attribute:NSLayoutAttributeBottom
|
||||
multiplier:1.0
|
||||
constant:0.0]];
|
||||
|
||||
[NSLayoutConstraint activateConstraints:constraints];
|
||||
|
||||
_countDownLabelZeroHeightConstraint = [NSLayoutConstraint constraintWithItem:_countDownLabel
|
||||
attribute:NSLayoutAttributeHeight
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:nil
|
||||
attribute:NSLayoutAttributeNotAnAttribute
|
||||
multiplier:1.0
|
||||
constant:0.0];
|
||||
_startTimerButtonZeroHeightConstraint = [NSLayoutConstraint constraintWithItem:_startTimerButton
|
||||
attribute:NSLayoutAttributeHeight
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:nil
|
||||
attribute:NSLayoutAttributeNotAnAttribute
|
||||
multiplier:1.0
|
||||
constant:0.0];
|
||||
[self setNeedsUpdateConstraints];
|
||||
}
|
||||
|
||||
- (void)updateConstraints {
|
||||
NSDictionary *dictionary = NSDictionaryOfVariableBindings(_countDownLabel, _startTimerButton);
|
||||
NSDictionary *metrics = @{@"CS" : @(2)};
|
||||
ORKEnableAutoLayoutForViews([dictionary allValues]);
|
||||
|
||||
for (UIView *view in [dictionary allValues]) {
|
||||
[self addConstraint:[NSLayoutConstraint
|
||||
constraintWithItem:view
|
||||
attribute:NSLayoutAttributeWidth
|
||||
relatedBy:NSLayoutRelationLessThanOrEqual
|
||||
toItem:self
|
||||
attribute:NSLayoutAttributeWidth
|
||||
multiplier:1
|
||||
constant:0]];
|
||||
[self addConstraint:[NSLayoutConstraint
|
||||
constraintWithItem:view
|
||||
attribute:NSLayoutAttributeCenterX
|
||||
relatedBy:NSLayoutRelationLessThanOrEqual
|
||||
toItem:self
|
||||
attribute:NSLayoutAttributeCenterX
|
||||
multiplier:1
|
||||
constant:0]];
|
||||
}
|
||||
|
||||
if (! _countDownLabel.hidden) {
|
||||
NSMutableString *verticalLayout = [NSMutableString new];
|
||||
[verticalLayout appendString:@"V:|[_countDownLabel]"];
|
||||
if (! _startTimerButton.hidden) {
|
||||
[verticalLayout appendString:@"-CS-[_startTimerButton]"];
|
||||
}
|
||||
[verticalLayout appendString:@"|"];
|
||||
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:verticalLayout
|
||||
options:NSLayoutFormatAlignAllCenterX
|
||||
metrics:metrics
|
||||
views:dictionary]];
|
||||
} else {
|
||||
[self addConstraint:[NSLayoutConstraint constraintWithItem:self
|
||||
attribute:NSLayoutAttributeHeight
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:nil
|
||||
attribute:NSLayoutAttributeNotAnAttribute
|
||||
multiplier:1
|
||||
constant:0]];
|
||||
}
|
||||
_countDownLabelZeroHeightConstraint.active = _countDownLabel.hidden;
|
||||
_startTimerButtonZeroHeightConstraint.active = (_countDownLabel.hidden || _startTimerButton.hidden);
|
||||
_countDownLabelBottomToStartTimerButtonTopConstraint.constant =
|
||||
(_countDownLabel.hidden || _startTimerButton.hidden) ? 0.0 : CountDownLabelToButtonMargin;
|
||||
[super updateConstraints];
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
*/
|
||||
|
||||
|
||||
@import UIKit;
|
||||
#import <ResearchKit/ORKStepViewController.h>
|
||||
#import <ResearchKit/ORKRecorder.h>
|
||||
|
||||
|
||||
@@ -30,23 +30,27 @@
|
||||
|
||||
|
||||
#import "ORKActiveStepViewController.h"
|
||||
#import "ORKVoiceEngine.h"
|
||||
#import "ORKSkin.h"
|
||||
#import "ORKHelpers.h"
|
||||
#import "ORKActiveStep.h"
|
||||
#import "ORKTask.h"
|
||||
#import "ORKTaskViewController.h"
|
||||
#import "ORKVerticalContainerView.h"
|
||||
#import "ORKStepViewController_Internal.h"
|
||||
#import "ORKActiveStepViewController_Internal.h"
|
||||
#import "ORKActiveStep_Internal.h"
|
||||
#import "ORKRecorder_Internal.h"
|
||||
#import "ORKTaskViewController_Internal.h"
|
||||
#import "ORKActiveStepTimerView.h"
|
||||
|
||||
#import "ORKActiveStepTimer.h"
|
||||
#import "ORKAccessibility.h"
|
||||
#import "ORKStepHeaderView_Internal.h"
|
||||
#import "ORKActiveStepTimerView.h"
|
||||
#import "ORKActiveStepView.h"
|
||||
#import "ORKNavigationContainerView.h"
|
||||
#import "ORKStepHeaderView_Internal.h"
|
||||
#import "ORKVerticalContainerView.h"
|
||||
#import "ORKVoiceEngine.h"
|
||||
|
||||
#import "ORKActiveStepViewController_Internal.h"
|
||||
#import "ORKStepViewController_Internal.h"
|
||||
#import "ORKTaskViewController_Internal.h"
|
||||
#import "ORKRecorder_Internal.h"
|
||||
|
||||
#import "ORKActiveStep_Internal.h"
|
||||
#import "ORKResult.h"
|
||||
#import "ORKTask.h"
|
||||
|
||||
#import "ORKAccessibility.h"
|
||||
#import "ORKHelpers_Internal.h"
|
||||
#import "ORKSkin.h"
|
||||
|
||||
|
||||
@interface ORKActiveStepViewController () {
|
||||
@@ -57,6 +61,7 @@
|
||||
|
||||
SystemSoundID _alertSound;
|
||||
NSURL *_alertSoundURL;
|
||||
BOOL _hasSpokenHalfwayCountdown;
|
||||
}
|
||||
|
||||
@property (nonatomic, strong) NSArray *recorders;
|
||||
@@ -92,6 +97,7 @@
|
||||
}
|
||||
|
||||
- (ORKActiveStep *)activeStep {
|
||||
NSAssert(self.step == nil || [self.step isKindOfClass:[ORKActiveStep class]], @"Step should be a subclass of an ORKActiveStep");
|
||||
return (ORKActiveStep *)self.step;
|
||||
}
|
||||
|
||||
@@ -103,7 +109,7 @@
|
||||
[super viewDidLoad];
|
||||
|
||||
_activeStepView = [[ORKActiveStepView alloc] initWithFrame:self.view.bounds];
|
||||
[_activeStepView setTranslatesAutoresizingMaskIntoConstraints:NO];
|
||||
_activeStepView.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
[_activeStepView setCustomView:_customView];
|
||||
[self updateContinueButtonItem];
|
||||
_activeStepView.headerView.learnMoreButtonItem = self.learnMoreButtonItem;
|
||||
@@ -111,9 +117,17 @@
|
||||
_activeStepView.continueSkipContainer.continueEnabled = _finished;
|
||||
[self.view addSubview:_activeStepView];
|
||||
|
||||
NSMutableArray *constraints = [NSMutableArray arrayWithArray:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[s]|" options:0 metrics:nil views:@{@"s":_activeStepView}]];
|
||||
[constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[tg][s]|" options:0 metrics:nil views:@{@"s":_activeStepView,@"tg":self.topLayoutGuide}]];
|
||||
[self.view addConstraints:constraints];
|
||||
NSMutableArray *constraints = [NSMutableArray new];
|
||||
[constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[activeStepView]|"
|
||||
options:(NSLayoutFormatOptions)0
|
||||
metrics:nil
|
||||
views:@{@"activeStepView": _activeStepView}]];
|
||||
[constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[topLayoutGuide][activeStepView]|"
|
||||
options:(NSLayoutFormatOptions)0
|
||||
metrics:nil
|
||||
views:@{@"activeStepView": _activeStepView,
|
||||
@"topLayoutGuide": self.topLayoutGuide}]];
|
||||
[NSLayoutConstraint activateConstraints:constraints];
|
||||
|
||||
[self prepareStep];
|
||||
}
|
||||
@@ -155,7 +169,10 @@
|
||||
|
||||
// Wait for animation complete
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if ([[self activeStep] shouldStartTimerAutomatically]) {
|
||||
if(self.started){
|
||||
// Should call resume instead of start when the task has been started.
|
||||
[self resume];
|
||||
} else if ([[self activeStep] shouldStartTimerAutomatically]) {
|
||||
[self start];
|
||||
}
|
||||
});
|
||||
@@ -194,7 +211,9 @@
|
||||
|
||||
- (ORKStepResult *)result {
|
||||
ORKStepResult *sResult = [super result];
|
||||
sResult.results = _recorderResults;
|
||||
if (_recorderResults) {
|
||||
sResult.results = [sResult.results arrayByAddingObjectsFromArray:_recorderResults] ? : _recorderResults;
|
||||
}
|
||||
return sResult;
|
||||
}
|
||||
|
||||
@@ -313,7 +332,7 @@
|
||||
|
||||
- (void)suspend {
|
||||
ORK_Log_Debug(@"%@",self);
|
||||
if (self.finished || ! self.started) {
|
||||
if (self.finished || !self.started) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -325,7 +344,7 @@
|
||||
|
||||
- (void)resume {
|
||||
ORK_Log_Debug(@"%@",self);
|
||||
if (self.finished || ! self.started) {
|
||||
if (self.finished || !self.started) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -351,7 +370,10 @@
|
||||
if (self.activeStep.shouldVibrateOnFinish) {
|
||||
AudioServicesPlayAlertSound(kSystemSoundID_Vibrate);
|
||||
}
|
||||
if (! self.activeStep.startsFinished) {
|
||||
if (self.activeStep.hasVoice && self.activeStep.finishedSpokenInstruction) {
|
||||
[[ORKVoiceEngine sharedVoiceEngine] speakText:self.activeStep.finishedSpokenInstruction];
|
||||
}
|
||||
if (!self.activeStep.startsFinished) {
|
||||
if (self.activeStep.shouldContinueOnFinish) {
|
||||
[self goForward];
|
||||
}
|
||||
@@ -379,12 +401,12 @@
|
||||
NSTimeInterval stepDuration = self.activeStep.stepDuration;
|
||||
|
||||
if (stepDuration > 0) {
|
||||
__weak typeof(self) weakSelf = self;
|
||||
ORKWeakTypeOf(self) weakSelf = self;
|
||||
_activeStepTimer = [[ORKActiveStepTimer alloc] initWithDuration:stepDuration
|
||||
interval:_timerUpdateInterval
|
||||
runtime:0
|
||||
handler:^(ORKActiveStepTimer *timer, BOOL finished) {
|
||||
typeof(self) strongSelf = weakSelf;
|
||||
ORKStrongTypeOf(self) strongSelf = weakSelf;
|
||||
[strongSelf countDownTimerFired:timer finished:finished];
|
||||
}];
|
||||
[_activeStepTimer resume];
|
||||
@@ -399,6 +421,7 @@
|
||||
ORKActiveStepCustomView *customView = _activeStepView.activeCustomView;
|
||||
[customView updateDisplay:self];
|
||||
|
||||
|
||||
ORKVoiceEngine *voice = [ORKVoiceEngine sharedVoiceEngine];
|
||||
|
||||
if (!finished && self.activeStep.shouldSpeakCountDown) {
|
||||
@@ -412,6 +435,13 @@
|
||||
[voice speakInt:countDownValue];
|
||||
}
|
||||
}
|
||||
|
||||
BOOL isHalfway = !_hasSpokenHalfwayCountdown && timer.runtime > timer.duration / 2.0;
|
||||
if (!finished && self.activeStep.shouldSpeakRemainingTimeAtHalfway && !UIAccessibilityIsVoiceOverRunning() && isHalfway) {
|
||||
_hasSpokenHalfwayCountdown = YES;
|
||||
NSString *text = [NSString localizedStringWithFormat:ORKLocalizedString(@"COUNTDOWN_SPOKEN_REMAINING_%@", nil), @(countDownValue)];
|
||||
[voice speakText:text];
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)timerActive {
|
||||
@@ -439,7 +469,7 @@
|
||||
|
||||
- (void)recorder:(ORKRecorder *)recorder didFailWithError:(NSError *)error {
|
||||
if (error) {
|
||||
STRONGTYPE(self.delegate) strongDelegate = self.delegate;
|
||||
ORKStrongTypeOf(self.delegate) strongDelegate = self.delegate;
|
||||
if ([strongDelegate respondsToSelector:@selector(stepViewController:recorder:didFailWithError:)]) {
|
||||
[strongDelegate stepViewController:self recorder:recorder didFailWithError:error];
|
||||
}
|
||||
|
||||
@@ -30,11 +30,11 @@
|
||||
|
||||
|
||||
#import "ORKActiveStepViewController.h"
|
||||
#import "ORKActiveStepTimer.h"
|
||||
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class ORKActiveStepTimer;
|
||||
@class ORKActiveStepView;
|
||||
|
||||
@interface ORKActiveStepViewController ()
|
||||
|
||||
@@ -29,14 +29,14 @@
|
||||
*/
|
||||
|
||||
|
||||
#import <ResearchKit/ORKActiveStep.h>
|
||||
#import "ORKActiveStep.h"
|
||||
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface ORKActiveStep ()
|
||||
|
||||
/**
|
||||
Convenience methods.
|
||||
*/
|
||||
// Convenience methods.
|
||||
- (BOOL)startsFinished;
|
||||
- (BOOL)hasCountDown;
|
||||
- (BOOL)hasTitle;
|
||||
@@ -44,3 +44,5 @@
|
||||
- (BOOL)hasVoice;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
*/
|
||||
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
@import UIKit;
|
||||
#import "ORKCustomStepView_Internal.h"
|
||||
|
||||
|
||||
|
||||
@@ -30,11 +30,13 @@
|
||||
|
||||
|
||||
#import "ORKAudioContentView.h"
|
||||
#import "ORKHelpers.h"
|
||||
#import "ORKSkin.h"
|
||||
#import "ORKLabel.h"
|
||||
|
||||
#import "ORKHeadlineLabel.h"
|
||||
#import "ORKLabel.h"
|
||||
|
||||
#import "ORKAccessibility.h"
|
||||
#import "ORKHelpers_Internal.h"
|
||||
#import "ORKSkin.h"
|
||||
|
||||
|
||||
// The central blue region.
|
||||
@@ -56,22 +58,38 @@ static const CGFloat GraphViewRedZoneHeight = 25;
|
||||
@end
|
||||
|
||||
|
||||
static const CGFloat kValueLineWidth = 4.5;
|
||||
static const CGFloat kValueLineMargin = 1.5;
|
||||
static const CGFloat ValueLineWidth = 4.5;
|
||||
static const CGFloat ValueLineMargin = 1.5;
|
||||
|
||||
|
||||
@implementation ORKAudioGraphView
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
[self setUpConstraints];
|
||||
|
||||
#if TARGET_IPHONE_SIMULATOR
|
||||
_values = @[@(0.2),@(0.6),@(0.55), @(0.1), @(0.75), @(0.7)];
|
||||
_values = @[ @(0.2), @(0.6), @(0.55), @(0.1), @(0.75), @(0.7) ];
|
||||
#endif
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setUpConstraints {
|
||||
|
||||
NSLayoutConstraint *heightConstraint = [NSLayoutConstraint constraintWithItem:self
|
||||
attribute:NSLayoutAttributeHeight
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:nil
|
||||
attribute:NSLayoutAttributeNotAnAttribute
|
||||
multiplier:1.0
|
||||
constant:CGFLOAT_MAX];
|
||||
heightConstraint.priority = UILayoutPriorityFittingSizeLevel;
|
||||
|
||||
[NSLayoutConstraint activateConstraints:@[heightConstraint]];
|
||||
}
|
||||
|
||||
- (void)setValues:(NSArray *)values {
|
||||
_values = [values copy];
|
||||
[self setNeedsDisplay];
|
||||
@@ -99,41 +117,41 @@ static const CGFloat kValueLineMargin = 1.5;
|
||||
CGContextSetFillColorWithColor(context, [UIColor whiteColor].CGColor);
|
||||
CGContextFillRect(context, bounds);
|
||||
|
||||
CGFloat scale = [self.window.screen scale];
|
||||
CGFloat scale = self.window.screen.scale;
|
||||
|
||||
CGFloat midY = CGRectGetMidY(bounds);
|
||||
CGFloat maxX = CGRectGetMaxX(bounds);
|
||||
CGFloat halfHeight = bounds.size.height/2;
|
||||
CGFloat halfHeight = bounds.size.height / 2;
|
||||
CGContextSaveGState(context);
|
||||
{
|
||||
UIBezierPath *centerLine = [UIBezierPath new];
|
||||
[centerLine moveToPoint:(CGPoint){.x=0,.y=midY}];
|
||||
[centerLine addLineToPoint:(CGPoint){.x=maxX,.y=midY}];
|
||||
[centerLine moveToPoint:(CGPoint){.x = 0, .y = midY}];
|
||||
[centerLine addLineToPoint:(CGPoint){.x = maxX, .y = midY}];
|
||||
|
||||
CGContextSetLineWidth(context, 1/scale);
|
||||
CGContextSetLineWidth(context, 1.0 / scale);
|
||||
[_keyColor setStroke];
|
||||
CGFloat lengths[2] = {3,3};
|
||||
CGFloat lengths[2] = {3, 3};
|
||||
CGContextSetLineDash(context, 0, lengths, 2);
|
||||
|
||||
[centerLine stroke];
|
||||
}
|
||||
CGContextRestoreGState(context);
|
||||
|
||||
CGFloat lineStep = kValueLineMargin + kValueLineWidth;
|
||||
CGFloat lineStep = ValueLineMargin + ValueLineWidth;
|
||||
|
||||
CGContextSaveGState(context);
|
||||
{
|
||||
CGFloat x = maxX - lineStep/2;
|
||||
CGContextSetLineWidth(context, kValueLineWidth);
|
||||
CGFloat x = maxX - lineStep / 2;
|
||||
CGContextSetLineWidth(context, ValueLineWidth);
|
||||
CGContextSetLineCap(context, kCGLineCapRound);
|
||||
|
||||
UIBezierPath *path1 = [UIBezierPath new];
|
||||
path1.lineCapStyle = kCGLineCapRound;
|
||||
path1.lineWidth = kValueLineWidth;
|
||||
path1.lineWidth = ValueLineWidth;
|
||||
UIBezierPath *path2 = [path1 copy];
|
||||
|
||||
for (NSNumber *value in [_values reverseObjectEnumerator]) {
|
||||
CGFloat floatValue = [value doubleValue];
|
||||
CGFloat floatValue = value.doubleValue;
|
||||
|
||||
UIBezierPath *path = nil;
|
||||
if (floatValue > _alertThreshold) {
|
||||
@@ -143,8 +161,8 @@ static const CGFloat kValueLineMargin = 1.5;
|
||||
path = path2;
|
||||
[_keyColor setStroke];
|
||||
}
|
||||
[path moveToPoint:(CGPoint){.x=x,.y=midY-floatValue*halfHeight}];
|
||||
[path addLineToPoint:(CGPoint){.x=x,.y=midY+floatValue*halfHeight}];
|
||||
[path moveToPoint:(CGPoint){.x = x, .y = midY - floatValue*halfHeight}];
|
||||
[path addLineToPoint:(CGPoint){.x = x, .y = midY + floatValue*halfHeight}];
|
||||
|
||||
x -= lineStep;
|
||||
|
||||
@@ -177,7 +195,7 @@ static const CGFloat kValueLineMargin = 1.5;
|
||||
+ (UIFont *)defaultFont {
|
||||
UIFontDescriptor *descriptor = [UIFontDescriptor preferredFontDescriptorWithTextStyle:UIFontTextStyleSubheadline];
|
||||
UIFontDescriptor *alternativeDescriptor = ORKFontDescriptorForLightStylisticAlternative(descriptor);
|
||||
return [UIFont fontWithDescriptor:alternativeDescriptor size:[alternativeDescriptor pointSize]+4];
|
||||
return [UIFont fontWithDescriptor:alternativeDescriptor size:[alternativeDescriptor pointSize] + 4];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -193,7 +211,6 @@ static const CGFloat kValueLineMargin = 1.5;
|
||||
|
||||
|
||||
@implementation ORKAudioContentView {
|
||||
NSArray *_constraints;
|
||||
NSMutableArray *_samples;
|
||||
UIColor *_keyColor;
|
||||
}
|
||||
@@ -221,11 +238,11 @@ static const CGFloat kValueLineMargin = 1.5;
|
||||
_timerLabel.text = @"06:00";
|
||||
_alertLabel.text = ORKLocalizedString(@"AUDIO_TOO_LOUD_LABEL", nil);
|
||||
|
||||
self.alertThreshold = GraphViewBlueZoneHeight/(GraphViewRedZoneHeight*2+GraphViewBlueZoneHeight);
|
||||
self.alertThreshold = GraphViewBlueZoneHeight / ((GraphViewRedZoneHeight * 2) + GraphViewBlueZoneHeight);
|
||||
|
||||
[self updateGraphSamples];
|
||||
[self applyKeyColor];
|
||||
[self setNeedsUpdateConstraints];
|
||||
[self setUpConstraints];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@@ -266,27 +283,21 @@ static const CGFloat kValueLineMargin = 1.5;
|
||||
_graphView.alertColor = alertColor;
|
||||
}
|
||||
|
||||
- (void)updateConstraints {
|
||||
if ([_constraints count]) {
|
||||
[NSLayoutConstraint deactivateConstraints:_constraints];
|
||||
_constraints = nil;
|
||||
}
|
||||
|
||||
- (void)setUpConstraints {
|
||||
NSMutableArray *constraints = [NSMutableArray array];
|
||||
|
||||
NSDictionary *views = NSDictionaryOfVariableBindings(_timerLabel, _alertLabel, _graphView);
|
||||
[constraints addObjectsFromArray:
|
||||
[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[_graphView]-[_alertLabel]|"
|
||||
options:0
|
||||
metrics:nil
|
||||
views:views]];
|
||||
[constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[_graphView]-[_alertLabel]|"
|
||||
options:(NSLayoutFormatOptions)0
|
||||
metrics:nil
|
||||
views:views]];
|
||||
[constraints addObject:[NSLayoutConstraint constraintWithItem:_alertLabel
|
||||
attribute:NSLayoutAttributeCenterX
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:self
|
||||
attribute:NSLayoutAttributeCenterX
|
||||
multiplier:1
|
||||
constant:0]];
|
||||
multiplier:1.0
|
||||
constant:0.0]];
|
||||
|
||||
const CGFloat sideMargin = self.layoutMargins.left + (2 * ORKStandardLeftMarginForTableViewCell(self));
|
||||
const CGFloat innerMargin = 2;
|
||||
@@ -297,18 +308,15 @@ static const CGFloat kValueLineMargin = 1.5;
|
||||
metrics:@{@"sideMargin": @(sideMargin), @"innerMargin": @(innerMargin)}
|
||||
views:views]];
|
||||
|
||||
|
||||
[constraints addObject:[NSLayoutConstraint constraintWithItem:_graphView
|
||||
attribute:NSLayoutAttributeHeight
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:nil
|
||||
attribute:NSLayoutAttributeNotAnAttribute
|
||||
multiplier:1
|
||||
constant:(GraphViewBlueZoneHeight+GraphViewRedZoneHeight*2)]];
|
||||
multiplier:1.0
|
||||
constant:(GraphViewBlueZoneHeight + GraphViewRedZoneHeight * 2)]];
|
||||
|
||||
_constraints = constraints;
|
||||
[NSLayoutConstraint activateConstraints:constraints];
|
||||
[super updateConstraints];
|
||||
}
|
||||
|
||||
- (void)setAlertThreshold:(CGFloat)alertThreshold {
|
||||
@@ -323,17 +331,16 @@ static const CGFloat kValueLineMargin = 1.5;
|
||||
}
|
||||
|
||||
- (void)updateTimerLabel {
|
||||
static NSDateComponentsFormatter *_formatter = nil;
|
||||
static NSDateComponentsFormatter *formatter = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
NSDateComponentsFormatter *formatter = [NSDateComponentsFormatter new];
|
||||
formatter.unitsStyle = NSDateComponentsFormatterUnitsStylePositional;
|
||||
formatter.zeroFormattingBehavior = NSDateComponentsFormatterZeroFormattingBehaviorPad;
|
||||
formatter.allowedUnits = NSCalendarUnitMinute | NSCalendarUnitSecond;
|
||||
_formatter = formatter;
|
||||
});
|
||||
|
||||
NSString *string = [_formatter stringFromTimeInterval:MAX(round(_timeLeft),0)];
|
||||
NSString *string = [formatter stringFromTimeInterval:MAX(round(_timeLeft),0)];
|
||||
_timerLabel.text = string;
|
||||
_timerLabel.hidden = (string == nil);
|
||||
}
|
||||
@@ -344,8 +351,12 @@ static const CGFloat kValueLineMargin = 1.5;
|
||||
}
|
||||
|
||||
- (void)updateAlertLabelHidden {
|
||||
NSNumber *sample = [_samples lastObject];
|
||||
BOOL show = (! _finished && ([sample doubleValue] > _alertThreshold)) || _failed;
|
||||
NSNumber *sample = _samples.lastObject;
|
||||
BOOL show = (!_finished && (sample.doubleValue > _alertThreshold)) || _failed;
|
||||
|
||||
if (_alertLabel.hidden && show) {
|
||||
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, _alertLabel.text);
|
||||
}
|
||||
_alertLabel.hidden = !show;
|
||||
}
|
||||
|
||||
@@ -356,13 +367,13 @@ static const CGFloat kValueLineMargin = 1.5;
|
||||
|
||||
- (void)addSample:(NSNumber *)sample {
|
||||
NSAssert(sample != nil, @"Sample should be non-nil");
|
||||
if (! _samples) {
|
||||
if (!_samples) {
|
||||
_samples = [NSMutableArray array];
|
||||
}
|
||||
[_samples addObject:sample];
|
||||
// Try to keep around 250 samples
|
||||
if ([_samples count] > 500) {
|
||||
_samples = [[_samples subarrayWithRange:(NSRange){250,_samples.count-250}] mutableCopy];
|
||||
if (_samples.count > 500) {
|
||||
_samples = [[_samples subarrayWithRange:(NSRange){250, _samples.count - 250}] mutableCopy];
|
||||
}
|
||||
[self updateGraphSamples];
|
||||
}
|
||||
@@ -379,11 +390,9 @@ static const CGFloat kValueLineMargin = 1.5;
|
||||
}
|
||||
|
||||
- (NSString *)accessibilityLabel {
|
||||
if (_alertLabel.isHidden) {
|
||||
return _timerLabel.accessibilityLabel;
|
||||
}
|
||||
|
||||
return ORKAccessibilityStringForVariables(_timerLabel.accessibilityLabel, _alertLabel.accessibilityLabel);
|
||||
NSString *timerAxString = _timerLabel.isHidden ? nil : _timerLabel.accessibilityLabel;
|
||||
NSString *alertAxString = _alertLabel.isHidden ? nil : _alertLabel.accessibilityLabel;
|
||||
return ORKAccessibilityStringForVariables(ORKLocalizedString(@"AX_AUDIO_BAR_GRAPH", nil), timerAxString, alertAxString);
|
||||
}
|
||||
|
||||
- (UIAccessibilityTraits)accessibilityTraits {
|
||||
|
||||
@@ -49,8 +49,11 @@
|
||||
3. This notice may not be removed or altered from any source distribution.
|
||||
*/
|
||||
|
||||
#import <ResearchKit/ORKRecorder.h>
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
|
||||
@import UIKit;
|
||||
@import AVFoundation;
|
||||
#import "ORKTypes.h"
|
||||
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
|
||||
@@ -49,15 +49,15 @@
|
||||
3. This notice may not be removed or altered from any source distribution.
|
||||
*/
|
||||
|
||||
|
||||
#import "ORKAudioGenerator.h"
|
||||
|
||||
@import AudioToolbox;
|
||||
|
||||
@interface ORKAudioGenerator () {
|
||||
@public
|
||||
AudioComponentInstance _toneUnit;
|
||||
|
||||
@public
|
||||
@interface ORKAudioGenerator () {
|
||||
@public
|
||||
AudioComponentInstance _toneUnit;
|
||||
double _frequency;
|
||||
double _theta;
|
||||
ORKAudioChannel _activeChannel;
|
||||
@@ -103,8 +103,7 @@ OSStatus ORKAudioGeneratorRenderTone(void *inRefCon,
|
||||
bufferActive[frame] = bufferValue;
|
||||
if (audioGenerator->_playsStereo) {
|
||||
bufferNonActive[frame] = bufferValue;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
bufferNonActive[frame] = 0;
|
||||
}
|
||||
|
||||
@@ -113,7 +112,7 @@ OSStatus ORKAudioGeneratorRenderTone(void *inRefCon,
|
||||
theta -= 2.0 * M_PI;
|
||||
}
|
||||
|
||||
fadeInFactor += 1/(ORKSineWaveToneGeneratorSampleRateDefault * audioGenerator->_fadeInDuration);
|
||||
fadeInFactor += 1.0 / (ORKSineWaveToneGeneratorSampleRateDefault * audioGenerator->_fadeInDuration);
|
||||
if (fadeInFactor >= 1) {
|
||||
fadeInFactor = 1;
|
||||
}
|
||||
@@ -149,15 +148,15 @@ OSStatus ORKAudioGeneratorRenderTone(void *inRefCon,
|
||||
|
||||
- (void)applicationDidBecomeActive:(NSNotification *)notification {
|
||||
if (_toneUnit) {
|
||||
__unused OSErr err = AudioOutputUnitStart(_toneUnit);
|
||||
NSAssert1(err == noErr, @"Error starting unit: %hd", err);
|
||||
__unused OSErr error = AudioOutputUnitStart(_toneUnit);
|
||||
NSAssert1(error == noErr, @"Error starting unit: %hd", error);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)applicationWillResignActive:(NSNotification *)notification {
|
||||
if (_toneUnit) {
|
||||
__unused OSErr err = AudioOutputUnitStop(_toneUnit);
|
||||
NSAssert1(err == noErr, @"Error stopping unit: %hd", err);
|
||||
__unused OSErr error = AudioOutputUnitStop(_toneUnit);
|
||||
NSAssert1(error == noErr, @"Error stopping unit: %hd", error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,12 +194,12 @@ OSStatus ORKAudioGeneratorRenderTone(void *inRefCon,
|
||||
[self createToneUnit];
|
||||
|
||||
// Stop changing parameters on the unit
|
||||
OSErr err = AudioUnitInitialize(_toneUnit);
|
||||
NSAssert1(err == noErr, @"Error initializing unit: %hd", err);
|
||||
OSErr error = AudioUnitInitialize(_toneUnit);
|
||||
NSAssert1(error == noErr, @"Error initializing unit: %hd", error);
|
||||
|
||||
// Start playback
|
||||
err = AudioOutputUnitStart(_toneUnit);
|
||||
NSAssert1(err == noErr, @"Error starting unit: %hd", err);
|
||||
error = AudioOutputUnitStart(_toneUnit);
|
||||
NSAssert1(error == noErr, @"Error starting unit: %hd", error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,20 +240,20 @@ OSStatus ORKAudioGeneratorRenderTone(void *inRefCon,
|
||||
NSAssert(defaultOutput, @"Can't find default output");
|
||||
|
||||
// Create a new unit based on this that we'll use for output
|
||||
OSErr err = AudioComponentInstanceNew(defaultOutput, &_toneUnit);
|
||||
NSAssert1(_toneUnit, @"Error creating unit: %hd", err);
|
||||
OSErr error = AudioComponentInstanceNew(defaultOutput, &_toneUnit);
|
||||
NSAssert1(_toneUnit, @"Error creating unit: %hd", error);
|
||||
|
||||
// Set our tone rendering function on the unit
|
||||
AURenderCallbackStruct input;
|
||||
input.inputProc = ORKAudioGeneratorRenderTone;
|
||||
input.inputProcRefCon = (__bridge void *)(self);
|
||||
err = AudioUnitSetProperty(_toneUnit,
|
||||
error = AudioUnitSetProperty(_toneUnit,
|
||||
kAudioUnitProperty_SetRenderCallback,
|
||||
kAudioUnitScope_Input,
|
||||
0,
|
||||
&input,
|
||||
sizeof(input));
|
||||
NSAssert1(err == noErr, @"Error setting callback: %hd", err);
|
||||
NSAssert1(error == noErr, @"Error setting callback: %hd", error);
|
||||
|
||||
// Set the format to 32 bit, single channel, floating point, linear PCM
|
||||
const int four_bytes_per_float = 4;
|
||||
@@ -268,13 +267,13 @@ OSStatus ORKAudioGeneratorRenderTone(void *inRefCon,
|
||||
streamFormat.mBytesPerFrame = four_bytes_per_float;
|
||||
streamFormat.mChannelsPerFrame = 2;
|
||||
streamFormat.mBitsPerChannel = four_bytes_per_float * eight_bits_per_byte;
|
||||
err = AudioUnitSetProperty (_toneUnit,
|
||||
error = AudioUnitSetProperty (_toneUnit,
|
||||
kAudioUnitProperty_StreamFormat,
|
||||
kAudioUnitScope_Input,
|
||||
0,
|
||||
&streamFormat,
|
||||
sizeof(AudioStreamBasicDescription));
|
||||
NSAssert1(err == noErr, @"Error setting stream format: %hd", err);
|
||||
NSAssert1(error == noErr, @"Error setting stream format: %hd", error);
|
||||
}
|
||||
|
||||
- (void)handleInterruption:(id)sender {
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
Copyright (c) 2016, Sage Bionetworks
|
||||
Copyright (c) 2016, Apple Inc.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
|
||||
#import <ResearchKit/ORKStepNavigationRule.h>
|
||||
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
ORK_CLASS_AVAILABLE
|
||||
@interface ORKAudioLevelNavigationRule : ORKStepNavigationRule
|
||||
|
||||
/*
|
||||
The `init` and `new` methods are unavailable.
|
||||
|
||||
`ORKStepNavigationRule` classes should be initialized with custom designated initializers on each
|
||||
subclass.
|
||||
*/
|
||||
+ (instancetype)new NS_UNAVAILABLE;
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
|
||||
/**
|
||||
Returns an initialized direct-step navigation rule using the specified destination step identifier.
|
||||
|
||||
@param audioLevelStepIdentifier The identifier of the step with the audio file to check.
|
||||
@param destinationStepIdentifier The identifier of the destination step if audio test passes.
|
||||
@param recordingSettings Use key AVNumberOfChannelsKey to sepcify the number of recording channels.
|
||||
@return A audio level step navigation rule.
|
||||
*/
|
||||
- (instancetype)initWithAudioLevelStepIdentifier:(NSString *)audioLevelStepIdentifier
|
||||
destinationStepIdentifier:(NSString *)destinationStepIdentifier
|
||||
recordingSettings:(NSDictionary *)recordingSettings NS_DESIGNATED_INITIALIZER;
|
||||
|
||||
/**
|
||||
Returns a new direct-step navigation rule initialized from data in a given unarchiver.
|
||||
|
||||
@param aDecoder The coder from which to initialize the step navigation rule.
|
||||
|
||||
@return A new direct-step navigation rule.
|
||||
*/
|
||||
- (instancetype)initWithCoder:(NSCoder *)aDecoder NS_DESIGNATED_INITIALIZER;
|
||||
|
||||
@property (nonatomic, copy, readonly) NSString *audioLevelStepIdentifier;
|
||||
@property (nonatomic, copy, readonly) NSString *destinationStepIdentifier;
|
||||
@property (nonatomic, copy, readonly) NSDictionary *recordingSettings;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,217 @@
|
||||
/*
|
||||
Copyright (c) 2016, Sage Bionetworks
|
||||
Copyright (c) 2016, Apple Inc.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
|
||||
#import "ORKAudioLevelNavigationRule.h"
|
||||
|
||||
#import "ORKResult.h"
|
||||
#import "ORKResultPredicate.h"
|
||||
#import "ORKStepNavigationRule_Internal.h"
|
||||
|
||||
#import "ORKHelpers_Internal.h"
|
||||
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
|
||||
|
||||
Float32 const VolumeThreshold = 0.45;
|
||||
UInt16 const LinearPCMBitDepth = 16;
|
||||
Float32 const MaxAmplitude = 32767.0;
|
||||
Float32 const VolumeClamp = 60.0;
|
||||
|
||||
|
||||
@interface ORKAudioLevelNavigationRule ()
|
||||
|
||||
@property (nonatomic, copy, readwrite) NSString *audioLevelStepIdentifier;
|
||||
@property (nonatomic, copy, readwrite) NSString *destinationStepIdentifier;
|
||||
@property (nonatomic, copy, readwrite) NSDictionary *recordingSettings;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation ORKAudioLevelNavigationRule
|
||||
|
||||
+ (instancetype)new {
|
||||
ORKThrowMethodUnavailableException();
|
||||
}
|
||||
|
||||
- (instancetype)init {
|
||||
ORKThrowMethodUnavailableException();
|
||||
}
|
||||
|
||||
- (instancetype)initWithAudioLevelStepIdentifier:(NSString *)audioLevelStepIdentifier
|
||||
destinationStepIdentifier:(NSString *)destinationStepIdentifier
|
||||
recordingSettings:(NSDictionary *)recordingSettings
|
||||
{
|
||||
ORKThrowInvalidArgumentExceptionIfNil(audioLevelStepIdentifier);
|
||||
ORKThrowInvalidArgumentExceptionIfNil(destinationStepIdentifier);
|
||||
ORKThrowInvalidArgumentExceptionIfNil(recordingSettings);
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_audioLevelStepIdentifier = [audioLevelStepIdentifier copy];
|
||||
_destinationStepIdentifier = [destinationStepIdentifier copy];
|
||||
_recordingSettings = [recordingSettings copy];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark NSSecureCoding
|
||||
|
||||
+ (BOOL)supportsSecureCoding {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
|
||||
self = [super initWithCoder:aDecoder];
|
||||
if (self) {
|
||||
ORK_DECODE_OBJ_CLASS(aDecoder, audioLevelStepIdentifier, NSString);
|
||||
ORK_DECODE_OBJ_CLASS(aDecoder, destinationStepIdentifier, NSString);
|
||||
ORK_DECODE_OBJ_CLASS(aDecoder, recordingSettings, NSDictionary);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)encodeWithCoder:(NSCoder *)aCoder {
|
||||
[super encodeWithCoder:aCoder];
|
||||
ORK_ENCODE_OBJ(aCoder, audioLevelStepIdentifier);
|
||||
ORK_ENCODE_OBJ(aCoder, destinationStepIdentifier);
|
||||
ORK_ENCODE_OBJ(aCoder, recordingSettings);
|
||||
}
|
||||
|
||||
#pragma mark NSCopying
|
||||
|
||||
- (instancetype)copyWithZone:(NSZone *)zone {
|
||||
typeof(self) rule = [[[self class] allocWithZone:zone] initWithAudioLevelStepIdentifier:self.audioLevelStepIdentifier destinationStepIdentifier:self.destinationStepIdentifier recordingSettings:self.recordingSettings];
|
||||
return rule;
|
||||
}
|
||||
|
||||
- (BOOL)isEqual:(id)object {
|
||||
BOOL isParentSame = [super isEqual:object];
|
||||
__typeof(self) castObject = object;
|
||||
return (isParentSame
|
||||
&& ORKEqualObjects(self.audioLevelStepIdentifier, castObject.audioLevelStepIdentifier)
|
||||
&& ORKEqualObjects(self.destinationStepIdentifier, castObject.destinationStepIdentifier)
|
||||
&& ORKEqualObjects(self.recordingSettings, castObject.recordingSettings));
|
||||
}
|
||||
|
||||
- (NSUInteger)hash {
|
||||
return _audioLevelStepIdentifier.hash ^ _destinationStepIdentifier.hash ^ _recordingSettings.hash;
|
||||
}
|
||||
|
||||
#pragma mark - Required overrides
|
||||
|
||||
- (NSString *)identifierForDestinationStepWithTaskResult:(ORKTaskResult *)taskResult {
|
||||
|
||||
// Get the result file
|
||||
ORKStepResult *stepResult = (ORKStepResult *)[taskResult resultForIdentifier:self.audioLevelStepIdentifier];
|
||||
ORKFileResult *audioLevelResult = (ORKFileResult *)[stepResult.results firstObject];
|
||||
|
||||
// Check the volume
|
||||
if ((audioLevelResult.fileURL != nil) && [self checkAudioLevelFromSoundFile:audioLevelResult.fileURL]) {
|
||||
// Returning nil will drop through to the next step (which should be the the step that has the instructions
|
||||
// for moving to a quieter room).
|
||||
return nil;
|
||||
}
|
||||
|
||||
return self.destinationStepIdentifier;
|
||||
}
|
||||
|
||||
- (BOOL)checkAudioLevelFromSoundFile:(NSURL *)fileURL {
|
||||
// Setup reader
|
||||
AVURLAsset *urlAsset = [AVURLAsset URLAssetWithURL:fileURL options:nil];
|
||||
if (urlAsset.tracks.count == 0) {
|
||||
NSLog(@"No tracks found for urlAsset: %@", fileURL);
|
||||
return NO;
|
||||
}
|
||||
|
||||
NSError *error = nil;
|
||||
AVAssetReader *reader = [[AVAssetReader alloc] initWithAsset:urlAsset error:&error];
|
||||
AVAssetTrack *track = [urlAsset.tracks objectAtIndex:0];
|
||||
NSDictionary *outputSettings = @{AVFormatIDKey: @(kAudioFormatLinearPCM),
|
||||
AVLinearPCMBitDepthKey: @(LinearPCMBitDepth),
|
||||
AVLinearPCMIsBigEndianKey: @(NO),
|
||||
AVLinearPCMIsFloatKey: @(NO),
|
||||
AVLinearPCMIsNonInterleaved: @(NO)};
|
||||
AVAssetReaderTrackOutput *output = [[AVAssetReaderTrackOutput alloc] initWithTrack:track outputSettings:outputSettings];
|
||||
[reader addOutput:output];
|
||||
|
||||
// Setup initial values - Assume 2 channels if not in recording settings
|
||||
const UInt32 channelCount = (UInt32)[self.recordingSettings[AVNumberOfChannelsKey] unsignedIntegerValue] ? : 2;
|
||||
const UInt32 bytesPerSample = 2 * channelCount;
|
||||
|
||||
// setup criteria block - Use a high-pass filter and a rolling average of the amplitude
|
||||
// normalized to be < 1
|
||||
__block Float32 rollingAvg = 0;
|
||||
__block UInt64 totalCount = 0;
|
||||
void (^processVolume)(Float32) = ^(Float32 amplitude) {
|
||||
if (amplitude != 0) {
|
||||
Float32 dB = 20 * log10(ABS(amplitude) / MaxAmplitude);
|
||||
float clampedValue = MAX(dB / VolumeClamp, -1) + 1;
|
||||
totalCount++;
|
||||
rollingAvg = (rollingAvg * (totalCount - 1) + clampedValue) / totalCount;
|
||||
}
|
||||
};
|
||||
|
||||
// While there are samples to read and the number of samples above the decibel threshold
|
||||
// is less than the total number of allowed samples over the limit, keep going
|
||||
[reader startReading];
|
||||
while (reader.status == AVAssetReaderStatusReading) {
|
||||
|
||||
AVAssetReaderTrackOutput *trackOutput = (AVAssetReaderTrackOutput *)[reader.outputs objectAtIndex:0];
|
||||
CMSampleBufferRef sampleBufferRef = [trackOutput copyNextSampleBuffer];
|
||||
|
||||
if (sampleBufferRef) {
|
||||
CMBlockBufferRef blockBufferRef = CMSampleBufferGetDataBuffer(sampleBufferRef);
|
||||
size_t length = CMBlockBufferGetDataLength(blockBufferRef);
|
||||
|
||||
NSMutableData *data = [NSMutableData dataWithLength:length];
|
||||
CMBlockBufferCopyDataBytes(blockBufferRef, 0, length, data.mutableBytes);
|
||||
|
||||
SInt16 *samples = (SInt16 *) data.mutableBytes;
|
||||
UInt64 sampleCount = length / bytesPerSample;
|
||||
for (UInt32 i = 0; i < sampleCount ; i++) {
|
||||
Float32 left = (Float32) *samples++;
|
||||
processVolume(left);
|
||||
if (channelCount == 2) {
|
||||
Float32 right = (Float32) *samples++;
|
||||
processVolume(right);
|
||||
}
|
||||
}
|
||||
|
||||
CMSampleBufferInvalidate(sampleBufferRef);
|
||||
CFRelease(sampleBufferRef);
|
||||
}
|
||||
}
|
||||
|
||||
return rollingAvg > VolumeThreshold;
|
||||
}
|
||||
|
||||
|
||||
@end
|
||||
@@ -29,8 +29,9 @@
|
||||
*/
|
||||
|
||||
|
||||
@import UIKit;
|
||||
@import AVFoundation;
|
||||
#import <ResearchKit/ORKRecorder.h>
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@@ -30,10 +30,10 @@
|
||||
|
||||
|
||||
#import "ORKAudioRecorder.h"
|
||||
#import "ORKHelpers.h"
|
||||
|
||||
#import "ORKRecorder_Internal.h"
|
||||
#import "ORKRecorder_Private.h"
|
||||
#import "ORKDefines_Private.h"
|
||||
|
||||
#import "ORKHelpers_Internal.h"
|
||||
|
||||
|
||||
@interface ORKAudioRecorder ()
|
||||
@@ -42,6 +42,8 @@
|
||||
|
||||
@property (nonatomic, copy) NSDictionary *recorderSettings;
|
||||
|
||||
@property (nonatomic, copy) NSString *savedSessionCategory;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@@ -68,10 +70,10 @@
|
||||
if (self) {
|
||||
|
||||
self.continuesInBackground = YES;
|
||||
if (! recorderSettings) {
|
||||
if (!recorderSettings) {
|
||||
recorderSettings = [[self class] defaultRecorderSettings];
|
||||
}
|
||||
if (! [recorderSettings isKindOfClass:[NSDictionary class]]) {
|
||||
if (![recorderSettings isKindOfClass:[NSDictionary class]]) {
|
||||
@throw [NSException exceptionWithName:NSInvalidArgumentException reason:@"recorderSettings should be a dictionary" userInfo:recorderSettings];
|
||||
}
|
||||
self.recorderSettings = recorderSettings;
|
||||
@@ -79,23 +81,34 @@
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)restoreSavedAudioSessionCategory {
|
||||
if (_savedSessionCategory) {
|
||||
NSError *error;
|
||||
if (![[AVAudioSession sharedInstance] setCategory:_savedSessionCategory error:&error]) {
|
||||
ORK_Log_Error(@"Failed to restore the audio session category: %@", [error localizedDescription]);
|
||||
}
|
||||
_savedSessionCategory = nil;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)start {
|
||||
if (self.outputDirectory == nil) {
|
||||
@throw [NSException exceptionWithName:NSDestinationInvalidException reason:@"audioRecorder requires an output directory" userInfo:nil];
|
||||
}
|
||||
// Only create the file when we should actually start recording.
|
||||
if (! _audioRecorder) {
|
||||
if (!_audioRecorder) {
|
||||
|
||||
NSError *error = nil;
|
||||
NSURL *soundFileURL = [self recordingFileURL];
|
||||
if (! [self recreateFileWithError:&error]) {
|
||||
if (![self recreateFileWithError:&error]) {
|
||||
[self finishRecordingWithError:error];
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
AVAudioSession *audioSession = [AVAudioSession sharedInstance];
|
||||
if (! [audioSession setCategory:AVAudioSessionCategoryPlayAndRecord error:&error]) {
|
||||
_savedSessionCategory = audioSession.category;
|
||||
if (![audioSession setCategory:AVAudioSessionCategoryPlayAndRecord error:&error]) {
|
||||
[self finishRecordingWithError:error];
|
||||
return;
|
||||
}
|
||||
@@ -105,19 +118,19 @@
|
||||
initWithURL:soundFileURL
|
||||
settings:self.recorderSettings
|
||||
error:&error];
|
||||
if (! _audioRecorder) {
|
||||
if (!_audioRecorder) {
|
||||
[self finishRecordingWithError:error];
|
||||
return;
|
||||
}
|
||||
|
||||
#if ! TARGET_IPHONE_SIMULATOR
|
||||
#if !TARGET_IPHONE_SIMULATOR
|
||||
if (!_audioRecorder.recording) {
|
||||
[_audioRecorder prepareToRecord];
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#if ! TARGET_IPHONE_SIMULATOR
|
||||
#if !TARGET_IPHONE_SIMULATOR
|
||||
if (!_audioRecorder.recording) {
|
||||
[_audioRecorder prepareToRecord];
|
||||
[_audioRecorder record];
|
||||
@@ -128,7 +141,7 @@
|
||||
}
|
||||
|
||||
- (void)stop {
|
||||
if (! _audioRecorder) {
|
||||
if (!_audioRecorder) {
|
||||
// Error has already been returned.
|
||||
return;
|
||||
}
|
||||
@@ -136,7 +149,7 @@
|
||||
[self doStopRecording];
|
||||
|
||||
NSURL *fileUrl = [self recordingFileURL];
|
||||
if (! [[NSFileManager defaultManager] fileExistsAtPath:[[self recordingFileURL] path]]) {
|
||||
if (![[NSFileManager defaultManager] fileExistsAtPath:[[self recordingFileURL] path]]) {
|
||||
fileUrl = nil;
|
||||
}
|
||||
|
||||
@@ -151,12 +164,12 @@
|
||||
|
||||
- (NSString *)mimeType {
|
||||
NSDictionary *recorderSettings = [self recorderSettings];
|
||||
unsigned int recorderFormat = [recorderSettings[AVFormatIDKey] unsignedIntValue];
|
||||
unsigned int recorderFormat = ((NSNumber *)recorderSettings[AVFormatIDKey]).unsignedIntValue;
|
||||
|
||||
NSString *contentType = @"audio";
|
||||
switch (recorderFormat) {
|
||||
case kAudioFormatLinearPCM: {
|
||||
int numBits = [recorderSettings[AVLinearPCMBitDepthKey] intValue] ? : 16;
|
||||
int numBits = ((NSNumber *)recorderSettings[AVLinearPCMBitDepthKey]).intValue ? : 16;
|
||||
contentType = [NSString stringWithFormat:@"audio/L%d", numBits];
|
||||
break;
|
||||
}
|
||||
@@ -191,18 +204,19 @@
|
||||
|
||||
[self applyFileProtection:ORKFileProtectionComplete toFileAtURL:[self recordingFileURL]];
|
||||
#endif
|
||||
[self restoreSavedAudioSessionCategory];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)finishRecordingWithError:(NSError *)error {
|
||||
[self doStopRecording];
|
||||
|
||||
|
||||
[super finishRecordingWithError:error];
|
||||
}
|
||||
|
||||
- (NSString *)extension {
|
||||
NSDictionary *recorderSettings = [self recorderSettings];
|
||||
unsigned int recorderFormat = [recorderSettings[AVFormatIDKey] unsignedIntValue];
|
||||
unsigned int recorderFormat = ((NSNumber *)recorderSettings[AVFormatIDKey]).unsignedIntValue;
|
||||
|
||||
NSString *extension = @"au";
|
||||
switch (recorderFormat) {
|
||||
@@ -231,9 +245,9 @@
|
||||
return [[self recordingDirectoryURL] URLByAppendingPathComponent:[NSString stringWithFormat:@"%@.%@", [self logName], [self extension]]];
|
||||
}
|
||||
|
||||
- (BOOL)recreateFileWithError:(NSError * __autoreleasing *)error {
|
||||
- (BOOL)recreateFileWithError:(NSError **)error {
|
||||
NSURL *url = [self recordingFileURL];
|
||||
if (! url) {
|
||||
if (!url) {
|
||||
if (error) {
|
||||
*error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileWriteInvalidFileNameError userInfo:@{NSLocalizedDescriptionKey:ORKLocalizedString(@"ERROR_RECORDER_NO_OUTPUT_DIRECTORY", nil)}];
|
||||
}
|
||||
@@ -242,18 +256,18 @@
|
||||
|
||||
NSFileManager *fileManager = [NSFileManager defaultManager];
|
||||
|
||||
if (! [fileManager createDirectoryAtURL:url withIntermediateDirectories:YES attributes:nil error:error]) {
|
||||
if (![fileManager createDirectoryAtURL:url withIntermediateDirectories:YES attributes:nil error:error]) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
if ([fileManager fileExistsAtPath:[url path]]) {
|
||||
if (! [fileManager removeItemAtPath:[url path] error:error]) {
|
||||
if (![fileManager removeItemAtPath:[url path] error:error]) {
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
|
||||
[fileManager createFileAtPath:[url path] contents:nil attributes:nil];
|
||||
[fileManager setAttributes:@{NSFileProtectionKey : ORKFileProtectionFromMode(ORKFileProtectionCompleteUnlessOpen)} ofItemAtPath:[url path] error:error];
|
||||
[fileManager setAttributes:@{NSFileProtectionKey: ORKFileProtectionFromMode(ORKFileProtectionCompleteUnlessOpen)} ofItemAtPath:[url path] error:error];
|
||||
return YES;
|
||||
}
|
||||
|
||||
@@ -266,11 +280,6 @@
|
||||
@end
|
||||
|
||||
|
||||
@interface ORKAudioRecorderConfiguration ()
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation ORKAudioRecorderConfiguration
|
||||
|
||||
#pragma clang diagnostic push
|
||||
@@ -283,7 +292,7 @@
|
||||
recorderSettings:(NSDictionary *)recorderSettings {
|
||||
self = [super initWithIdentifier:identifier];
|
||||
if (self) {
|
||||
if (recorderSettings && ! [recorderSettings isKindOfClass:[NSDictionary class]]) {
|
||||
if (recorderSettings && ![recorderSettings isKindOfClass:[NSDictionary class]]) {
|
||||
@throw [NSException exceptionWithName:NSInvalidArgumentException reason:@"recorderSettings should be a dictionary" userInfo:recorderSettings];
|
||||
}
|
||||
_recorderSettings = recorderSettings;
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
*/
|
||||
|
||||
|
||||
@import Foundation;
|
||||
#import <ResearchKit/ORKActiveStep.h>
|
||||
|
||||
|
||||
|
||||
@@ -30,10 +30,13 @@
|
||||
|
||||
|
||||
#import "ORKAudioStep.h"
|
||||
|
||||
#import "ORKAudioStepViewController.h"
|
||||
#import "ORKHelpers.h"
|
||||
|
||||
#import "ORKStep_Private.h"
|
||||
|
||||
#import "ORKHelpers_Internal.h"
|
||||
|
||||
|
||||
@implementation ORKAudioStep
|
||||
|
||||
@@ -45,6 +48,7 @@
|
||||
self = [super initWithIdentifier:identifier];
|
||||
if (self) {
|
||||
self.shouldShowDefaultTimer = NO;
|
||||
self.shouldStartTimerAutomatically = YES;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@@ -55,7 +59,7 @@
|
||||
NSTimeInterval const ORKAudioTaskMinimumDuration = 5.0;
|
||||
|
||||
if ( self.stepDuration < ORKAudioTaskMinimumDuration) {
|
||||
@throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"duration can not be shorter than %@ seconds.", @(ORKAudioTaskMinimumDuration)] userInfo:nil];
|
||||
@throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"duration cannot be shorter than %@ seconds.", @(ORKAudioTaskMinimumDuration)] userInfo:nil];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,9 @@
|
||||
*/
|
||||
|
||||
|
||||
#import <ResearchKit/ResearchKit_Private.h>
|
||||
@import Foundation;
|
||||
#import <ResearchKit/ORKDefines.h>
|
||||
#import <ResearchKit/ORKActiveStepViewController.h>
|
||||
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
@@ -37,6 +39,8 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
ORK_CLASS_AVAILABLE
|
||||
@interface ORKAudioStepViewController : ORKActiveStepViewController
|
||||
|
||||
@property (nonatomic, assign) CGFloat alertThreshold;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
@@ -30,17 +30,22 @@
|
||||
|
||||
|
||||
#import "ORKAudioStepViewController.h"
|
||||
#import "ORKAudioContentView.h"
|
||||
#import "ORKActiveStepViewController_Internal.h"
|
||||
#import "ORKVerticalContainerView.h"
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
|
||||
#import "ORKActiveStepTimer.h"
|
||||
#import "ORKHelpers.h"
|
||||
#import "ORKStep_Private.h"
|
||||
#import "ORKAudioStep.h"
|
||||
#import "ORKAudioRecorder.h"
|
||||
#import "ORKActiveStepView.h"
|
||||
#import "ORKAudioContentView.h"
|
||||
#import "ORKCustomStepView_Internal.h"
|
||||
#import "ORKVerticalContainerView.h"
|
||||
|
||||
#import "ORKActiveStepViewController_Internal.h"
|
||||
#import "ORKAudioRecorder.h"
|
||||
|
||||
#import "ORKAudioStep.h"
|
||||
#import "ORKStep_Private.h"
|
||||
|
||||
#import "ORKHelpers_Internal.h"
|
||||
|
||||
@import AVFoundation;
|
||||
|
||||
|
||||
@interface ORKAudioStepViewController ()
|
||||
@@ -66,17 +71,24 @@
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setAlertThreshold:(CGFloat)alertThreshold {
|
||||
_alertThreshold = alertThreshold;
|
||||
if (self.isViewLoaded && alertThreshold > 0) {
|
||||
_audioContentView.alertThreshold = alertThreshold;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
// Do any additional setup after loading the view.
|
||||
_audioContentView = [ORKAudioContentView new];
|
||||
_audioContentView.timeLeft = self.audioStep.stepDuration;
|
||||
self.activeStepView.activeCustomView = _audioContentView;
|
||||
}
|
||||
|
||||
- (void)viewDidAppear:(BOOL)animated {
|
||||
[super viewDidAppear:animated];
|
||||
[self start];
|
||||
if (self.alertThreshold > 0) {
|
||||
_audioContentView.alertThreshold = self.alertThreshold;
|
||||
}
|
||||
|
||||
self.activeStepView.activeCustomView = _audioContentView;
|
||||
}
|
||||
|
||||
- (void)audioRecorderDidChange {
|
||||
@@ -107,17 +119,17 @@
|
||||
[_avAudioRecorder updateMeters];
|
||||
float value = [_avAudioRecorder averagePowerForChannel:0];
|
||||
// Assume value is in range roughly -60dB to 0dB
|
||||
float clampedValue = MAX(value/60.0, -1) + 1;
|
||||
float clampedValue = MAX(value / 60.0, -1) + 1;
|
||||
[_audioContentView addSample:@(clampedValue)];
|
||||
_audioContentView.timeLeft = [_timer duration] - [_timer runtime];
|
||||
}
|
||||
|
||||
- (void)startNewTimerIfNeeded {
|
||||
if (! _timer) {
|
||||
if (!_timer) {
|
||||
NSTimeInterval duration = self.audioStep.stepDuration;
|
||||
__weak typeof(self) weakSelf = self;
|
||||
_timer = [[ORKActiveStepTimer alloc] initWithDuration:duration interval:duration/100 runtime:0 handler:^(ORKActiveStepTimer *timer, BOOL finished) {
|
||||
typeof(self) strongSelf = weakSelf;
|
||||
ORKWeakTypeOf(self) weakSelf = self;
|
||||
_timer = [[ORKActiveStepTimer alloc] initWithDuration:duration interval:duration / 100 runtime:0 handler:^(ORKActiveStepTimer *timer, BOOL finished) {
|
||||
ORKStrongTypeOf(self) strongSelf = weakSelf;
|
||||
[strongSelf doSample];
|
||||
if (finished) {
|
||||
[strongSelf finish];
|
||||
@@ -170,7 +182,7 @@
|
||||
_avAudioRecorder = recorder;
|
||||
}
|
||||
|
||||
- (void) recorder:(ORKRecorder *)recorder didFailWithError:(NSError *)error {
|
||||
- (void)recorder:(ORKRecorder *)recorder didFailWithError:(NSError *)error {
|
||||
[super recorder:recorder didFailWithError:error];
|
||||
_audioRecorderError = error;
|
||||
_audioContentView.failed = YES;
|
||||
|
||||
@@ -29,10 +29,13 @@
|
||||
*/
|
||||
|
||||
|
||||
#import <ResearchKit/ResearchKit.h>
|
||||
@import Foundation;
|
||||
#import <ResearchKit/ORKDefines.h>
|
||||
#import <ResearchKit/ORKActiveStep.h>
|
||||
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/**
|
||||
The `ORKCountdownStep` class represents a step that displays a label and a
|
||||
countdown for a time equal to its duration.
|
||||
@@ -46,3 +49,5 @@ ORK_CLASS_AVAILABLE
|
||||
@interface ORKCountdownStep : ORKActiveStep
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
|
||||
|
||||
#import "ORKCountdownStep.h"
|
||||
|
||||
#import "ORKCountdownStepViewController.h"
|
||||
|
||||
|
||||
@@ -57,7 +58,7 @@
|
||||
NSTimeInterval const ORKCountdownStepMinimumDuration = 3.0;
|
||||
|
||||
if ( self.stepDuration < ORKCountdownStepMinimumDuration) {
|
||||
@throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"duration can not be shorter than %@ seconds.", @(ORKCountdownStepMinimumDuration)] userInfo:nil];
|
||||
@throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"duration cannot be shorter than %@ seconds.", @(ORKCountdownStepMinimumDuration)] userInfo:nil];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -29,9 +29,13 @@
|
||||
*/
|
||||
|
||||
|
||||
#import <ResearchKit/ResearchKit_Private.h>
|
||||
@import Foundation;
|
||||
#import <ResearchKit/ORKDefines.h>
|
||||
#import <ResearchKit/ORKActiveStepViewController.h>
|
||||
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/**
|
||||
The `ORKCountdownStepViewController` class represents the step view controller that corresponds to an `ORKCountdownStep`.
|
||||
|
||||
@@ -43,3 +47,5 @@ ORK_CLASS_AVAILABLE
|
||||
@interface ORKCountdownStepViewController : ORKActiveStepViewController
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
@@ -30,23 +30,29 @@
|
||||
|
||||
|
||||
#import "ORKCountdownStepViewController.h"
|
||||
#import "ORKCustomStepView_Internal.h"
|
||||
#import "ORKActiveStepViewController_internal.h"
|
||||
#import "ORKVerticalContainerView.h"
|
||||
#import "ORKStepViewController_Internal.h"
|
||||
|
||||
#import "ORKActiveStepTimer.h"
|
||||
#import "ORKResult.h"
|
||||
#import "ORKActiveStepView.h"
|
||||
#import "ORKCustomStepView_Internal.h"
|
||||
#import "ORKLabel.h"
|
||||
#import "ORKSubheadlineLabel.h"
|
||||
#import "ORKHelpers.h"
|
||||
#import "ORKVerticalContainerView.h"
|
||||
|
||||
#import "ORKActiveStepViewController_Internal.h"
|
||||
#import "ORKStepViewController_Internal.h"
|
||||
|
||||
#import "ORKActiveStep.h"
|
||||
#import "ORKResult.h"
|
||||
|
||||
#import "ORKAccessibility.h"
|
||||
#import "ORKActiveStepView.h"
|
||||
#import "ORKHelpers_Internal.h"
|
||||
|
||||
|
||||
@interface ORKCountDownViewLabel : ORKLabel
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation ORKCountDownViewLabel
|
||||
+ (UIFont *)defaultFont {
|
||||
return ORKThinFontWithSize(56);
|
||||
@@ -69,6 +75,9 @@
|
||||
CAShapeLayer *_circleLayer;
|
||||
}
|
||||
|
||||
static const CGFloat ProgressIndicatorDiameter = 104.0;
|
||||
static const CGFloat ProgressIndicatorOuterMargin = 1.0;
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
@@ -88,55 +97,11 @@
|
||||
[self addSubview:_progressView];
|
||||
|
||||
self.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
|
||||
static const CGFloat ProgressIndicatorDiameter = 104;
|
||||
static const CGFloat ProgressIndicatorOuterMargin = 1;
|
||||
NSDictionary *metrics = @{@"d":@(ProgressIndicatorDiameter+2*ProgressIndicatorOuterMargin)};
|
||||
|
||||
NSDictionary *views = NSDictionaryOfVariableBindings(_textLabel, _timeLabel, _progressView);
|
||||
|
||||
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[_textLabel]-(>=0)-[_progressView(==d)]|"
|
||||
options:NSLayoutFormatDirectionLeadingToTrailing|NSLayoutFormatAlignAllCenterX
|
||||
metrics:metrics
|
||||
views:views]];
|
||||
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-(>=0)-[_textLabel]-(>=0)-|"
|
||||
options:NSLayoutFormatDirectionLeadingToTrailing
|
||||
metrics:metrics
|
||||
views:views]];
|
||||
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-(>=0)-[_progressView(==d)]-(>=0)-|"
|
||||
options:NSLayoutFormatDirectionLeadingToTrailing
|
||||
metrics:metrics
|
||||
views:views]];
|
||||
[self addConstraint:[NSLayoutConstraint constraintWithItem:_progressView
|
||||
attribute:NSLayoutAttributeCenterX
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:self
|
||||
attribute:NSLayoutAttributeCenterX
|
||||
multiplier:1.0 constant:0]];
|
||||
[self addConstraint:[NSLayoutConstraint constraintWithItem:_timeLabel
|
||||
attribute:NSLayoutAttributeCenterX
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:_progressView
|
||||
attribute:NSLayoutAttributeCenterX
|
||||
multiplier:1.0 constant:0]];
|
||||
|
||||
// Constant required in order to give appearance of vertical centering (compensating for leading on font)
|
||||
[self addConstraint:[NSLayoutConstraint constraintWithItem:_timeLabel
|
||||
attribute:NSLayoutAttributeCenterY
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:_progressView
|
||||
attribute:NSLayoutAttributeCenterY
|
||||
multiplier:1.0 constant:-3]];
|
||||
[self addConstraint:[NSLayoutConstraint constraintWithItem:_progressView
|
||||
attribute:NSLayoutAttributeTop
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:_textLabel
|
||||
attribute:NSLayoutAttributeLastBaseline
|
||||
multiplier:1 constant:16-ProgressIndicatorOuterMargin]];
|
||||
[self setUpConstraints];
|
||||
|
||||
_circleLayer = [CAShapeLayer layer];
|
||||
static const CGFloat ProgressIndicatorRadius = ProgressIndicatorDiameter/2;
|
||||
_circleLayer.path = [[UIBezierPath bezierPathWithArcCenter:CGPointMake(ProgressIndicatorRadius+ProgressIndicatorOuterMargin, ProgressIndicatorRadius+ProgressIndicatorOuterMargin)
|
||||
static const CGFloat ProgressIndicatorRadius = ProgressIndicatorDiameter / 2;
|
||||
_circleLayer.path = [[UIBezierPath bezierPathWithArcCenter:CGPointMake(ProgressIndicatorRadius + ProgressIndicatorOuterMargin, ProgressIndicatorRadius + ProgressIndicatorOuterMargin)
|
||||
radius:ProgressIndicatorRadius
|
||||
startAngle:M_PI + M_PI_2
|
||||
endAngle:-M_PI_2
|
||||
@@ -154,16 +119,68 @@
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setUpConstraints {
|
||||
NSMutableArray *constraints = [NSMutableArray new];
|
||||
|
||||
NSDictionary *metrics = @{@"d": @(ProgressIndicatorDiameter + 2 * ProgressIndicatorOuterMargin)};
|
||||
NSDictionary *views = NSDictionaryOfVariableBindings(_textLabel, _timeLabel, _progressView);
|
||||
|
||||
[constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[_textLabel]-(>=0)-[_progressView(==d)]|"
|
||||
options:NSLayoutFormatDirectionLeadingToTrailing | NSLayoutFormatAlignAllCenterX
|
||||
metrics:metrics
|
||||
views:views]];
|
||||
[constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-(>=0)-[_textLabel]-(>=0)-|"
|
||||
options:NSLayoutFormatDirectionLeadingToTrailing
|
||||
metrics:metrics
|
||||
views:views]];
|
||||
[constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-(>=0)-[_progressView(==d)]-(>=0)-|"
|
||||
options:NSLayoutFormatDirectionLeadingToTrailing
|
||||
metrics:metrics
|
||||
views:views]];
|
||||
[constraints addObject:[NSLayoutConstraint constraintWithItem:_progressView
|
||||
attribute:NSLayoutAttributeCenterX
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:self
|
||||
attribute:NSLayoutAttributeCenterX
|
||||
multiplier:1.0
|
||||
constant:0.0]];
|
||||
[constraints addObject:[NSLayoutConstraint constraintWithItem:_timeLabel
|
||||
attribute:NSLayoutAttributeCenterX
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:_progressView
|
||||
attribute:NSLayoutAttributeCenterX
|
||||
multiplier:1.0
|
||||
constant:0.0]];
|
||||
|
||||
// Constant required in order to give appearance of vertical centering (compensating for leading on font)
|
||||
[constraints addObject:[NSLayoutConstraint constraintWithItem:_timeLabel
|
||||
attribute:NSLayoutAttributeCenterY
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:_progressView
|
||||
attribute:NSLayoutAttributeCenterY
|
||||
multiplier:1.0
|
||||
constant:-3.0]];
|
||||
[constraints addObject:[NSLayoutConstraint constraintWithItem:_progressView
|
||||
attribute:NSLayoutAttributeTop
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:_textLabel
|
||||
attribute:NSLayoutAttributeLastBaseline
|
||||
multiplier:1.0
|
||||
constant:16.0 - ProgressIndicatorOuterMargin]];
|
||||
|
||||
[NSLayoutConstraint activateConstraints:constraints];
|
||||
}
|
||||
|
||||
- (void)tintColorDidChange {
|
||||
_circleLayer.strokeColor = self.tintColor.CGColor;
|
||||
}
|
||||
|
||||
- (void)startAnimateWithDuration:(NSTimeInterval)duration {
|
||||
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"strokeEnd"];
|
||||
animation.duration = duration*2;
|
||||
animation.duration = duration * 2;
|
||||
animation.removedOnCompletion = YES;
|
||||
animation.values = @[@(1.0), @(0.0), @(0.0)];
|
||||
animation.keyTimes = @[@(0.0), @(0.5), @(1.0)];
|
||||
animation.values = @[ @(1.0), @(0.0), @(0.0) ];
|
||||
animation.keyTimes = @[ @(0.0), @(0.5), @(1.0) ];
|
||||
animation.timingFunction=[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
|
||||
[_circleLayer addAnimation:animation forKey:@"drawCircleAnimation"];
|
||||
}
|
||||
@@ -224,7 +241,7 @@
|
||||
- (void)viewDidAppear:(BOOL)animated {
|
||||
[super viewDidAppear:animated];
|
||||
|
||||
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, [@(_countDown) stringValue]);
|
||||
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, @(_countDown).stringValue);
|
||||
[_countdownView startAnimateWithDuration:[(ORKActiveStep *)self.step stepDuration]];
|
||||
}
|
||||
|
||||
@@ -233,7 +250,7 @@
|
||||
}
|
||||
|
||||
- (void)countDownTimerFired:(ORKActiveStepTimer *)timer finished:(BOOL)finished {
|
||||
_countDown = MAX((_countDown-1), 0);
|
||||
_countDown = MAX((_countDown - 1), 0);
|
||||
[self updateCountdownLabel];
|
||||
|
||||
if (UIAccessibilityIsVoiceOverRunning()) {
|
||||
@@ -247,7 +264,7 @@
|
||||
}];
|
||||
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, ORKLocalizedString(@"AX_ANNOUNCE_BEGIN_TASK", nil));
|
||||
} else {
|
||||
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, [@(_countDown) stringValue]);
|
||||
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, @(_countDown).stringValue);
|
||||
[super countDownTimerFired:timer finished:finished];
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -29,8 +29,8 @@
|
||||
*/
|
||||
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <ResearchKit/ORKDefines.h>
|
||||
@import Foundation;
|
||||
#import <ResearchKit/ORKTypes.h>
|
||||
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
@@ -65,6 +65,18 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
@end
|
||||
|
||||
|
||||
@protocol ORKDataLoggerExtendedDelegate <ORKDataLoggerDelegate>
|
||||
|
||||
@optional
|
||||
/**
|
||||
Tells the delegate that the maximum current log file lifetime changed.
|
||||
@param dataLogger Source of this event.
|
||||
*/
|
||||
- (void)dataLoggerThresholdsDidChange:(ORKDataLogger *)dataLogger;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@class ORKLogFormatter;
|
||||
|
||||
/**
|
||||
@@ -101,6 +113,7 @@ ORK_CLASS_AVAILABLE
|
||||
*/
|
||||
+ (ORKDataLogger *)JSONDataLoggerWithDirectory:(NSURL *)url logName:(NSString *)logName delegate:(nullable id<ORKDataLoggerDelegate>)delegate;
|
||||
|
||||
+ (instancetype)new NS_UNAVAILABLE;
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
|
||||
/**
|
||||
@@ -166,7 +179,7 @@ ORK_CLASS_AVAILABLE
|
||||
|
||||
@return `YES` if the enumeration was successful; otherwise, `NO`.
|
||||
*/
|
||||
- (BOOL)enumerateLogs:(void (^)(NSURL *logFileUrl, BOOL *stop))block error:(NSError * __autoreleasing *)error;
|
||||
- (BOOL)enumerateLogs:(void (^)(NSURL *logFileUrl, BOOL *stop))block error:(NSError * _Nullable *)error;
|
||||
|
||||
/**
|
||||
Enumerates the URLs of completed log files not yet marked uploaded,
|
||||
@@ -181,7 +194,7 @@ ORK_CLASS_AVAILABLE
|
||||
|
||||
@return `YES` if the enumeration was successful; otherwise, `NO`.
|
||||
*/
|
||||
- (BOOL)enumerateLogsNeedingUpload:(void (^)(NSURL *logFileUrl, BOOL *stop))block error:(NSError * __autoreleasing *)error;
|
||||
- (BOOL)enumerateLogsNeedingUpload:(void (^)(NSURL *logFileUrl, BOOL *stop))block error:(NSError * _Nullable *)error;
|
||||
|
||||
/**
|
||||
Enumerates the URLs of completed log files not already marked uploaded,
|
||||
@@ -196,7 +209,7 @@ ORK_CLASS_AVAILABLE
|
||||
|
||||
@return `YES` if the enumeration was successful; otherwise, `NO`.
|
||||
*/
|
||||
- (BOOL)enumerateLogsAlreadyUploaded:(void (^)(NSURL *logFileUrl, BOOL *stop))block error:(NSError * __autoreleasing *)error;
|
||||
- (BOOL)enumerateLogsAlreadyUploaded:(void (^)(NSURL *logFileUrl, BOOL *stop))block error:(NSError * _Nullable *)error;
|
||||
|
||||
/**
|
||||
Appends an object to the log file, which is formatted with `logFormatter`.
|
||||
@@ -207,12 +220,12 @@ ORK_CLASS_AVAILABLE
|
||||
log data is made. If an attempt is made to log data and there is no access due
|
||||
to file protection, the log is immediately rolled over and a new file created.
|
||||
|
||||
@param object Should be an object of a class that is accepted by the logFormatter.
|
||||
@param error Error output, if the append fails.
|
||||
@param object Should be an object of a class that is accepted by the logFormatter.
|
||||
@param error Error output, if the append fails.
|
||||
|
||||
@return `YES` if appending succeeds; otherwise, `NO`.
|
||||
*/
|
||||
- (BOOL)append:(id)object error:(NSError * __autoreleasing *)error;
|
||||
- (BOOL)append:(id)object error:(NSError * _Nullable *)error;
|
||||
|
||||
/**
|
||||
Appends multiple objects to the log file.
|
||||
@@ -220,12 +233,12 @@ ORK_CLASS_AVAILABLE
|
||||
This method formats and appends all the objects at once. Using this method may have efficiency
|
||||
and atomicity gains for error handling, compared to making multiple calls to `append:error`.
|
||||
|
||||
@param objects An array of objects of a class that is accepted by the logFormatter.
|
||||
@param error Error output, if the append fails.
|
||||
@param objects An array of objects of a class that is accepted by the logFormatter.
|
||||
@param error Error output, if the append fails.
|
||||
|
||||
@return `YES` if appending succeeds; otherwise, `NO`.
|
||||
*/
|
||||
- (BOOL)appendObjects:(NSArray *)objects error:(NSError * __nullable __autoreleasing *)error;
|
||||
- (BOOL)appendObjects:(NSArray *)objects error:(NSError * _Nullable *)error;
|
||||
|
||||
/**
|
||||
Checks whether a file has been marked as uploaded.
|
||||
@@ -251,7 +264,7 @@ ORK_CLASS_AVAILABLE
|
||||
|
||||
@return `YES` if adding or removing the attribute succeeded; otherwise, `NO`.
|
||||
*/
|
||||
- (BOOL)markFileUploaded:(BOOL)uploaded atURL:(NSURL *)url error:(NSError * __nullable __autoreleasing *)error;
|
||||
- (BOOL)markFileUploaded:(BOOL)uploaded atURL:(NSURL *)url error:(NSError * _Nullable *)error;
|
||||
|
||||
/**
|
||||
Removes files if they are marked uploaded.
|
||||
@@ -265,7 +278,7 @@ ORK_CLASS_AVAILABLE
|
||||
|
||||
@return `YES` if removing the files succeeded; otherwise, `NO`.
|
||||
*/
|
||||
- (BOOL)removeUploadedFiles:(NSArray<NSURL *> *)fileURLs withError:(NSError * __nullable __autoreleasing *)error;
|
||||
- (BOOL)removeUploadedFiles:(NSArray<NSURL *> *)fileURLs withError:(NSError * _Nullable *)error;
|
||||
|
||||
/**
|
||||
Removes all files managed by this logger (files that have the `logName` prefix).
|
||||
@@ -274,7 +287,7 @@ ORK_CLASS_AVAILABLE
|
||||
|
||||
@return `YES` if removing the files succeeded.; otherwise, `NO`.
|
||||
*/
|
||||
- (BOOL)removeAllFilesWithError:(NSError *__nullable __autoreleasing *)error;
|
||||
- (BOOL)removeAllFilesWithError:(NSError * _Nullable *)error;
|
||||
|
||||
@end
|
||||
|
||||
@@ -320,7 +333,7 @@ ORK_CLASS_AVAILABLE
|
||||
|
||||
@return `YES` if the write succeeds; otherwise, `NO`.
|
||||
*/
|
||||
- (BOOL)beginLogWithFileHandle:(NSFileHandle *)fileHandle error:(NSError * __nullable __autoreleasing *)error;
|
||||
- (BOOL)beginLogWithFileHandle:(NSFileHandle *)fileHandle error:(NSError * _Nullable *)error;
|
||||
|
||||
/**
|
||||
Appends the specified object to the log file.
|
||||
@@ -331,7 +344,7 @@ ORK_CLASS_AVAILABLE
|
||||
|
||||
@return `YES` if the write succeeds; otherwise, `NO`.
|
||||
*/
|
||||
- (BOOL)appendObject:(id)object fileHandle:(NSFileHandle *)fileHandle error:(NSError * __nullable __autoreleasing *)error;
|
||||
- (BOOL)appendObject:(id)object fileHandle:(NSFileHandle *)fileHandle error:(NSError * _Nullable *)error;
|
||||
|
||||
/**
|
||||
Appends the specified objects to the log file.
|
||||
@@ -342,7 +355,7 @@ ORK_CLASS_AVAILABLE
|
||||
|
||||
@return `YES` if the write succeeds; otherwise, `NO`.
|
||||
*/
|
||||
- (BOOL)appendObjects:(NSArray *)objects fileHandle:(NSFileHandle *)fileHandle error:(NSError * __nullable __autoreleasing *)error;
|
||||
- (BOOL)appendObjects:(NSArray *)objects fileHandle:(NSFileHandle *)fileHandle error:(NSError * _Nullable *)error;
|
||||
|
||||
@end
|
||||
|
||||
@@ -425,6 +438,7 @@ ORK_CLASS_AVAILABLE
|
||||
ORK_CLASS_AVAILABLE
|
||||
@interface ORKDataLoggerManager : NSObject <ORKDataLoggerDelegate>
|
||||
|
||||
+ (instancetype)new NS_UNAVAILABLE;
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
|
||||
/**
|
||||
@@ -505,7 +519,7 @@ ORK_CLASS_AVAILABLE
|
||||
|
||||
@return `YES` if the enumeration succeeds; otherwise, `NO`.
|
||||
*/
|
||||
- (BOOL)enumerateLogsNeedingUpload:(void (^)(ORKDataLogger *dataLogger, NSURL *logFileUrl, BOOL *stop))block error:(NSError * __autoreleasing *)error;
|
||||
- (BOOL)enumerateLogsNeedingUpload:(void (^)(ORKDataLogger *dataLogger, NSURL *logFileUrl, BOOL *stop))block error:(NSError * _Nullable *)error;
|
||||
|
||||
/**
|
||||
Unmarks the set of uploaded files.
|
||||
@@ -518,7 +532,7 @@ ORK_CLASS_AVAILABLE
|
||||
|
||||
@return `YES` if the operation succeeds; otherwise, `NO`.
|
||||
*/
|
||||
- (BOOL)unmarkUploadedFiles:(NSArray<NSURL *> *)fileURLs error:(NSError * __nullable __autoreleasing *)error;
|
||||
- (BOOL)unmarkUploadedFiles:(NSArray<NSURL *> *)fileURLs error:(NSError * _Nullable *)error;
|
||||
|
||||
/**
|
||||
Removes a set of uploaded files.
|
||||
@@ -532,7 +546,7 @@ ORK_CLASS_AVAILABLE
|
||||
|
||||
@return `YES` if the operation succeeds; otherwise, `NO`.
|
||||
*/
|
||||
- (BOOL)removeUploadedFiles:(NSArray<NSURL *> *)fileURLs error:(NSError * __nullable __autoreleasing *)error;
|
||||
- (BOOL)removeUploadedFiles:(NSArray<NSURL *> *)fileURLs error:(NSError * _Nullable *)error;
|
||||
|
||||
/**
|
||||
Removes old and uploaded logs to bring total bytes down to a threshold.
|
||||
@@ -545,7 +559,23 @@ ORK_CLASS_AVAILABLE
|
||||
|
||||
@return `YES` if the operation succeeds; otherwise, `NO`.
|
||||
*/
|
||||
- (BOOL)removeOldAndUploadedLogsToThreshold:(unsigned long long)bytes error:(NSError * __nullable __autoreleasing *)error;
|
||||
- (BOOL)removeOldAndUploadedLogsToThreshold:(unsigned long long)bytes error:(NSError * _Nullable *)error;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@interface ORKDataLogger (Tests)
|
||||
|
||||
/// The file handle to which to write
|
||||
- (nullable NSFileHandle *)fileHandle;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@interface NSURL (ORKDataLogger)
|
||||
|
||||
- (BOOL)ork_isUploaded;
|
||||
- (BOOL)ork_setUploaded:(BOOL)uploaded error:(NSError * _Nullable *)error;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@@ -30,22 +30,21 @@
|
||||
|
||||
|
||||
#import "ORKDataLogger.h"
|
||||
#import <ResearchKit/ResearchKit.h>
|
||||
#import "ORKHelpers.h"
|
||||
#include <sys/xattr.h>
|
||||
#import "ORKDataLogger_Private.h"
|
||||
#import "HKSample+ORKJSONDictionary.h"
|
||||
|
||||
#import "ORKHelpers_Internal.h"
|
||||
#import "CMMotionActivity+ORKJSONDictionary.h"
|
||||
#import "ORKDefines_Private.h"
|
||||
#import "HKSample+ORKJSONDictionary.h"
|
||||
|
||||
#include <sys/xattr.h>
|
||||
|
||||
|
||||
static const char * kORKDataLoggerUploadedAttr = "com.apple.ResearchKit.uploaded";
|
||||
static const char *ORKDataLoggerUploadedAttr = "com.apple.ResearchKit.uploaded";
|
||||
|
||||
// Default per-logfile settings when a data logger is used in an ORKDataLoggerManager
|
||||
static const NSTimeInterval kORKDataLoggerManagerDefaultLogFileLifetime = 60*60*24*3; // 3 days
|
||||
static const unsigned long long kORKDataLoggerManagerDefaultLogFileSize = 1024*1024; // 1 MB
|
||||
static const NSTimeInterval ORKDataLoggerManagerDefaultLogFileLifetime = 60 * 60 * 24 * 3; // 3 days
|
||||
static const unsigned long long ORKDataLoggerManagerDefaultLogFileSize = 1024 * 1024; // 1 MB
|
||||
|
||||
static NSString *const kORKDataLoggerManagerConfigurationFilename = @".ORKDataLoggerManagerConfiguration";
|
||||
static NSString *const ORKDataLoggerManagerConfigurationFilename = @".ORKDataLoggerManagerConfiguration";
|
||||
|
||||
|
||||
@interface ORKDataLogger ()
|
||||
@@ -80,7 +79,7 @@ static NSString *const kORKDataLoggerManagerConfigurationFilename = @".ORKDataLo
|
||||
- (NSString *)ork_logName {
|
||||
NSString *lastComponent = [self lastPathComponent];
|
||||
NSRange idx = [lastComponent rangeOfString:@"-"];
|
||||
if (! idx.length) {
|
||||
if (!idx.length) {
|
||||
@throw [NSException exceptionWithName:NSGenericException reason:@"URL is not a completed log file" userInfo:@{@"url":self}];
|
||||
}
|
||||
|
||||
@@ -91,28 +90,28 @@ static NSString *const kORKDataLoggerManagerConfigurationFilename = @".ORKDataLo
|
||||
- (NSString *)ork_logDateComponent {
|
||||
NSString *lastComponent = [self lastPathComponent];
|
||||
NSRange idx = [lastComponent rangeOfString:@"-"];
|
||||
if (! idx.length) {
|
||||
if (!idx.length) {
|
||||
@throw [NSException exceptionWithName:NSGenericException reason:@"URL is not a completed log file" userInfo:@{@"url":self}];
|
||||
}
|
||||
|
||||
NSString *logDateComponent = [lastComponent substringFromIndex:idx.location+1];
|
||||
NSString *logDateComponent = [lastComponent substringFromIndex:idx.location + 1];
|
||||
return logDateComponent;
|
||||
}
|
||||
|
||||
- (BOOL)ork_isUploaded {
|
||||
NSData *data = [self ork_dataForAttr:kORKDataLoggerUploadedAttr];
|
||||
NSData *data = [self ork_dataForAttr:ORKDataLoggerUploadedAttr];
|
||||
if (!data) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
NSString *string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
|
||||
return ([string integerValue] != 0);
|
||||
return (string.integerValue != 0);
|
||||
}
|
||||
|
||||
- (BOOL)ork_setUploaded:(BOOL)uploaded error:(NSError * __autoreleasing *)error {
|
||||
- (BOOL)ork_setUploaded:(BOOL)uploaded error:(NSError **)error {
|
||||
NSString *value = (uploaded ? @"1" : @"0");
|
||||
NSData *encodedString = [value dataUsingEncoding:NSUTF8StringEncoding];
|
||||
return [self ork_setData:encodedString forAttr:kORKDataLoggerUploadedAttr error:error];
|
||||
return [self ork_setData:encodedString forAttr:ORKDataLoggerUploadedAttr error:error];
|
||||
}
|
||||
|
||||
- (NSData *)ork_dataForAttr:(const char *)attr {
|
||||
@@ -125,7 +124,7 @@ static NSString *const kORKDataLoggerManagerConfigurationFilename = @".ORKDataLo
|
||||
}
|
||||
|
||||
NSMutableData *data = [NSMutableData dataWithLength:length];
|
||||
length = getxattr(path, attr, [data mutableBytes], length, 0, 0);
|
||||
length = getxattr(path, attr, data.mutableBytes, length, 0, 0);
|
||||
if (length <= 0) {
|
||||
return nil;
|
||||
}
|
||||
@@ -133,25 +132,25 @@ static NSString *const kORKDataLoggerManagerConfigurationFilename = @".ORKDataLo
|
||||
return data;
|
||||
}
|
||||
|
||||
- (BOOL)ork_setData:(NSData *)data forAttr:(const char *)attr error:(NSError * __autoreleasing *)error {
|
||||
- (BOOL)ork_setData:(NSData *)data forAttr:(const char *)attr error:(NSError **)error {
|
||||
const char *path = [self fileSystemRepresentation];
|
||||
int rc = setxattr(path, attr, [data bytes], [data length], 0, 0);
|
||||
int rc = setxattr(path, attr, data.bytes, data.length, 0, 0);
|
||||
if (rc != 0) {
|
||||
if (error) {
|
||||
*error = [NSError errorWithDomain:NSCocoaErrorDomain code:rc userInfo:@{NSLocalizedDescriptionKey : ORKLocalizedString(@"ERROR_DATALOGGER_SET_ATTRIBUTE", nil)}];
|
||||
*error = [NSError errorWithDomain:NSCocoaErrorDomain code:rc userInfo:@{NSLocalizedDescriptionKey: ORKLocalizedString(@"ERROR_DATALOGGER_SET_ATTRIBUTE", nil)}];
|
||||
}
|
||||
}
|
||||
return (rc == 0);
|
||||
}
|
||||
|
||||
- (NSString *)ork_logNameInDirectory:(NSURL *)directory {
|
||||
if (! [self isFileURL]) {
|
||||
if (![self isFileURL]) {
|
||||
@throw [NSException exceptionWithName:NSGenericException reason:@"URL is not a fileURL" userInfo:@{@"url":self}];
|
||||
}
|
||||
|
||||
NSString *lastComponent = [self lastPathComponent];
|
||||
NSRange idx = [lastComponent rangeOfString:@"-"];
|
||||
if (! idx.length) {
|
||||
if (!idx.length) {
|
||||
@throw [NSException exceptionWithName:NSGenericException reason:@"URL is not a completed log file" userInfo:@{@"url":self}];
|
||||
}
|
||||
|
||||
@@ -239,11 +238,11 @@ static void *ORKObjectObserverContext = &ORKObjectObserverContext;
|
||||
return [object isKindOfClass:[NSData class]];
|
||||
}
|
||||
|
||||
- (BOOL)beginLogWithFileHandle:(NSFileHandle *)fileHandle error:(NSError * __autoreleasing *)error {
|
||||
- (BOOL)beginLogWithFileHandle:(NSFileHandle *)fileHandle error:(NSError **)error {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)writeData:(NSData *)data fileHandle:(NSFileHandle *)fileHandle error:(NSError * __autoreleasing *)error {
|
||||
- (BOOL)writeData:(NSData *)data fileHandle:(NSFileHandle *)fileHandle error:(NSError **)error {
|
||||
BOOL result = YES;
|
||||
@try {
|
||||
[fileHandle writeData:data];
|
||||
@@ -251,7 +250,7 @@ static void *ORKObjectObserverContext = &ORKObjectObserverContext;
|
||||
@catch (NSException *exception) {
|
||||
result = NO;
|
||||
if (error) {
|
||||
*error = [NSError errorWithDomain:ORKErrorDomain code:ORKErrorException userInfo:@{@"exception" : exception}];
|
||||
*error = [NSError errorWithDomain:ORKErrorDomain code:ORKErrorException userInfo:@{@"exception": exception}];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
@@ -266,14 +265,14 @@ static void *ORKObjectObserverContext = &ORKObjectObserverContext;
|
||||
[fileHandle truncateFileAtOffset:offset];
|
||||
}
|
||||
|
||||
- (BOOL)appendObject:(id)object fileHandle:(NSFileHandle *)fileHandle error:(NSError * __autoreleasing *)error {
|
||||
if (! [self canAcceptLogObject:object]) {
|
||||
- (BOOL)appendObject:(id)object fileHandle:(NSFileHandle *)fileHandle error:(NSError **)error {
|
||||
if (![self canAcceptLogObject:object]) {
|
||||
@throw [NSException exceptionWithName:NSInvalidArgumentException reason:@"ORKLogFormatter accepts NSData only" userInfo:nil];
|
||||
}
|
||||
return [self writeData:(NSData *)object fileHandle:fileHandle error:error];
|
||||
}
|
||||
|
||||
- (BOOL)appendObjects:(NSArray *)objects fileHandle:(NSFileHandle *)fileHandle error:(NSError * __autoreleasing *)error {
|
||||
- (BOOL)appendObjects:(NSArray *)objects fileHandle:(NSFileHandle *)fileHandle error:(NSError **)error {
|
||||
unsigned long long checkpoint = [self checkpointWithFileHandle:fileHandle];
|
||||
|
||||
NSError *errorOut = nil;
|
||||
@@ -285,7 +284,7 @@ static void *ORKObjectObserverContext = &ORKObjectObserverContext;
|
||||
}
|
||||
}
|
||||
|
||||
if (! success) {
|
||||
if (!success) {
|
||||
[self rollbackToCheckpoint:checkpoint fileHandle:fileHandle];
|
||||
if (error) {
|
||||
*error = errorOut;
|
||||
@@ -312,8 +311,8 @@ static NSInteger _ORKJSON_terminatorLength = 0;
|
||||
if (self) {
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
_ORKJSON_emptyLogLength = [[kJSONLogEmptyLogString dataUsingEncoding:NSUTF8StringEncoding] length];
|
||||
_ORKJSON_terminatorLength = [[kJSONLogFooterString dataUsingEncoding:NSUTF8StringEncoding] length];
|
||||
_ORKJSON_emptyLogLength = [kJSONLogEmptyLogString dataUsingEncoding:NSUTF8StringEncoding].length;
|
||||
_ORKJSON_terminatorLength = [kJSONLogFooterString dataUsingEncoding:NSUTF8StringEncoding].length;
|
||||
});
|
||||
}
|
||||
return self;
|
||||
@@ -327,7 +326,7 @@ static NSInteger _ORKJSON_terminatorLength = 0;
|
||||
return [object isKindOfClass:[NSDictionary class]] && [NSJSONSerialization isValidJSONObject:object];
|
||||
}
|
||||
|
||||
- (BOOL)beginLogWithFileHandle:(NSFileHandle *)fileHandle error:(NSError * __autoreleasing *)error {
|
||||
- (BOOL)beginLogWithFileHandle:(NSFileHandle *)fileHandle error:(NSError **)error {
|
||||
// Write valid JSON containing no objects
|
||||
NSData *data = [kJSONLogEmptyLogString dataUsingEncoding:NSUTF8StringEncoding];
|
||||
return [self writeData:data fileHandle:fileHandle error:error];
|
||||
@@ -348,7 +347,7 @@ static NSInteger _ORKJSON_terminatorLength = 0;
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)appendObject:(id)object fileHandle:(NSFileHandle *)fileHandle error:(NSError * __autoreleasing *)error {
|
||||
- (BOOL)appendObject:(id)object fileHandle:(NSFileHandle *)fileHandle error:(NSError **)error {
|
||||
return [self appendObjects:@[object] fileHandle:fileHandle error:error];
|
||||
}
|
||||
|
||||
@@ -361,15 +360,15 @@ static NSInteger _ORKJSON_terminatorLength = 0;
|
||||
* object being appended, and the footer bytes.
|
||||
*/
|
||||
- (BOOL)appendObjects:(NSArray *)objects fileHandle:(NSFileHandle *)fileHandle error:(NSError * __autoreleasing *)error {
|
||||
if (! fileHandle) {
|
||||
if (!fileHandle) {
|
||||
@throw [NSException exceptionWithName:NSInvalidArgumentException reason:@"Filehandle is nil" userInfo:nil];
|
||||
}
|
||||
NSInteger numObjects = [objects count];
|
||||
NSInteger numObjects = objects.count;
|
||||
if (numObjects == 0) {
|
||||
@throw [NSException exceptionWithName:NSInvalidArgumentException reason:@"No objects" userInfo:nil];
|
||||
}
|
||||
for (NSObject *object in objects) {
|
||||
if (! [self canAcceptLogObject:object]) {
|
||||
if (![self canAcceptLogObject:object]) {
|
||||
@throw [NSException exceptionWithName:NSInvalidArgumentException reason:@"ORKLogFormatter accepts JSON serializable objects only" userInfo:nil];
|
||||
}
|
||||
}
|
||||
@@ -406,7 +405,7 @@ static NSInteger _ORKJSON_terminatorLength = 0;
|
||||
}
|
||||
}
|
||||
}];
|
||||
if (! success) {
|
||||
if (!success) {
|
||||
return success;
|
||||
}
|
||||
|
||||
@@ -417,7 +416,7 @@ static NSInteger _ORKJSON_terminatorLength = 0;
|
||||
|
||||
success = [self writeData:outputData fileHandle:fileHandle error:error];
|
||||
|
||||
if (! success) {
|
||||
if (!success) {
|
||||
[self rollbackToCheckpoint:checkpoint fileHandle:fileHandle];
|
||||
}
|
||||
|
||||
@@ -446,23 +445,25 @@ static NSInteger _ORKJSON_terminatorLength = 0;
|
||||
return [[ORKDataLogger alloc] initWithDirectory:url logName:logName formatter:[ORKJSONLogFormatter new] delegate:delegate];
|
||||
}
|
||||
|
||||
+ (instancetype)new {
|
||||
ORKThrowMethodUnavailableException();
|
||||
}
|
||||
|
||||
- (instancetype)init {
|
||||
ORKThrowMethodUnavailableException();
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (instancetype)initWithDirectory:(NSURL *)url logName:(NSString *)logName formatter:(ORKLogFormatter *)formatter delegate:(id<ORKDataLoggerDelegate>)delegate {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_url = [url copy];
|
||||
if (! [[NSFileManager defaultManager] fileExistsAtPath:[url path]]) {
|
||||
if (![[NSFileManager defaultManager] fileExistsAtPath:[url path]]) {
|
||||
@throw [NSException exceptionWithName:NSInvalidArgumentException reason:@"directory does not exist" userInfo:nil];
|
||||
}
|
||||
if ([logName hasSuffix:@"-"]) {
|
||||
@throw [NSException exceptionWithName:NSInvalidArgumentException reason:@"logName should not terminate with '-'" userInfo:nil];
|
||||
}
|
||||
if (! [logName length]) {
|
||||
if (!logName.length) {
|
||||
@throw [NSException exceptionWithName:NSInvalidArgumentException reason:@"logName must be non-empty" userInfo:nil];
|
||||
}
|
||||
|
||||
@@ -486,27 +487,27 @@ static NSInteger _ORKJSON_terminatorLength = 0;
|
||||
|
||||
- (instancetype)initWithDirectory:(NSURL *)url configuration:(NSDictionary *)configuration delegate:(id<ORKDataLoggerDelegate>)delegate {
|
||||
Class formatterClass = NSClassFromString(configuration[@"formatterClass"]);
|
||||
if (! formatterClass) {
|
||||
if (!formatterClass) {
|
||||
@throw [NSException exceptionWithName:NSGenericException reason:[NSString stringWithFormat:@"%@ is not a class", configuration[@"formatterClass"]] userInfo:nil];
|
||||
}
|
||||
|
||||
self = [self initWithDirectory:url logName:configuration[@"logName"] formatter:[formatterClass new] delegate:delegate];
|
||||
self = [self initWithDirectory:url logName:configuration[@"logName"] formatter:[[formatterClass alloc] init] delegate:delegate];
|
||||
if (self) {
|
||||
// Don't notify about initial setup
|
||||
[_observer pause];
|
||||
self.maximumCurrentLogFileSize = [configuration[@"maximumCurrentLogFileSize"] unsignedLongValue];
|
||||
self.maximumCurrentLogFileLifetime = [configuration[@"maximumCurrentLogFileLifetime"] doubleValue];
|
||||
self.maximumCurrentLogFileSize = ((NSNumber *)configuration[@"maximumCurrentLogFileSize"]).unsignedLongValue;
|
||||
self.maximumCurrentLogFileLifetime = ((NSNumber *)configuration[@"maximumCurrentLogFileLifetime"]).doubleValue;
|
||||
[_observer resume];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (NSDictionary *)configuration {
|
||||
return @{@"logName" : self.logName,
|
||||
@"formatterClass" : NSStringFromClass([self.logFormatter class]),
|
||||
@"fileProtectionMode" : @(self.fileProtectionMode),
|
||||
@"maximumCurrentLogFileSize" : @(self.maximumCurrentLogFileSize),
|
||||
@"maximumCurrentLogFileLifetime" : @(self.maximumCurrentLogFileLifetime)
|
||||
return @{@"logName": self.logName,
|
||||
@"formatterClass": NSStringFromClass([self.logFormatter class]),
|
||||
@"fileProtectionMode": @(self.fileProtectionMode),
|
||||
@"maximumCurrentLogFileSize": @(self.maximumCurrentLogFileSize),
|
||||
@"maximumCurrentLogFileLifetime": @(self.maximumCurrentLogFileLifetime)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -515,7 +516,7 @@ static NSInteger _ORKJSON_terminatorLength = 0;
|
||||
- (void)setupDirectorySource {
|
||||
int dirFD = open([_url fileSystemRepresentation], O_EVTONLY);
|
||||
if (dirFD < 0) {
|
||||
ORK_Log_Oops(@"Could not track directory %s (%d)", [_url fileSystemRepresentation], [[NSFileManager defaultManager] fileExistsAtPath:[_url path]]);
|
||||
ORK_Log_Warning(@"Could not track directory %s (%d)", [_url fileSystemRepresentation], [[NSFileManager defaultManager] fileExistsAtPath:[_url path]]);
|
||||
} else {
|
||||
// Dispatch to a concurrent queue, so we don't store up blocks while our
|
||||
// queue is working.
|
||||
@@ -527,9 +528,9 @@ static NSInteger _ORKJSON_terminatorLength = 0;
|
||||
|
||||
if (_directorySource) {
|
||||
dispatch_source_set_cancel_handler(_directorySource, ^{ close(dirFD); });
|
||||
__weak __typeof(self) weakSelf = self;
|
||||
ORKWeakTypeOf(self) weakSelf = self;
|
||||
dispatch_source_set_event_handler(_directorySource, ^{
|
||||
__strong __typeof(self) strongSelf = weakSelf;
|
||||
ORKStrongTypeOf(self) strongSelf = weakSelf;
|
||||
[strongSelf directoryUpdated];
|
||||
});
|
||||
dispatch_resume(_directorySource);
|
||||
@@ -594,11 +595,11 @@ static NSInteger _ORKJSON_terminatorLength = 0;
|
||||
return success;
|
||||
}
|
||||
|
||||
- (BOOL)enumerateLogsNeedingUpload:(void (^)(NSURL *logFileUrl, BOOL *stop))block error:(NSError * __autoreleasing *)error {
|
||||
- (BOOL)enumerateLogsNeedingUpload:(void (^)(NSURL *logFileUrl, BOOL *stop))block error:(NSError **)error {
|
||||
return [self enumerateLogsUploaded:NO block:block error:error];
|
||||
}
|
||||
|
||||
- (BOOL)enumerateLogsAlreadyUploaded:(void (^)(NSURL *logFileUrl, BOOL *stop))block error:(NSError * __autoreleasing *)error {
|
||||
- (BOOL)enumerateLogsAlreadyUploaded:(void (^)(NSURL *logFileUrl, BOOL *stop))block error:(NSError **)error {
|
||||
return [self enumerateLogsUploaded:YES block:block error:error];
|
||||
}
|
||||
|
||||
@@ -614,7 +615,7 @@ static NSInteger _ORKJSON_terminatorLength = 0;
|
||||
}
|
||||
|
||||
- (BOOL)appendObjects:(NSArray *)objects error:(NSError * __autoreleasing *)error {
|
||||
if (![objects count]) {
|
||||
if (!objects.count) {
|
||||
@throw [NSException exceptionWithName:NSInvalidArgumentException reason:@"Empty array" userInfo:nil];
|
||||
}
|
||||
__block BOOL success = NO;
|
||||
@@ -664,7 +665,7 @@ static NSInteger _ORKJSON_terminatorLength = 0;
|
||||
}
|
||||
|
||||
- (void)queue_setNeedsUpdateBytes {
|
||||
if (! _directoryDirty) {
|
||||
if (!_directoryDirty) {
|
||||
_directoryDirty = YES;
|
||||
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.05 * NSEC_PER_SEC)), _queue, ^{
|
||||
@@ -686,7 +687,7 @@ static NSInteger _ORKJSON_terminatorLength = 0;
|
||||
});
|
||||
}
|
||||
|
||||
- (BOOL)queue_enumerateLogs:(void (^)(NSURL *logFileUrl, BOOL *stop))block error:(NSError * __autoreleasing *)error {
|
||||
- (BOOL)queue_enumerateLogs:(void (^)(NSURL *logFileUrl, BOOL *stop))block error:(NSError **)error {
|
||||
static NSArray *keys = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
@@ -704,7 +705,7 @@ static NSInteger _ORKJSON_terminatorLength = 0;
|
||||
NSError *errorOut = nil;
|
||||
NSMutableArray *urls = [NSMutableArray array];
|
||||
for (NSURL *url in enumerator) {
|
||||
if (! [self urlMatchesLogName:url]) {
|
||||
if (![self urlMatchesLogName:url]) {
|
||||
continue;
|
||||
}
|
||||
if ( [[url lastPathComponent] isEqualToString:_logName]) {
|
||||
@@ -716,13 +717,13 @@ static NSInteger _ORKJSON_terminatorLength = 0;
|
||||
// If there's been an error getting the resource values, give up
|
||||
break;
|
||||
}
|
||||
if (! [resources[NSURLIsRegularFileKey] boolValue]) {
|
||||
if (!((NSNumber *)resources[NSURLIsRegularFileKey]).boolValue) {
|
||||
continue;
|
||||
}
|
||||
[urls addObject:url];
|
||||
}
|
||||
|
||||
if (! errorOut) {
|
||||
if (!errorOut) {
|
||||
// Sort the URLs before beginning enumeration for the caller
|
||||
[urls sortUsingComparator:^NSComparisonResult(NSURL *obj1, NSURL *obj2) {
|
||||
// We can assume all relate to files in the same directory
|
||||
@@ -744,7 +745,7 @@ static NSInteger _ORKJSON_terminatorLength = 0;
|
||||
return (errorOut ? NO : YES);
|
||||
}
|
||||
|
||||
- (BOOL)queue_enumerateLogsUploaded:(BOOL)uploaded block:(void (^)(NSURL *logFileUrl, BOOL *stop))block error:(NSError * __autoreleasing *)error {
|
||||
- (BOOL)queue_enumerateLogsUploaded:(BOOL)uploaded block:(void (^)(NSURL *logFileUrl, BOOL *stop))block error:(NSError **)error {
|
||||
return [self queue_enumerateLogs:^(NSURL *logFileUrl, BOOL *stop) {
|
||||
NSError *errorOut = nil;
|
||||
BOOL wantUploaded = [logFileUrl ork_isUploaded];
|
||||
@@ -758,7 +759,7 @@ static NSInteger _ORKJSON_terminatorLength = 0;
|
||||
} error:error];
|
||||
}
|
||||
|
||||
- (NSFileHandle *)queue_makeFileHandleWithError:(NSError * __autoreleasing *)error {
|
||||
- (NSFileHandle *)queue_makeFileHandleWithError:(NSError **)error {
|
||||
NSFileManager *fileManager = [NSFileManager defaultManager];
|
||||
NSURL *url = [self currentLogFileURL];
|
||||
|
||||
@@ -766,12 +767,12 @@ static NSInteger _ORKJSON_terminatorLength = 0;
|
||||
NSNumber *fileExists = nil;
|
||||
[url getResourceValue:&fileExists forKey:NSURLIsRegularFileKey error:nil];
|
||||
|
||||
BOOL createNewFile = ! [fileExists boolValue];
|
||||
BOOL createNewFile = !fileExists.boolValue;
|
||||
|
||||
NSFileHandle *fileHandle = nil;
|
||||
if (!createNewFile) {
|
||||
fileHandle = [NSFileHandle fileHandleForWritingToURL:url error:error];
|
||||
if (! fileHandle) {
|
||||
if (!fileHandle) {
|
||||
// Assume it's because we can't open the file, perhaps for security reasons.
|
||||
// Close and rename the log.
|
||||
[self queue_closeAndRenameLog];
|
||||
@@ -784,7 +785,7 @@ static NSInteger _ORKJSON_terminatorLength = 0;
|
||||
BOOL success = [fileManager createFileAtPath:filePath contents:nil attributes:nil];
|
||||
if (!success) {
|
||||
if (error) {
|
||||
*error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileNoSuchFileError userInfo:@{NSLocalizedDescriptionKey : ORKLocalizedString(@"ERROR_DATALOGGER_CREATE_FILE", nil)}];
|
||||
*error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileNoSuchFileError userInfo:@{NSLocalizedDescriptionKey: ORKLocalizedString(@"ERROR_DATALOGGER_CREATE_FILE", nil)}];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
@@ -799,7 +800,7 @@ static NSInteger _ORKJSON_terminatorLength = 0;
|
||||
assert(fileHandle);
|
||||
|
||||
// Set file protection after opening the file, so that class B works as expected.
|
||||
BOOL success = [fileManager setAttributes:@{NSFileProtectionKey : ORKFileProtectionFromMode(self.fileProtectionMode)} ofItemAtPath:[url path] error:error];
|
||||
BOOL success = [fileManager setAttributes:@{NSFileProtectionKey: ORKFileProtectionFromMode(self.fileProtectionMode)} ofItemAtPath:[url path] error:error];
|
||||
|
||||
// Allow formatter to initialize the log file with header content
|
||||
success = success && [self.logFormatter beginLogWithFileHandle:fileHandle error:error];
|
||||
@@ -814,7 +815,7 @@ static NSInteger _ORKJSON_terminatorLength = 0;
|
||||
return _currentFileHandle;
|
||||
}
|
||||
|
||||
- (NSFileHandle *)queue_fileHandleWithError:(NSError * __autoreleasing *)error {
|
||||
- (NSFileHandle *)queue_fileHandleWithError:(NSError **)error {
|
||||
if (!_currentFileHandle) {
|
||||
_currentFileHandle = [self queue_makeFileHandleWithError:error];
|
||||
|
||||
@@ -824,15 +825,15 @@ static NSInteger _ORKJSON_terminatorLength = 0;
|
||||
}
|
||||
|
||||
+ (NSURL *)nextUrlForDirectoryUrl:(NSURL *)directory logName:(NSString *)logName {
|
||||
static NSDateFormatter *dfm = nil;
|
||||
static NSDateFormatter *dateFromatter = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
dfm = [NSDateFormatter new];
|
||||
[dfm setLocale:[NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]];
|
||||
dfm.dateFormat = @"yyyyMMddHHmmss";
|
||||
dateFromatter = [NSDateFormatter new];
|
||||
[dateFromatter setLocale:[NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]];
|
||||
dateFromatter.dateFormat = @"yyyyMMddHHmmss";
|
||||
});
|
||||
|
||||
NSString *datedLog = [NSString stringWithFormat:@"%@-%@",logName, [dfm stringFromDate:[NSDate date]]];
|
||||
NSString *datedLog = [NSString stringWithFormat:@"%@-%@",logName, [dateFromatter stringFromDate:[NSDate date]]];
|
||||
NSURL *destinationUrl = [directory URLByAppendingPathComponent:datedLog];
|
||||
|
||||
NSFileManager *fileManager = [NSFileManager defaultManager];
|
||||
@@ -858,19 +859,19 @@ static NSInteger _ORKJSON_terminatorLength = 0;
|
||||
}
|
||||
|
||||
// Check if a non-empty file exists, and create the file handle if so
|
||||
NSDictionary *params = [url resourceValuesForKeys:@[NSURLIsRegularFileKey,NSURLFileSizeKey] error:nil];
|
||||
NSDictionary *parameters = [url resourceValuesForKeys:@[NSURLIsRegularFileKey,NSURLFileSizeKey] error:nil];
|
||||
|
||||
if ( [params[NSURLIsRegularFileKey] boolValue]) {
|
||||
if ([params[NSURLFileSizeKey] intValue] > 0) {
|
||||
if (((NSNumber *)parameters[NSURLIsRegularFileKey]).boolValue) {
|
||||
if (((NSNumber *)parameters[NSURLFileSizeKey]).intValue > 0) {
|
||||
NSURL *destinationUrl = [ORKDataLogger nextUrlForDirectoryUrl:_url logName:_logName];
|
||||
ORK_Log_Debug(@"Rollover: %@ to %@", [url lastPathComponent], [destinationUrl lastPathComponent]);
|
||||
[fileManager moveItemAtURL:url toURL:destinationUrl error:nil];
|
||||
if (self.fileProtectionMode == ORKFileProtectionCompleteUnlessOpen) {
|
||||
// Upgrade to complete file protection after roll-over
|
||||
NSError *error = nil;
|
||||
if (! [fileManager setAttributes:@{NSFileProtectionKey : NSFileProtectionComplete}
|
||||
ofItemAtPath:[destinationUrl path] error:&error]) {
|
||||
ORK_Log_Debug(@"Error setting NSFileProtectionComplete on %@: %@", destinationUrl, error);
|
||||
if (![fileManager setAttributes:@{NSFileProtectionKey: NSFileProtectionComplete}
|
||||
ofItemAtPath:[destinationUrl path] error:&error]) {
|
||||
ORK_Log_Warning(@"Error setting NSFileProtectionComplete on %@: %@", destinationUrl, error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -889,7 +890,7 @@ static NSInteger _ORKJSON_terminatorLength = 0;
|
||||
NSURL *url = [self currentLogFileURL];
|
||||
NSDictionary *parameters = [url resourceValuesForKeys:@[NSURLIsRegularFileKey, NSURLFileSizeKey, NSURLCreationDateKey] error:nil];
|
||||
|
||||
NSInteger fileSize = [parameters[NSURLFileSizeKey] integerValue];
|
||||
NSInteger fileSize = ((NSNumber *)parameters[NSURLFileSizeKey]).integerValue;
|
||||
NSDate *creationDate = parameters[NSURLCreationDateKey];
|
||||
|
||||
BOOL exceededSizeThreshold = ( (self.maximumCurrentLogFileSize > 0) && (fileSize >= self.maximumCurrentLogFileSize));
|
||||
@@ -907,7 +908,7 @@ static NSInteger _ORKJSON_terminatorLength = 0;
|
||||
[self queue_closeAndRenameLog];
|
||||
}
|
||||
|
||||
- (BOOL)queue_append:(id)object error:(NSError * __autoreleasing *)error {
|
||||
- (BOOL)queue_append:(id)object error:(NSError **)error {
|
||||
[self queue_rolloverIfNeeded];
|
||||
|
||||
NSFileHandle *fileHandle = [self queue_fileHandleWithError:error];
|
||||
@@ -925,7 +926,7 @@ static NSInteger _ORKJSON_terminatorLength = 0;
|
||||
return result;
|
||||
}
|
||||
|
||||
- (BOOL)queue_appendObjects:(NSArray *)objects error:(NSError * __autoreleasing *)error {
|
||||
- (BOOL)queue_appendObjects:(NSArray *)objects error:(NSError **)error {
|
||||
[self queue_rolloverIfNeeded];
|
||||
|
||||
NSFileHandle *fileHandle = [self queue_fileHandleWithError:error];
|
||||
@@ -942,13 +943,13 @@ static NSInteger _ORKJSON_terminatorLength = 0;
|
||||
return result;
|
||||
}
|
||||
|
||||
- (BOOL)queue_markFileUploaded:(BOOL)uploaded atURL:(NSURL *)url error:(NSError * __autoreleasing *)error {
|
||||
- (BOOL)queue_markFileUploaded:(BOOL)uploaded atURL:(NSURL *)url error:(NSError **)error {
|
||||
BOOL success = [url ork_setUploaded:uploaded error:error];
|
||||
[self queue_setNeedsUpdateBytes];
|
||||
return success;
|
||||
}
|
||||
|
||||
- (BOOL)queue_removeUploadedFiles:(NSArray<NSURL *> *)fileURLs withError:(NSError * __autoreleasing *)error {
|
||||
- (BOOL)queue_removeUploadedFiles:(NSArray<NSURL *> *)fileURLs withError:(NSError **)error {
|
||||
NSFileManager *fileManager = [NSFileManager defaultManager];
|
||||
__block NSMutableArray *errors = [NSMutableArray array];
|
||||
BOOL success = [self queue_enumerateLogs:^(NSURL *logFileUrl, BOOL *stop) {
|
||||
@@ -962,16 +963,20 @@ static NSInteger _ORKJSON_terminatorLength = 0;
|
||||
}
|
||||
} else {
|
||||
// File was requested to be removed, but was not marked uploaded
|
||||
[errors addObject:[NSError errorWithDomain:ORKErrorDomain code:ORKErrorInvalidObject userInfo:@{NSLocalizedDescriptionKey : ORKLocalizedString(@"ERROR_DATALOGGER_COULD_NOT_MAORK", nil), @"url" : logFileUrl}]];
|
||||
[errors addObject:[NSError errorWithDomain:ORKErrorDomain
|
||||
code:ORKErrorInvalidObject
|
||||
userInfo:@{NSLocalizedDescriptionKey: ORKLocalizedString(@"ERROR_DATALOGGER_COULD_NOT_MAORK", nil), @"url": logFileUrl}]];
|
||||
}
|
||||
}
|
||||
} error:error];
|
||||
|
||||
// Reporting multiple errors
|
||||
if ([errors count]) {
|
||||
if (errors.count) {
|
||||
if (!success && error && *error) {
|
||||
[errors addObject:*error];
|
||||
*error = [NSError errorWithDomain:ORKErrorDomain code:ORKErrorMultipleErrors userInfo:@{NSLocalizedDescriptionKey : ORKLocalizedString(@"ERROR_DATALOGGER_MULTIPLE", nil), @"errors" : errors}];
|
||||
*error = [NSError errorWithDomain:ORKErrorDomain
|
||||
code:ORKErrorMultipleErrors
|
||||
userInfo:@{NSLocalizedDescriptionKey: ORKLocalizedString(@"ERROR_DATALOGGER_MULTIPLE", nil), @"errors": errors}];
|
||||
}
|
||||
success = NO;
|
||||
}
|
||||
@@ -1042,17 +1047,19 @@ static NSInteger _ORKJSON_terminatorLength = 0;
|
||||
|
||||
@implementation ORKDataLoggerManager
|
||||
|
||||
+ (instancetype)new {
|
||||
ORKThrowMethodUnavailableException();
|
||||
}
|
||||
|
||||
- (instancetype)init {
|
||||
ORKThrowMethodUnavailableException();
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (instancetype)initWithDirectory:(NSURL *)directory delegate:(id<ORKDataLoggerManagerDelegate>)delegate {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_directory = directory;
|
||||
if (! [[NSFileManager defaultManager] fileExistsAtPath:[_directory path]]) {
|
||||
if (![[NSFileManager defaultManager] fileExistsAtPath:[_directory path]]) {
|
||||
@throw [NSException exceptionWithName:NSGenericException reason:@"directory does not exist" userInfo:nil];
|
||||
}
|
||||
_delegate = delegate;
|
||||
@@ -1065,39 +1072,43 @@ static NSInteger _ORKJSON_terminatorLength = 0;
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSDictionary *configuration = [NSDictionary dictionaryWithContentsOfURL:[_directory URLByAppendingPathComponent:kORKDataLoggerManagerConfigurationFilename]];
|
||||
NSDictionary *configuration = [NSDictionary dictionaryWithContentsOfURL:[_directory URLByAppendingPathComponent:ORKDataLoggerManagerConfigurationFilename]];
|
||||
[self loadConfiguration:configuration];
|
||||
|
||||
_observer = [[ORKObjectObserver alloc] initWithObject:self keys:@[@"pendingUploadBytesThreshold",@"totalBytesThreshold"] selector:@selector(configurationDidChange)];
|
||||
_observer = [[ORKObjectObserver alloc] initWithObject:self keys:@[@"pendingUploadBytesThreshold", @"totalBytesThreshold"] selector:@selector(configurationDidChange)];
|
||||
|
||||
[self setNeedsUpdateBytes];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
static NSString *const PendingUploadBytesThresholdKey = @"pendingUploadBytesThreshold";
|
||||
static NSString *const TotalBytesThresholdKey = @"totalBytesThreshold";
|
||||
static NSString *const LoggerConfigurationsKey = @"loggers";
|
||||
|
||||
- (void)loadConfiguration:(NSDictionary *)configuration {
|
||||
self.pendingUploadBytesThreshold = [configuration[@"pendingUploadBytesThreshold"] unsignedLongLongValue];
|
||||
self.totalBytesThreshold = [configuration[@"totalBytesThreshold"] unsignedLongLongValue];
|
||||
self.pendingUploadBytesThreshold = ((NSNumber *)configuration[PendingUploadBytesThresholdKey]).unsignedLongLongValue;
|
||||
self.totalBytesThreshold = ((NSNumber *)configuration[TotalBytesThresholdKey]).unsignedLongLongValue;
|
||||
|
||||
NSMutableDictionary *records = [NSMutableDictionary dictionary];
|
||||
for (NSDictionary *loggerConfig in configuration[@"loggers"]) {
|
||||
ORKDataLogger *logger = [[ORKDataLogger alloc] initWithDirectory:_directory configuration:loggerConfig delegate:self];
|
||||
for (NSDictionary *loggerConfiguration in configuration[LoggerConfigurationsKey]) {
|
||||
ORKDataLogger *logger = [[ORKDataLogger alloc] initWithDirectory:_directory configuration:loggerConfiguration delegate:self];
|
||||
records[logger.logName] = logger;
|
||||
}
|
||||
_records = records;
|
||||
}
|
||||
|
||||
- (NSDictionary *)queue_configuration {
|
||||
NSMutableArray *loggerConfigurations = [[_records allValues] valueForKey:@"configuration"];
|
||||
NSMutableArray *loggerConfigurations = [_records.allValues valueForKey:@"configuration"];
|
||||
|
||||
return @{@"pendingUploadBytesThreshold" : @(self.pendingUploadBytesThreshold),
|
||||
@"totalBytesThreshold" : @(self.totalBytesThreshold),
|
||||
@"loggers" : loggerConfigurations };
|
||||
return @{PendingUploadBytesThresholdKey: @(self.pendingUploadBytesThreshold),
|
||||
TotalBytesThresholdKey: @(self.totalBytesThreshold),
|
||||
LoggerConfigurationsKey: loggerConfigurations };
|
||||
}
|
||||
|
||||
- (void)queue_synchronizeConfiguration {
|
||||
NSDictionary *configuration = [self queue_configuration];
|
||||
[configuration writeToURL:[_directory URLByAppendingPathComponent:kORKDataLoggerManagerConfigurationFilename] atomically:YES];
|
||||
[configuration writeToURL:[_directory URLByAppendingPathComponent:ORKDataLoggerManagerConfigurationFilename] atomically:YES];
|
||||
}
|
||||
|
||||
- (void)configurationDidChange {
|
||||
@@ -1114,8 +1125,8 @@ static NSInteger _ORKJSON_terminatorLength = 0;
|
||||
ORKDataLogger *dataLogger = [[ORKDataLogger alloc] initWithDirectory:_directory logName:logName formatter:formatter delegate:self];
|
||||
dataLogger.delegate = nil;
|
||||
// Pick suitable defaults for a typical use pattern
|
||||
dataLogger.maximumCurrentLogFileLifetime = kORKDataLoggerManagerDefaultLogFileLifetime;
|
||||
dataLogger.maximumCurrentLogFileSize = kORKDataLoggerManagerDefaultLogFileSize;
|
||||
dataLogger.maximumCurrentLogFileLifetime = ORKDataLoggerManagerDefaultLogFileLifetime;
|
||||
dataLogger.maximumCurrentLogFileSize = ORKDataLoggerManagerDefaultLogFileSize;
|
||||
dataLogger.delegate = self;
|
||||
|
||||
_records[logName] = dataLogger;
|
||||
@@ -1166,16 +1177,16 @@ static NSInteger _ORKJSON_terminatorLength = 0;
|
||||
- (NSArray<NSString *> *)logNames {
|
||||
__block NSArray<NSString *> *logNames = nil;
|
||||
dispatch_sync(_queue, ^{
|
||||
logNames = [_records allKeys];
|
||||
logNames = _records.allKeys;
|
||||
});
|
||||
return logNames;
|
||||
}
|
||||
|
||||
- (BOOL)queue_enumerateLogsNeedingUpload:(void (^)(ORKDataLogger *dataLogger, NSURL *logFileUrl, BOOL *stop))block error:(NSError * __autoreleasing *)error {
|
||||
- (BOOL)queue_enumerateLogsNeedingUpload:(void (^)(ORKDataLogger *dataLogger, NSURL *logFileUrl, BOOL *stop))block error:(NSError **)error {
|
||||
BOOL success = YES;
|
||||
NSMutableArray *allFiles = [NSMutableArray array];
|
||||
// Collect all the log file URLs so we can sort them by date rather than enumerating by logger.
|
||||
for (ORKDataLogger *logger in [_records allValues]) {
|
||||
for (ORKDataLogger *logger in _records.allValues) {
|
||||
success = [logger enumerateLogsNeedingUpload:^(NSURL *logFileUrl, BOOL *stop) {
|
||||
[allFiles addObject:logFileUrl];
|
||||
} error:error];
|
||||
@@ -1222,13 +1233,13 @@ static NSInteger _ORKJSON_terminatorLength = 0;
|
||||
return success;
|
||||
}
|
||||
|
||||
- (BOOL)queue_removeUploadedFiles:(NSArray<NSURL *> *)fileURLs error:(NSError * __autoreleasing *)error {
|
||||
- (BOOL)queue_removeUploadedFiles:(NSArray<NSURL *> *)fileURLs error:(NSError **)error {
|
||||
BOOL success = YES;
|
||||
NSMutableArray *notRemoved = [NSMutableArray array];
|
||||
for (NSURL *url in fileURLs) {
|
||||
NSString *logName = [url ork_logNameInDirectory:_directory];
|
||||
|
||||
if (! _records[logName]) {
|
||||
if (!_records[logName]) {
|
||||
@throw [NSException exceptionWithName:NSGenericException reason:@"URL is not from a known logger" userInfo:@{@"url":url}];
|
||||
}
|
||||
|
||||
@@ -1239,7 +1250,7 @@ static NSInteger _ORKJSON_terminatorLength = 0;
|
||||
success = NO;
|
||||
}
|
||||
}
|
||||
if (error && [notRemoved count]) {
|
||||
if (error && notRemoved.count) {
|
||||
*error = [NSError errorWithDomain:ORKErrorDomain code:ORKErrorMultipleErrors userInfo:@{@"notRemoved":notRemoved}];
|
||||
}
|
||||
return success;
|
||||
@@ -1254,13 +1265,13 @@ static NSInteger _ORKJSON_terminatorLength = 0;
|
||||
return success;
|
||||
}
|
||||
|
||||
- (BOOL)queue_unmarkUploadedFiles:(NSArray<NSURL *> *)fileURLs error:(NSError * __autoreleasing *)error {
|
||||
- (BOOL)queue_unmarkUploadedFiles:(NSArray<NSURL *> *)fileURLs error:(NSError **)error {
|
||||
BOOL success = YES;
|
||||
NSMutableArray<NSURL *> *notRemoved = [NSMutableArray array];
|
||||
for (NSURL *url in fileURLs) {
|
||||
NSString *logName = [url ork_logNameInDirectory:_directory];
|
||||
ORKDataLogger *logger = _records[logName];
|
||||
if (! logger) {
|
||||
if (!logger) {
|
||||
@throw [NSException exceptionWithName:NSGenericException reason:@"URL is not from a known logger" userInfo:@{@"url":url}];
|
||||
}
|
||||
|
||||
@@ -1271,13 +1282,13 @@ static NSInteger _ORKJSON_terminatorLength = 0;
|
||||
success = NO;
|
||||
}
|
||||
}
|
||||
if (error && [notRemoved count]) {
|
||||
if (error && notRemoved.count) {
|
||||
*error = [NSError errorWithDomain:ORKErrorDomain code:ORKErrorMultipleErrors userInfo:@{@"notRemoved":notRemoved}];
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
- (BOOL)unmarkUploadedFiles:(NSArray<NSURL *> *)fileURLs error:(NSError *__autoreleasing *)error {
|
||||
- (BOOL)unmarkUploadedFiles:(NSArray<NSURL *> *)fileURLs error:(NSError * __autoreleasing *)error {
|
||||
__block BOOL success = YES;
|
||||
dispatch_sync(_queue, ^{
|
||||
success = [self queue_unmarkUploadedFiles:fileURLs error:error];
|
||||
@@ -1285,7 +1296,7 @@ static NSInteger _ORKJSON_terminatorLength = 0;
|
||||
return success;
|
||||
}
|
||||
|
||||
- (BOOL)queue_removeOldAndUploadedLogsToThreshold:(unsigned long long)bytes error:(NSError *__autoreleasing *)error {
|
||||
- (BOOL)queue_removeOldAndUploadedLogsToThreshold:(unsigned long long)bytes error:(NSError **)error {
|
||||
if (bytes == 0) {
|
||||
for (ORKDataLogger *logger in _records) {
|
||||
[logger removeAllFilesWithError:nil];
|
||||
@@ -1300,7 +1311,7 @@ static NSInteger _ORKJSON_terminatorLength = 0;
|
||||
NSFileManager *fileManager = [NSFileManager defaultManager];
|
||||
|
||||
if (totalBytes > bytes) {
|
||||
for (ORKDataLogger *logger in [_records allValues]) {
|
||||
for (ORKDataLogger *logger in _records.allValues) {
|
||||
[logger enumerateLogsAlreadyUploaded:^(NSURL *logFileUrl, BOOL *stop) {
|
||||
unsigned long long fileSize = [[fileManager attributesOfItemAtPath:[logFileUrl path] error:nil] fileSize];
|
||||
if (fileSize > 0) {
|
||||
@@ -1342,7 +1353,7 @@ static NSInteger _ORKJSON_terminatorLength = 0;
|
||||
return (totalBytes <= bytes);
|
||||
}
|
||||
|
||||
- (BOOL)removeOldAndUploadedLogsToThreshold:(unsigned long long)bytes error:(NSError *__autoreleasing *)error {
|
||||
- (BOOL)removeOldAndUploadedLogsToThreshold:(unsigned long long)bytes error:(NSError * __autoreleasing *)error {
|
||||
__block BOOL success = YES;
|
||||
dispatch_sync(_queue, ^{
|
||||
success = [self queue_removeOldAndUploadedLogsToThreshold:bytes error:error];
|
||||
@@ -1353,7 +1364,7 @@ static NSInteger _ORKJSON_terminatorLength = 0;
|
||||
- (void)queue_updateBytes {
|
||||
unsigned long long pending = 0;
|
||||
unsigned long long uploaded = 0;
|
||||
for (ORKDataLogger *logger in [_records allValues]) {
|
||||
for (ORKDataLogger *logger in _records.allValues) {
|
||||
pending += logger.pendingBytes;
|
||||
uploaded += logger.uploadedBytes;
|
||||
}
|
||||
@@ -1367,14 +1378,14 @@ static NSInteger _ORKJSON_terminatorLength = 0;
|
||||
if (exceededPendingThreshold && !_pendingUploadDelegateSent) {
|
||||
[self.delegate dataLoggerManager:self pendingUploadBytesReachedThreshold:pending];
|
||||
_pendingUploadDelegateSent = YES;
|
||||
} else if (! exceededPendingThreshold) {
|
||||
} else if (!exceededPendingThreshold) {
|
||||
_pendingUploadDelegateSent = NO;
|
||||
}
|
||||
|
||||
if (exceededTotalThreshold && !_totalBytesDelegateSent) {
|
||||
[self.delegate dataLoggerManager:self totalBytesReachedThreshold:(pending + uploaded)];
|
||||
_totalBytesDelegateSent = YES;
|
||||
} else if (! exceededTotalThreshold) {
|
||||
} else if (!exceededTotalThreshold) {
|
||||
_totalBytesDelegateSent = NO;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,8 +28,11 @@
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
|
||||
@import UIKit;
|
||||
#import <ResearchKit/ORKRecorder.h>
|
||||
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class CMDeviceMotion;
|
||||
|
||||
@@ -30,13 +30,16 @@
|
||||
|
||||
|
||||
#import "ORKDeviceMotionRecorder.h"
|
||||
#import "ORKHelpers.h"
|
||||
#import "ORKRecorder_Internal.h"
|
||||
#import "ORKRecorder_Private.h"
|
||||
|
||||
#import "ORKDataLogger.h"
|
||||
#import <CoreMotion/CoreMotion.h>
|
||||
|
||||
#import "ORKRecorder_Internal.h"
|
||||
|
||||
#import "ORKHelpers_Internal.h"
|
||||
#import "CMDeviceMotion+ORKJSONDictionary.h"
|
||||
|
||||
@import CoreMotion;
|
||||
|
||||
|
||||
@interface ORKDeviceMotionRecorder () {
|
||||
ORKDataLogger *_logger;
|
||||
@@ -84,17 +87,17 @@
|
||||
- (void)start {
|
||||
[super start];
|
||||
|
||||
if (! _logger) {
|
||||
NSError *err = nil;
|
||||
_logger = [self makeJSONDataLoggerWithError:&err];
|
||||
if (! _logger) {
|
||||
[self finishRecordingWithError:err];
|
||||
if (!_logger) {
|
||||
NSError *error = nil;
|
||||
_logger = [self makeJSONDataLoggerWithError:&error];
|
||||
if (!_logger) {
|
||||
[self finishRecordingWithError:error];
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
self.motionManager = [self createMotionManager];
|
||||
self.motionManager.deviceMotionUpdateInterval = 1.0/_frequency;
|
||||
self.motionManager.deviceMotionUpdateInterval = 1.0 / _frequency;
|
||||
|
||||
self.uptime = [NSProcessInfo processInfo].systemUptime;
|
||||
|
||||
@@ -165,11 +168,6 @@
|
||||
@end
|
||||
|
||||
|
||||
@interface ORKDeviceMotionRecorderConfiguration ()
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation ORKDeviceMotionRecorderConfiguration
|
||||
|
||||
#pragma clang diagnostic push
|
||||
|
||||
@@ -30,12 +30,16 @@
|
||||
|
||||
|
||||
#import "ORKFitnessContentView.h"
|
||||
#import "ORKHelpers.h"
|
||||
#import <CoreMotion/CoreMotion.h>
|
||||
#import "ORKSkin.h"
|
||||
|
||||
#import "ORKActiveStepQuantityView.h"
|
||||
#import "ORKTintedImageView.h"
|
||||
|
||||
#import "ORKHelpers_Internal.h"
|
||||
#import "ORKSkin.h"
|
||||
|
||||
@import CoreMotion;
|
||||
@import HealthKit;
|
||||
|
||||
|
||||
// #define LAYOUT_TEST 1
|
||||
// #define LAYOUT_DEBUG 1
|
||||
@@ -48,8 +52,6 @@
|
||||
ORKTintedImageView *_imageView;
|
||||
NSLengthFormatter *_lengthFormatter;
|
||||
NSLayoutConstraint *_imageRatioConstraint;
|
||||
ORKScreenType _screenType;
|
||||
NSArray *_constraints;
|
||||
NSLayoutConstraint *_topConstraint;
|
||||
}
|
||||
|
||||
@@ -69,13 +71,12 @@
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
_screenType = ORKScreenTypeiPhone4;
|
||||
_timerLabel = [ORKQuantityLabel new];
|
||||
_quantityPairView = [ORKQuantityPairView new];
|
||||
_imageSpacer1 = [UIView new];
|
||||
[_imageSpacer1 setTranslatesAutoresizingMaskIntoConstraints:NO];
|
||||
_imageSpacer1.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
_imageSpacer2 = [UIView new];
|
||||
[_imageSpacer2 setTranslatesAutoresizingMaskIntoConstraints:NO];
|
||||
_imageSpacer2.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
[self addSubview:_imageSpacer1];
|
||||
[self addSubview:_imageSpacer2];
|
||||
[self heartRateView].image = [UIImage imageNamed:@"heart-fitness" inBundle:[NSBundle bundleForClass:[self class]] compatibleWithTraitCollection:nil];
|
||||
@@ -96,7 +97,7 @@
|
||||
self.hasDistance = _hasDistance;
|
||||
|
||||
#if LAYOUT_TEST
|
||||
self.timeLeft = 60*5;
|
||||
self.timeLeft = 60 * 5;
|
||||
self.hasHeartRate = YES;
|
||||
self.hasDistance = YES;
|
||||
self.distanceInMeters = 100;
|
||||
@@ -113,7 +114,7 @@
|
||||
[self addSubview:_quantityPairView];
|
||||
[self addSubview:_imageView];
|
||||
[self addSubview:_timerLabel];
|
||||
[self setupConstraints];
|
||||
[self setUpConstraints];
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(localeDidChange:) name:NSCurrentLocaleDidChangeNotification object:nil];
|
||||
|
||||
@@ -139,105 +140,114 @@
|
||||
|
||||
- (void)willMoveToWindow:(UIWindow *)newWindow {
|
||||
[super willMoveToWindow:newWindow];
|
||||
_screenType = ORKGetVerticalScreenTypeForWindow(newWindow);
|
||||
[self updateConstraintConstants];
|
||||
[self updateConstraintConstantsForWindow:newWindow];
|
||||
}
|
||||
|
||||
- (void)updateConstraintConstants {
|
||||
ORKScreenType screenType = _screenType;
|
||||
const CGFloat CaptionBaselineToTimerTop = ORKGetMetricForScreenType(ORKScreenMetricCaptionBaselineToFitnessTimerTop, screenType);
|
||||
const CGFloat CaptionBaselineToStepViewTop = ORKGetMetricForScreenType(ORKScreenMetricLearnMoreBaselineToStepViewTop, screenType);
|
||||
- (void)updateConstraintConstantsForWindow:(UIWindow *)window {
|
||||
const CGFloat CaptionBaselineToTimerTop = ORKGetMetricForWindow(ORKScreenMetricCaptionBaselineToFitnessTimerTop, window);
|
||||
const CGFloat CaptionBaselineToStepViewTop = ORKGetMetricForWindow(ORKScreenMetricLearnMoreBaselineToStepViewTop, window);
|
||||
_topConstraint.constant = (CaptionBaselineToTimerTop - CaptionBaselineToStepViewTop);
|
||||
}
|
||||
|
||||
- (void)setupConstraints {
|
||||
- (void)setUpConstraints {
|
||||
NSMutableArray *constraints = [NSMutableArray array];
|
||||
NSDictionary *views = NSDictionaryOfVariableBindings(_timerLabel, _imageView, _quantityPairView, _imageSpacer1, _imageSpacer2);
|
||||
[constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[_timerLabel][_imageSpacer1(>=0)][_imageView]"
|
||||
options:NSLayoutFormatAlignAllCenterX
|
||||
metrics:nil
|
||||
views:views]];
|
||||
|
||||
[constraints addObjectsFromArray:
|
||||
[NSLayoutConstraint constraintsWithVisualFormat:@"V:[_timerLabel][_imageSpacer1(>=0)][_imageView]"
|
||||
options:NSLayoutFormatAlignAllCenterX
|
||||
metrics:nil
|
||||
views:views]];
|
||||
|
||||
_topConstraint = [NSLayoutConstraint constraintWithItem:_timerLabel
|
||||
attribute:NSLayoutAttributeTop
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:self
|
||||
attribute:NSLayoutAttributeTop
|
||||
multiplier:1
|
||||
constant:0];
|
||||
[self updateConstraintConstants];
|
||||
multiplier:1.0
|
||||
constant:0.0];
|
||||
[constraints addObject:_topConstraint];
|
||||
[constraints addObject:[NSLayoutConstraint
|
||||
constraintWithItem:_timerLabel
|
||||
attribute:NSLayoutAttributeCenterX
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:self
|
||||
attribute:NSLayoutAttributeCenterX
|
||||
multiplier:1
|
||||
constant:0]];
|
||||
|
||||
[constraints addObject:[NSLayoutConstraint constraintWithItem:_timerLabel
|
||||
attribute:NSLayoutAttributeCenterX
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:self
|
||||
attribute:NSLayoutAttributeCenterX
|
||||
multiplier:1.0
|
||||
constant:0.0]];
|
||||
|
||||
[constraints addObject:[NSLayoutConstraint constraintWithItem:_timerLabel
|
||||
attribute:NSLayoutAttributeWidth
|
||||
relatedBy:NSLayoutRelationLessThanOrEqual
|
||||
toItem:self attribute:NSLayoutAttributeWidth
|
||||
multiplier:1
|
||||
constant:0]];
|
||||
multiplier:1.0
|
||||
constant:0.0]];
|
||||
|
||||
[constraints addObject:[NSLayoutConstraint constraintWithItem:_imageView
|
||||
attribute:NSLayoutAttributeWidth
|
||||
relatedBy:NSLayoutRelationLessThanOrEqual
|
||||
toItem:self attribute:NSLayoutAttributeWidth
|
||||
multiplier:1
|
||||
constant:0]];
|
||||
multiplier:1.0
|
||||
constant:0.0]];
|
||||
|
||||
[constraints addObjectsFromArray:
|
||||
[NSLayoutConstraint constraintsWithVisualFormat:@"V:[_imageView][_imageSpacer2(>=0)][_quantityPairView]|"
|
||||
options:(NSLayoutFormatOptions)0
|
||||
metrics:nil
|
||||
views:views]];
|
||||
|
||||
[constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[_imageView][_imageSpacer2(>=0)][_quantityPairView]|"
|
||||
options:0
|
||||
metrics:nil
|
||||
views:views]];
|
||||
[constraints addObject:[NSLayoutConstraint constraintWithItem:_imageSpacer1
|
||||
attribute:NSLayoutAttributeWidth
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:nil
|
||||
attribute:NSLayoutAttributeNotAnAttribute
|
||||
multiplier:1
|
||||
constant:0]];
|
||||
multiplier:1.0
|
||||
constant:0.0]];
|
||||
|
||||
[constraints addObject:[NSLayoutConstraint constraintWithItem:_imageSpacer2
|
||||
attribute:NSLayoutAttributeWidth
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:nil
|
||||
attribute:NSLayoutAttributeNotAnAttribute
|
||||
multiplier:1
|
||||
constant:0]];
|
||||
multiplier:1.0
|
||||
constant:0.0]];
|
||||
|
||||
[constraints addObject:[NSLayoutConstraint constraintWithItem:_imageSpacer1
|
||||
attribute:NSLayoutAttributeHeight
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:_imageSpacer2
|
||||
attribute:NSLayoutAttributeHeight
|
||||
multiplier:1
|
||||
constant:0]];
|
||||
NSLayoutConstraint *constraint1 = [NSLayoutConstraint constraintWithItem:_imageSpacer1
|
||||
attribute:NSLayoutAttributeHeight
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:nil
|
||||
attribute:NSLayoutAttributeNotAnAttribute
|
||||
multiplier:1
|
||||
constant:1000];
|
||||
constraint1.priority = UILayoutPriorityDefaultLow-1;
|
||||
[constraints addObject:constraint1];
|
||||
multiplier:1.0
|
||||
constant:0.0]];
|
||||
|
||||
[constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[_quantityPairView]|"
|
||||
options:(NSLayoutFormatOptions)0
|
||||
metrics:nil
|
||||
views:views]];
|
||||
NSLayoutConstraint *imageSpacerHeightConstraint = [NSLayoutConstraint constraintWithItem:_imageSpacer1
|
||||
attribute:NSLayoutAttributeHeight
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:nil
|
||||
attribute:NSLayoutAttributeNotAnAttribute
|
||||
multiplier:1.0
|
||||
constant:ORKScreenMetricMaxDimension];
|
||||
imageSpacerHeightConstraint.priority = UILayoutPriorityDefaultLow - 1;
|
||||
[constraints addObject:imageSpacerHeightConstraint];
|
||||
|
||||
NSLayoutConstraint *maxWidthConstraint = [NSLayoutConstraint constraintWithItem:self
|
||||
[constraints addObjectsFromArray:
|
||||
[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[_quantityPairView]|"
|
||||
options:(NSLayoutFormatOptions)0
|
||||
metrics:nil
|
||||
views:views]];
|
||||
|
||||
NSLayoutConstraint *maxWidthConstraint = [NSLayoutConstraint constraintWithItem:self
|
||||
attribute:NSLayoutAttributeWidth
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:nil
|
||||
attribute:NSLayoutAttributeNotAnAttribute
|
||||
multiplier:1
|
||||
multiplier:1.0
|
||||
constant:ORKScreenMetricMaxDimension];
|
||||
maxWidthConstraint.priority = UILayoutPriorityRequired-1;
|
||||
maxWidthConstraint.priority = UILayoutPriorityRequired - 1;
|
||||
[constraints addObject:maxWidthConstraint];
|
||||
|
||||
[self addConstraints:constraints];
|
||||
[NSLayoutConstraint activateConstraints:constraints];
|
||||
[self updateConstraintConstantsForWindow:self.window];
|
||||
}
|
||||
|
||||
- (void)setImage:(UIImage *)image {
|
||||
@@ -253,8 +263,8 @@
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:_imageView
|
||||
attribute:NSLayoutAttributeWidth
|
||||
multiplier:size
|
||||
.height/size.width constant:0];
|
||||
multiplier:size.height / size.width
|
||||
constant:0.0];
|
||||
_imageRatioConstraint.active = YES;
|
||||
}
|
||||
}
|
||||
@@ -277,7 +287,7 @@
|
||||
}
|
||||
|
||||
- (void)updateKeylineVisible {
|
||||
[_quantityPairView setKeylineHidden:! (_hasDistance && _hasHeartRate)];
|
||||
[_quantityPairView setKeylineHidden:!(_hasDistance && _hasHeartRate)];
|
||||
}
|
||||
|
||||
- (void)setDistanceInMeters:(double)distanceInMeters {
|
||||
@@ -304,12 +314,12 @@
|
||||
double conversionFactor = 1.0;
|
||||
if ([hkUnit isNull] && (unit == NSLengthFormatterUnitYard)) {
|
||||
hkUnit = [HKUnit footUnit];
|
||||
conversionFactor = 1/3.0;
|
||||
conversionFactor = 1.0 / 3.0;
|
||||
}
|
||||
HKQuantity *quantity = [HKQuantity quantityWithUnit:[HKUnit meterUnit] doubleValue:displayDistance];
|
||||
distanceString = [_lengthFormatter.numberFormatter stringFromNumber:@([quantity doubleValueForUnit:hkUnit]*conversionFactor)];
|
||||
|
||||
[self distanceView].title = [NSString stringWithFormat:ORKLocalizedString(@"FITNESS_DISTANCE_TITLE_FORMAT", nil), unitString];
|
||||
[self distanceView].title = [NSString localizedStringWithFormat:ORKLocalizedString(@"FITNESS_DISTANCE_TITLE_FORMAT", nil), unitString];
|
||||
[self distanceView].value = distanceString;
|
||||
}
|
||||
|
||||
@@ -319,19 +329,18 @@
|
||||
}
|
||||
|
||||
- (void)updateTimerLabel {
|
||||
static NSDateComponentsFormatter *_formatter = nil;
|
||||
static NSDateComponentsFormatter *formatter = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
NSDateComponentsFormatter *fmt = [NSDateComponentsFormatter new];
|
||||
fmt.unitsStyle = NSDateComponentsFormatterUnitsStylePositional;
|
||||
fmt.zeroFormattingBehavior = NSDateComponentsFormatterZeroFormattingBehaviorPad;
|
||||
fmt.allowedUnits = NSCalendarUnitMinute | NSCalendarUnitSecond;
|
||||
_formatter = fmt;
|
||||
formatter = [NSDateComponentsFormatter new];
|
||||
formatter.unitsStyle = NSDateComponentsFormatterUnitsStylePositional;
|
||||
formatter.zeroFormattingBehavior = NSDateComponentsFormatterZeroFormattingBehaviorPad;
|
||||
formatter.allowedUnits = NSCalendarUnitMinute | NSCalendarUnitSecond;
|
||||
});
|
||||
|
||||
NSString *s = [_formatter stringFromTimeInterval:MAX(round(_timeLeft),0)];
|
||||
_timerLabel.text = s;
|
||||
_timerLabel.hidden = (s == nil);
|
||||
NSString *labelString = [formatter stringFromTimeInterval:MAX(round(_timeLeft),0)];
|
||||
_timerLabel.text = labelString;
|
||||
_timerLabel.hidden = (labelString == nil);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -29,10 +29,13 @@
|
||||
*/
|
||||
|
||||
|
||||
#import <ResearchKit/ResearchKit.h>
|
||||
@import Foundation;
|
||||
#import <ResearchKit/ORKDefines.h>
|
||||
#import <ResearchKit/ORKActiveStep.h>
|
||||
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/**
|
||||
Fitness step.
|
||||
|
||||
@@ -45,3 +48,5 @@ ORK_CLASS_AVAILABLE
|
||||
@interface ORKFitnessStep : ORKActiveStep
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
|
||||
|
||||
#import "ORKFitnessStep.h"
|
||||
|
||||
#import "ORKFitnessStepViewController.h"
|
||||
|
||||
|
||||
@@ -52,8 +53,8 @@
|
||||
|
||||
NSTimeInterval const ORKFitnessStepMinimumDuration = 5.0;
|
||||
|
||||
if ( self.stepDuration < ORKFitnessStepMinimumDuration) {
|
||||
@throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"rest duration can not be shorter than %@ seconds.", @(ORKFitnessStepMinimumDuration)] userInfo:nil];
|
||||
if (self.stepDuration < ORKFitnessStepMinimumDuration) {
|
||||
@throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"rest duration cannot be shorter than %@ seconds.", @(ORKFitnessStepMinimumDuration)] userInfo:nil];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,9 +29,13 @@
|
||||
*/
|
||||
|
||||
|
||||
#import <ResearchKit/ResearchKit_Private.h>
|
||||
@import Foundation;
|
||||
#import <ResearchKit/ORKDefines.h>
|
||||
#import <ResearchKit/ORKActiveStepViewController.h>
|
||||
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/**
|
||||
Step view controller corresponding to `ORKFitnessStep`.
|
||||
|
||||
@@ -42,3 +46,5 @@ ORK_CLASS_AVAILABLE
|
||||
@interface ORKFitnessStepViewController : ORKActiveStepViewController
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
@@ -30,19 +30,24 @@
|
||||
|
||||
|
||||
#import "ORKFitnessStepViewController.h"
|
||||
#import "ORKHelpers.h"
|
||||
#import "ORKStep_Private.h"
|
||||
#import "ORKStepViewController_Internal.h"
|
||||
#import "ORKActiveStepViewController_Internal.h"
|
||||
|
||||
#import "ORKActiveStepTimer.h"
|
||||
#import "ORKActiveStepView.h"
|
||||
#import "ORKFitnessContentView.h"
|
||||
#import "ORKVerticalContainerView.h"
|
||||
#import "ORKFitnessStep.h"
|
||||
|
||||
#import "ORKStepViewController_Internal.h"
|
||||
#import "ORKHealthQuantityTypeRecorder.h"
|
||||
#import "ORKPedometerRecorder.h"
|
||||
#import "ORKActiveStepView.h"
|
||||
|
||||
#import "ORKActiveStepViewController_Internal.h"
|
||||
#import "ORKFitnessStep.h"
|
||||
#import "ORKStep_Private.h"
|
||||
|
||||
#import "ORKHelpers_Internal.h"
|
||||
|
||||
|
||||
@interface ORKFitnessStepViewController () <ORKHealthQuantityTypeRecorderDelegate,ORKPedometerRecorderDelegate> {
|
||||
@interface ORKFitnessStepViewController () <ORKHealthQuantityTypeRecorderDelegate, ORKPedometerRecorderDelegate> {
|
||||
NSInteger _intendedSteps;
|
||||
ORKFitnessContentView *_contentView;
|
||||
NSNumberFormatter *_hrFormatter;
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
*/
|
||||
|
||||
|
||||
@import UIKit;
|
||||
#import <ResearchKit/ORKRecorder.h>
|
||||
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
|
||||
|
||||
#import "ORKHealthQuantityTypeRecorder.h"
|
||||
#import "ORKHelpers.h"
|
||||
#import "ORKHelpers_Internal.h"
|
||||
#import "ORKDataLogger.h"
|
||||
#import "ORKRecorder_Private.h"
|
||||
#import "ORKRecorder_Internal.h"
|
||||
@@ -43,12 +43,29 @@
|
||||
HKHealthStore *_healthStore;
|
||||
NSPredicate *_samplePredicate;
|
||||
HKObserverQuery *_observerQuery;
|
||||
NSInteger _anchor;
|
||||
/// Either the HKQueryAnchor object *or* NSUInteger value are tracked since the initializer for
|
||||
/// iOS 8 and iOS 9 use different objects. Only one will actually be referenced in the initalizer.
|
||||
HKQueryAnchor *_anchor;
|
||||
NSUInteger _anchorValue;
|
||||
HKQuantitySample *_lastSample;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#ifdef __IPHONE_10_0
|
||||
/// Add a protocol defining the initializer for iOS 8 apps. This signature was deprecated in iOS 9
|
||||
/// and deleted in iOS 10.
|
||||
@interface HKAnchoredObjectQuery (iOS8)
|
||||
- (instancetype)initWithType:(HKSampleType *)type
|
||||
predicate:(NSPredicate *)predicate
|
||||
anchor:(NSUInteger)anchor
|
||||
limit:(NSUInteger)limit
|
||||
completionHandler:(void (^)(HKAnchoredObjectQuery *query,
|
||||
NSArray<__kindof HKSample *> *results,
|
||||
NSUInteger newAnchor,
|
||||
NSError *error))handler NS_DEPRECATED_IOS(8_0, 9_0);
|
||||
@end
|
||||
#endif
|
||||
|
||||
@implementation ORKHealthQuantityTypeRecorder
|
||||
|
||||
@@ -67,7 +84,8 @@
|
||||
_quantityType = quantityType;
|
||||
_unit = unit;
|
||||
self.continuesInBackground = YES;
|
||||
_anchor = HKAnchoredObjectQueryNoAnchor;
|
||||
_anchorValue = HKAnchoredObjectQueryNoAnchor;
|
||||
_anchor = [HKQueryAnchor anchorFromValue:_anchorValue];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@@ -89,9 +107,9 @@
|
||||
|
||||
static const NSInteger _HealthAnchoredQueryLimit = 100;
|
||||
|
||||
- (void)query_logResults:(NSArray *)results withAnchor:(NSUInteger)newAnchor {
|
||||
- (void)query_logResults:(NSArray *)results withAnchor:(HKQueryAnchor*)newAnchor anchorValue:(NSUInteger)anchorValue {
|
||||
|
||||
NSUInteger resultCount = [results count];
|
||||
NSUInteger resultCount = results.count;
|
||||
if (resultCount == 0) {
|
||||
return;
|
||||
}
|
||||
@@ -103,16 +121,17 @@ static const NSInteger _HealthAnchoredQueryLimit = 100;
|
||||
}];
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self updateMostRecentSample:[results lastObject]];
|
||||
[self updateMostRecentSample:results.lastObject];
|
||||
|
||||
NSError *error = nil;
|
||||
if (! [_logger appendObjects:dictionaries error:&error]) {
|
||||
if (![_logger appendObjects:dictionaries error:&error]) {
|
||||
// Logger writes are unrecoverable
|
||||
[self finishRecordingWithError:error];
|
||||
return;
|
||||
}
|
||||
|
||||
_anchor = newAnchor;
|
||||
_anchorValue = anchorValue;
|
||||
|
||||
if (resultCount == _HealthAnchoredQueryLimit) {
|
||||
// Do another fetch immediately rather than wait for an observation
|
||||
@@ -122,45 +141,68 @@ static const NSInteger _HealthAnchoredQueryLimit = 100;
|
||||
}
|
||||
|
||||
- (void)doFetchNewData {
|
||||
if (! _healthStore || ! _isRecording) {
|
||||
if (!_healthStore || !_isRecording) {
|
||||
return;
|
||||
}
|
||||
NSAssert(_samplePredicate != nil, @"Sample predicate should be non-nil if recording");
|
||||
|
||||
__weak typeof(self) weakSelf = self;
|
||||
HKAnchoredObjectQuery *anchoredQuery = [[HKAnchoredObjectQuery alloc]
|
||||
initWithType:_quantityType
|
||||
predicate:_samplePredicate
|
||||
anchor:_anchor
|
||||
limit:_HealthAnchoredQueryLimit
|
||||
completionHandler:^(HKAnchoredObjectQuery *query, NSArray *results, NSUInteger newAnchor, NSError *error)
|
||||
{
|
||||
if (error) {
|
||||
// An error in the query's not the end of the world: we'll probably get another chance. Just log it.
|
||||
ORK_Log_Debug(@"Anchored query error: %@", error);
|
||||
return;
|
||||
}
|
||||
|
||||
__typeof(self) strongSelf = weakSelf;
|
||||
[strongSelf query_logResults:results withAnchor:newAnchor];
|
||||
|
||||
}];
|
||||
void (^handleResults)(NSArray <__kindof HKSample *> *, HKQueryAnchor *, NSUInteger, NSError *) = ^ (NSArray *results, HKQueryAnchor *newAnchor, NSUInteger newAnchorValue, NSError *error) {
|
||||
if (error) {
|
||||
// An error in the query's not the end of the world: we'll probably get another chance. Just log it.
|
||||
ORK_Log_Warning(@"Anchored query error: %@", error);
|
||||
return;
|
||||
}
|
||||
|
||||
__typeof(self) strongSelf = weakSelf;
|
||||
[strongSelf query_logResults:results withAnchor:newAnchor anchorValue:newAnchorValue];
|
||||
};
|
||||
|
||||
|
||||
HKAnchoredObjectQuery *anchoredQuery;
|
||||
if ([HKAnchoredObjectQuery instancesRespondToSelector:@selector(initWithType:predicate:anchor:limit:resultsHandler:)]) {
|
||||
|
||||
anchoredQuery = [[HKAnchoredObjectQuery alloc] initWithType:_quantityType
|
||||
predicate:_samplePredicate
|
||||
anchor:_anchor
|
||||
limit:_HealthAnchoredQueryLimit
|
||||
resultsHandler:
|
||||
^(HKAnchoredObjectQuery *query, NSArray *sampleObjects, NSArray *deletedObjects, HKQueryAnchor *newAnchor, NSError *error) {
|
||||
handleResults(sampleObjects, newAnchor, 0, error);
|
||||
}];
|
||||
} else if ([HKAnchoredObjectQuery instancesRespondToSelector:@selector(initWithType:predicate:anchor:limit:completionHandler:)]) {
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||
anchoredQuery = [[HKAnchoredObjectQuery alloc] initWithType:_quantityType
|
||||
predicate:_samplePredicate
|
||||
anchor:_anchorValue
|
||||
limit:_HealthAnchoredQueryLimit
|
||||
completionHandler:
|
||||
^(HKAnchoredObjectQuery *query, NSArray<__kindof HKSample *> *results, NSUInteger newAnchor, NSError *error) {
|
||||
handleResults(results, nil, newAnchor, error);
|
||||
}];
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
else {
|
||||
NSAssert(NO, @"Could not instantiate an HKAnchoredObjectQuery.");
|
||||
}
|
||||
|
||||
[_healthStore executeQuery:anchoredQuery];
|
||||
}
|
||||
|
||||
- (void)start {
|
||||
[super start];
|
||||
|
||||
if (! _logger) {
|
||||
if (!_logger) {
|
||||
NSError *err = nil;
|
||||
_logger = [self makeJSONDataLoggerWithError:&err];
|
||||
if (! _logger) {
|
||||
if (!_logger) {
|
||||
[self finishRecordingWithError:err];
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (! [HKHealthStore isHealthDataAvailable]) {
|
||||
if (![HKHealthStore isHealthDataAvailable]) {
|
||||
[self finishRecordingWithError:[NSError errorWithDomain:NSCocoaErrorDomain
|
||||
code:NSFeatureUnsupportedError
|
||||
userInfo:@{@"recorder" : self}]];
|
||||
@@ -178,16 +220,6 @@ static const NSInteger _HealthAnchoredQueryLimit = 100;
|
||||
}
|
||||
}
|
||||
|
||||
[_healthStore enableBackgroundDeliveryForType:_quantityType
|
||||
frequency:HKUpdateFrequencyImmediate
|
||||
withCompletion:^(BOOL success, NSError *error) {
|
||||
|
||||
// Doesn't really matter if this succeeds, but nice if it does.
|
||||
if (! success) {
|
||||
ORK_Log_Debug(@"Failed to enable background delivery: %@", error);
|
||||
}
|
||||
}];
|
||||
|
||||
_lastSample = nil;
|
||||
_samplePredicate = [HKQuery predicateForSamplesWithStartDate:[NSDate date] endDate:nil options:HKQueryOptionStrictStartDate];
|
||||
|
||||
@@ -222,7 +254,7 @@ static const NSInteger _HealthAnchoredQueryLimit = 100;
|
||||
}
|
||||
|
||||
- (void)stop {
|
||||
if (! _isRecording) {
|
||||
if (!_isRecording) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -275,11 +307,6 @@ static const NSInteger _HealthAnchoredQueryLimit = 100;
|
||||
@end
|
||||
|
||||
|
||||
@interface ORKHealthQuantityTypeRecorderConfiguration ()
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation ORKHealthQuantityTypeRecorderConfiguration
|
||||
|
||||
#pragma clang diagnostic push
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
Copyright (c) 2015, Shazino SAS. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
|
||||
@import UIKit;
|
||||
#import "ORKCustomStepView_Internal.h"
|
||||
#import "ORKTypes.h"
|
||||
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@protocol ORKHolePegTestPlaceContentViewDelegate;
|
||||
|
||||
ORK_CLASS_AVAILABLE
|
||||
@interface ORKHolePegTestPlaceContentView : ORKActiveStepCustomView
|
||||
|
||||
- (instancetype)initWithCoder:(NSCoder *)aDecoder NS_UNAVAILABLE;
|
||||
- (instancetype)initWithFrame:(CGRect)frame NS_UNAVAILABLE;
|
||||
- (instancetype)initWithMovingDirection:(ORKBodySagittal)movingDirection rotated:(BOOL)rotated NS_DESIGNATED_INITIALIZER;
|
||||
|
||||
@property (nonatomic, assign) ORKBodySagittal movingDirection;
|
||||
@property (nonatomic, assign) double threshold;
|
||||
@property (nonatomic, assign, getter = isRotated) BOOL rotated;
|
||||
@property (nonatomic, weak) id<ORKHolePegTestPlaceContentViewDelegate> delegate;
|
||||
|
||||
- (void)setProgress:(CGFloat)progress animated:(BOOL)animated;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@protocol ORKHolePegTestPlaceContentViewDelegate <NSObject>
|
||||
|
||||
- (void)holePegTestPlaceDidProgress:(ORKHolePegTestPlaceContentView *)holePegTestPlaceContentView;
|
||||
- (void)holePegTestPlaceDidSucceed:(ORKHolePegTestPlaceContentView *)holePegTestPlaceContentView withDistance:(CGFloat)distance;
|
||||
- (void)holePegTestPlaceDidFail:(ORKHolePegTestPlaceContentView *)holePegTestPlaceContentView;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,399 @@
|
||||
/*
|
||||
Copyright (c) 2015, Shazino SAS. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
|
||||
#import "ORKHolePegTestPlaceContentView.h"
|
||||
|
||||
#import "ORKDirectionView.h"
|
||||
#import "ORKHolePegTestPlaceHoleView.h"
|
||||
#import "ORKHolePegTestPlacePegView.h"
|
||||
|
||||
#import "ORKHelpers_Internal.h"
|
||||
#import "ORKSkin.h"
|
||||
|
||||
|
||||
static const CGFloat ORKOrientationThreshold = 12.0f;
|
||||
static const CGFloat ORKHolePegViewDiameter = 88.0f;
|
||||
#define degreesToRadians(degrees) ((degrees) / 180.0 * M_PI)
|
||||
|
||||
|
||||
@interface ORKHolePegTestPlaceContentView () <UIGestureRecognizerDelegate>
|
||||
|
||||
@property (nonatomic, strong) UIProgressView *progressView;
|
||||
@property (nonatomic, strong) ORKHolePegTestPlacePegView *pegView;
|
||||
@property (nonatomic, strong) ORKHolePegTestPlaceHoleView *holeView;
|
||||
@property (nonatomic, strong) ORKDirectionView *directionView;
|
||||
@property (nonatomic, copy) NSArray *constraints;
|
||||
|
||||
@property (nonatomic, strong) UIPinchGestureRecognizer *pinchRecognizer;
|
||||
@property (nonatomic, strong) UIPanGestureRecognizer *panRecognizer;
|
||||
@property (nonatomic, strong) UIRotationGestureRecognizer *rotationRecognizer;
|
||||
@property (nonatomic, assign, getter = isMovable) BOOL movable;
|
||||
@property (nonatomic, assign, getter = hasMoveEnded) BOOL moveEnded;
|
||||
@property (nonatomic, assign) CGFloat rotation;
|
||||
@property (nonatomic, assign) CGFloat rotationOffset;
|
||||
@property (nonatomic, assign) CGPoint translation;
|
||||
@property (nonatomic, assign) CGPoint translationOffset;
|
||||
@property (nonatomic, assign) CGPoint startPoint;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation ORKHolePegTestPlaceContentView
|
||||
|
||||
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
|
||||
ORKThrowMethodUnavailableException();
|
||||
}
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
ORKThrowMethodUnavailableException();
|
||||
}
|
||||
|
||||
- (instancetype)initWithMovingDirection:(ORKBodySagittal)movingDirection rotated:(BOOL)rotated {
|
||||
self = [super initWithFrame:CGRectZero];
|
||||
if (self) {
|
||||
self.movingDirection = movingDirection;
|
||||
self.rotated = rotated;
|
||||
|
||||
self.progressView = [UIProgressView new];
|
||||
self.progressView.progressTintColor = self.tintColor;
|
||||
[self.progressView setTranslatesAutoresizingMaskIntoConstraints:NO];
|
||||
[self.progressView setAlpha:0];
|
||||
[self addSubview:self.progressView];
|
||||
|
||||
self.holeView = [[ORKHolePegTestPlaceHoleView alloc] initWithFrame:CGRectMake(0, 0, ORKHolePegViewDiameter, ORKHolePegViewDiameter)];
|
||||
self.holeView.rotated = self.isRotated;
|
||||
[self.holeView setTranslatesAutoresizingMaskIntoConstraints:NO];
|
||||
[self addSubview:self.holeView];
|
||||
|
||||
self.pegView = [[ORKHolePegTestPlacePegView alloc] initWithFrame:CGRectMake(0, 0, ORKHolePegViewDiameter, ORKHolePegViewDiameter)];
|
||||
self.pegView.rotated = self.isRotated;
|
||||
[self.pegView setTranslatesAutoresizingMaskIntoConstraints:NO];
|
||||
[self addSubview:self.pegView];
|
||||
|
||||
self.directionView = [[ORKDirectionView alloc] initWithOrientation:(self.movingDirection == ORKBodySagittalLeft) ? ORKBodySagittalRight : ORKBodySagittalLeft];
|
||||
[self.directionView setTranslatesAutoresizingMaskIntoConstraints:NO];
|
||||
[self addSubview:self.directionView];
|
||||
|
||||
[self setTranslatesAutoresizingMaskIntoConstraints:NO];
|
||||
[self setNeedsUpdateConstraints];
|
||||
|
||||
self.movable = NO;
|
||||
self.moveEnded = NO;
|
||||
self.startPoint = CGPointZero;
|
||||
|
||||
self.pinchRecognizer = [[UIPinchGestureRecognizer alloc] initWithTarget:self
|
||||
action:@selector(handlePinch:)];
|
||||
self.pinchRecognizer.delegate = self;
|
||||
[self addGestureRecognizer:self.pinchRecognizer];
|
||||
|
||||
self.panRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self
|
||||
action:@selector(handlePan:)];
|
||||
self.panRecognizer.delegate = self;
|
||||
[self addGestureRecognizer:self.panRecognizer];
|
||||
|
||||
if (rotated) {
|
||||
self.rotationRecognizer = [[UIRotationGestureRecognizer alloc] initWithTarget:self
|
||||
action:@selector(handleRotate:)];
|
||||
self.rotationRecognizer.delegate = self;
|
||||
[self addGestureRecognizer:self.rotationRecognizer];
|
||||
}
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)tintColorDidChange {
|
||||
[super tintColorDidChange];
|
||||
self.progressView.progressTintColor = self.tintColor;
|
||||
}
|
||||
|
||||
- (void)setProgress:(CGFloat)progress animated:(BOOL)animated {
|
||||
[self.progressView setProgress:progress animated:animated];
|
||||
[UIView animateWithDuration:animated ? 0.2 : 0 animations:^{
|
||||
[self.progressView setAlpha:(progress == 0) ? 0 : 1];
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)updateLayoutMargins {
|
||||
CGFloat margin = ORKStandardHorizontalMarginForView(self);
|
||||
self.layoutMargins = (UIEdgeInsets){.left = margin * 2, .right = margin * 2};
|
||||
}
|
||||
|
||||
- (void)setFrame:(CGRect)frame {
|
||||
[super setFrame:frame];
|
||||
[self updateLayoutMargins];
|
||||
}
|
||||
|
||||
- (void)setBounds:(CGRect)bounds {
|
||||
[super setBounds:bounds];
|
||||
[self updateLayoutMargins];
|
||||
}
|
||||
|
||||
- (void)updateConstraints {
|
||||
if ([self.constraints count]) {
|
||||
[NSLayoutConstraint deactivateConstraints:self.constraints];
|
||||
self.constraints = nil;
|
||||
}
|
||||
|
||||
NSMutableArray *constraintsArray = [NSMutableArray array];
|
||||
|
||||
NSDictionary *views = NSDictionaryOfVariableBindings(_progressView, _pegView, _holeView, _directionView);
|
||||
NSDictionary *metrics = @{@"diameter": @(ORKHolePegViewDiameter)};
|
||||
|
||||
[constraintsArray addObjectsFromArray:
|
||||
[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-[_progressView]-|"
|
||||
options:(NSLayoutFormatOptions)0
|
||||
metrics:nil views:views]];
|
||||
|
||||
[constraintsArray addObjectsFromArray:
|
||||
[NSLayoutConstraint constraintsWithVisualFormat:(self.movingDirection == ORKBodySagittalLeft) ? @"H:|-[_pegView]->=0-[_holeView]-|" : @"H:|-[_holeView]->=0-[_pegView]-|"
|
||||
options:NSLayoutFormatAlignAllCenterY
|
||||
metrics:nil views:views]];
|
||||
|
||||
[constraintsArray addObjectsFromArray:
|
||||
[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[_progressView]"
|
||||
options:(NSLayoutFormatOptions)0
|
||||
metrics:nil views:views]];
|
||||
|
||||
[constraintsArray addObjectsFromArray:
|
||||
[NSLayoutConstraint constraintsWithVisualFormat:@"V:|->=0-[_pegView(diameter)]->=0-|"
|
||||
options:(NSLayoutFormatOptions)0
|
||||
metrics:metrics views:views]];
|
||||
|
||||
[constraintsArray addObjectsFromArray:
|
||||
[NSLayoutConstraint constraintsWithVisualFormat:@"V:|->=0-[_holeView]->=0-|"
|
||||
options:(NSLayoutFormatOptions)0
|
||||
metrics:nil views:views]];
|
||||
|
||||
[constraintsArray addObject:[NSLayoutConstraint constraintWithItem:self.pegView
|
||||
attribute:NSLayoutAttributeCenterY
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:self
|
||||
attribute:NSLayoutAttributeCenterY
|
||||
multiplier:1
|
||||
constant:0]];
|
||||
|
||||
[constraintsArray addObject:[NSLayoutConstraint constraintWithItem:self.directionView
|
||||
attribute:NSLayoutAttributeCenterX
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:self
|
||||
attribute:NSLayoutAttributeCenterX
|
||||
multiplier:1
|
||||
constant:0]];
|
||||
|
||||
[constraintsArray addObject:[NSLayoutConstraint constraintWithItem:self.directionView
|
||||
attribute:NSLayoutAttributeCenterY
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:self
|
||||
attribute:NSLayoutAttributeCenterY
|
||||
multiplier:1
|
||||
constant:0]];
|
||||
|
||||
self.constraints = constraintsArray;
|
||||
[self addConstraints:self.constraints];
|
||||
|
||||
[NSLayoutConstraint activateConstraints:self.constraints];
|
||||
[super updateConstraints];
|
||||
}
|
||||
|
||||
#pragma mark - gesture recognizer methods
|
||||
|
||||
- (void)pickupPegWithGestureRecognizer:(UIGestureRecognizer *)gestureRecognizer {
|
||||
CGPoint touch = [gestureRecognizer locationInView:self];
|
||||
CGPoint touch1 = [gestureRecognizer locationOfTouch:0 inView:self];
|
||||
CGPoint touch2 = [gestureRecognizer locationOfTouch:1 inView:self];
|
||||
double distance = hypot(touch1.x - touch2.x, touch1.y - touch2.y);
|
||||
|
||||
if (distance < 3 * CGRectGetWidth(self.pegView.frame) &&
|
||||
CGRectContainsPoint(self.pegView.frame, touch)) {
|
||||
self.movable = YES;
|
||||
} else {
|
||||
self.movable = NO;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)handlePinch:(UIPinchGestureRecognizer *)pinchGestureRecognizer {
|
||||
if ([pinchGestureRecognizer numberOfTouches] == 2) {
|
||||
[self pickupPegWithGestureRecognizer:pinchGestureRecognizer];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)handlePan:(UIPanGestureRecognizer *)panGestureRecognizer {
|
||||
if ([panGestureRecognizer numberOfTouches] != 2 ||
|
||||
panGestureRecognizer.state == UIGestureRecognizerStateEnded ||
|
||||
panGestureRecognizer.state == UIGestureRecognizerStateCancelled ||
|
||||
panGestureRecognizer.state == UIGestureRecognizerStateFailed) {
|
||||
[self resetTransformAtPoint:[panGestureRecognizer locationInView:self]];
|
||||
} else {
|
||||
if (self.isMovable) {
|
||||
self.translation = CGPointMake([panGestureRecognizer translationInView:self].x - self.translationOffset.x,
|
||||
[panGestureRecognizer translationInView:self].y - self.translationOffset.y);
|
||||
[self updateTransformAtPoint:[panGestureRecognizer locationInView:self]];
|
||||
} else {
|
||||
self.translationOffset = CGPointMake([panGestureRecognizer translationInView:self].x - self.translation.x,
|
||||
[panGestureRecognizer translationInView:self].y - self.translation.y);
|
||||
if (CGPointEqualToPoint(self.startPoint, CGPointZero)) {
|
||||
[self pickupPegWithGestureRecognizer:panGestureRecognizer];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)handleRotate:(UIRotationGestureRecognizer *)rotationGestureRecognizer {
|
||||
if ([rotationGestureRecognizer numberOfTouches] != 2 ||
|
||||
rotationGestureRecognizer.state == UIGestureRecognizerStateEnded ||
|
||||
rotationGestureRecognizer.state == UIGestureRecognizerStateCancelled ||
|
||||
rotationGestureRecognizer.state == UIGestureRecognizerStateFailed) {
|
||||
[self resetTransformAtPoint:[rotationGestureRecognizer locationInView:self]];
|
||||
} else {
|
||||
if (self.isMovable) {
|
||||
self.rotation = rotationGestureRecognizer.rotation - self.rotationOffset;
|
||||
[self updateTransformAtPoint:[rotationGestureRecognizer locationInView:self]];
|
||||
} else {
|
||||
self.rotationOffset = rotationGestureRecognizer.rotation - self.rotation;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)updateTransformAtPoint:(CGPoint)point {
|
||||
self.pegView.transform = CGAffineTransformMakeTranslation(self.translation.x, self.translation.y);
|
||||
self.pegView.transform = CGAffineTransformRotate(self.pegView.transform, self.rotation);
|
||||
[self pegViewDidMoveAtPoint:point];
|
||||
}
|
||||
|
||||
- (void)resetTransformAtPoint:(CGPoint)point {
|
||||
if (!self.hasMoveEnded) {
|
||||
self.movable = NO;
|
||||
self.moveEnded = YES;
|
||||
|
||||
self.pinchRecognizer.enabled = NO;
|
||||
self.panRecognizer.enabled = NO;
|
||||
self.rotationRecognizer.enabled = NO;
|
||||
|
||||
BOOL animated = ![self pegViewMoveDidEndAtPoint:point];
|
||||
self.pegView.hidden = !animated;
|
||||
|
||||
[UIView animateWithDuration:animated ? 0.15f : 0.0f
|
||||
delay:0.0f
|
||||
options:UIViewAnimationOptionCurveEaseOut
|
||||
animations:^(){
|
||||
self.pegView.transform = CGAffineTransformIdentity;
|
||||
self.pegView.alpha = 1.0f;
|
||||
}
|
||||
completion:^(BOOL finished){
|
||||
self.rotation = 0.0f;
|
||||
self.rotationOffset = 0.0f;
|
||||
self.translation = CGPointZero;
|
||||
self.translationOffset = CGPointZero;
|
||||
self.pinchRecognizer.enabled = YES;
|
||||
self.panRecognizer.enabled = YES;
|
||||
self.rotationRecognizer.enabled = YES;
|
||||
self.moveEnded = NO;
|
||||
self.pegView.hidden = NO;
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
|
||||
shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
|
||||
return YES;
|
||||
}
|
||||
|
||||
#pragma mark - peg view delegate
|
||||
|
||||
- (void)pegViewDidMoveAtPoint:(CGPoint)point {
|
||||
self.directionView.hidden = YES;
|
||||
|
||||
if (CGPointEqualToPoint(self.startPoint, CGPointZero)) {
|
||||
self.startPoint = point;
|
||||
}
|
||||
|
||||
if ([self.delegate respondsToSelector:@selector(holePegTestPlaceDidProgress:)]) {
|
||||
[self.delegate holePegTestPlaceDidProgress:self];
|
||||
}
|
||||
|
||||
if (self.holeView.isSuccess) {
|
||||
self.holeView.success = NO;
|
||||
}
|
||||
|
||||
if ([self holeViewContainsPegView]) {
|
||||
self.pegView.alpha = 1.0f;
|
||||
} else {
|
||||
self.pegView.alpha = 0.2f;
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)pegViewMoveDidEndAtPoint:(CGPoint)point {
|
||||
self.directionView.hidden = NO;
|
||||
|
||||
BOOL succeeded = NO;
|
||||
if ([self holeViewContainsPegView]) {
|
||||
if ([self.delegate respondsToSelector:@selector(holePegTestPlaceDidSucceed:withDistance:)]) {
|
||||
CGFloat distance = hypotf(point.x - self.startPoint.x, point.y - self.startPoint.y);
|
||||
[self.delegate holePegTestPlaceDidSucceed:self withDistance:distance];
|
||||
}
|
||||
self.holeView.success = YES;
|
||||
succeeded = YES;
|
||||
} else {
|
||||
if ([self.delegate respondsToSelector:@selector(holePegTestPlaceDidFail:)]) {
|
||||
[self.delegate holePegTestPlaceDidFail:self];
|
||||
}
|
||||
self.holeView.success = NO;
|
||||
}
|
||||
self.startPoint = CGPointZero;
|
||||
return succeeded;
|
||||
}
|
||||
|
||||
- (BOOL)holeViewContainsPegView {
|
||||
CGRect detectionFrame = CGRectMake(CGRectGetMidX(self.holeView.frame) - (self.threshold * CGRectGetWidth(self.holeView.frame) / 2),
|
||||
CGRectGetMidY(self.holeView.frame) - (self.threshold * CGRectGetHeight(self.holeView.frame) / 2),
|
||||
self.threshold * CGRectGetWidth(self.holeView.frame),
|
||||
self.threshold * CGRectGetHeight(self.holeView.frame));
|
||||
|
||||
CGPoint pegCenter = CGPointMake(CGRectGetMaxX(self.pegView.frame) - CGRectGetWidth(self.pegView.frame) / 2,
|
||||
CGRectGetMaxY(self.pegView.frame) - CGRectGetHeight(self.pegView.frame) / 2);
|
||||
|
||||
if (CGRectContainsPoint(detectionFrame, pegCenter)) {
|
||||
if (self.isRotated) {
|
||||
double rotation = atan2(self.pegView.transform.b, self.pegView.transform.a);
|
||||
double angle = fmod(fabs(rotation), M_PI_2);
|
||||
if (angle > M_PI_4 - degreesToRadians(ORKOrientationThreshold) &&
|
||||
angle < M_PI_4 + degreesToRadians(ORKOrientationThreshold)) {
|
||||
return YES;
|
||||
}
|
||||
} else {
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
Copyright (c) 2015, Shazino SAS. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
|
||||
@import UIKit;
|
||||
#import "ORKDefines.h"
|
||||
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
ORK_CLASS_AVAILABLE
|
||||
@interface ORKHolePegTestPlaceHoleView : UIView <CAAnimationDelegate>
|
||||
|
||||
@property (nonatomic, assign, getter = isRotated) BOOL rotated;
|
||||
@property (nonatomic, assign, getter = isSuccess) BOOL success;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,173 @@
|
||||
/*
|
||||
Copyright (c) 2015, Shazino SAS. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
|
||||
#import "ORKHolePegTestPlaceHoleView.h"
|
||||
|
||||
#import "ORKHelpers_Internal.h"
|
||||
|
||||
|
||||
static const CGFloat ORKPlaceHoleViewRotation = 45.0f;
|
||||
|
||||
|
||||
@interface ORKHolePegTestPlaceHoleView ()
|
||||
|
||||
@property (nonatomic, strong) CAShapeLayer *checkLayer;
|
||||
@property (nonatomic, strong) CAShapeLayer *crossLayer;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation ORKHolePegTestPlaceHoleView
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame
|
||||
{
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
UIBezierPath *path = [[UIBezierPath alloc] init];
|
||||
[path moveToPoint:CGPointMake(27.7f, 46.9f)];
|
||||
[path addLineToPoint:CGPointMake(36.1f, 56.3f)];
|
||||
[path addLineToPoint:CGPointMake(62.8f, 30.3f)];
|
||||
path.lineCapStyle = kCGLineCapRound;
|
||||
path.lineWidth = 3.6f;
|
||||
|
||||
CAShapeLayer *checkLayer = [CAShapeLayer new];
|
||||
checkLayer.path = path.CGPath;
|
||||
checkLayer.lineWidth = 3.6f;
|
||||
checkLayer.lineCap = kCALineCapRound;
|
||||
checkLayer.lineJoin = kCALineJoinRound;
|
||||
checkLayer.frame = self.layer.bounds;
|
||||
checkLayer.strokeColor = self.tintColor.CGColor;
|
||||
checkLayer.backgroundColor = [UIColor clearColor].CGColor;
|
||||
checkLayer.fillColor = nil;
|
||||
self.checkLayer = checkLayer;
|
||||
|
||||
self.opaque = NO;
|
||||
self.success = NO;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (CGSize)intrinsicContentSize {
|
||||
return CGSizeMake(self.frame.size.width, self.frame.size.height);
|
||||
}
|
||||
|
||||
#pragma mark - drawing method
|
||||
|
||||
- (void)tintColorDidChange {
|
||||
[super tintColorDidChange];
|
||||
self.checkLayer.strokeColor = self.tintColor.CGColor;
|
||||
[self setNeedsDisplay];
|
||||
}
|
||||
|
||||
- (void)setSuccess:(BOOL)success
|
||||
{
|
||||
_success = success;
|
||||
[self.checkLayer removeFromSuperlayer];
|
||||
[self.crossLayer removeFromSuperlayer];
|
||||
[self setNeedsDisplay];
|
||||
}
|
||||
|
||||
- (void)setRotated:(BOOL)rotated
|
||||
{
|
||||
_rotated = rotated;
|
||||
[self setNeedsDisplay];
|
||||
}
|
||||
|
||||
- (void)drawRect:(CGRect)rect {
|
||||
CGContextRef context = UIGraphicsGetCurrentContext();
|
||||
CGContextSaveGState(context);
|
||||
|
||||
CGRect bounds = self.bounds;
|
||||
UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:CGRectInset(bounds, 1.0f, 1.0f)];
|
||||
path.lineWidth = 2.0f;
|
||||
[self.tintColor setStroke];
|
||||
[path stroke];
|
||||
|
||||
if (self.isSuccess) {
|
||||
[self.layer addSublayer:self.checkLayer];
|
||||
|
||||
CAMediaTimingFunction *timing = [[CAMediaTimingFunction alloc] initWithControlPoints:0.180739998817444
|
||||
:0
|
||||
:0.577960014343262
|
||||
:0.918200016021729];
|
||||
|
||||
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
|
||||
[animation setTimingFunction:timing];
|
||||
[animation setFillMode:kCAFillModeBoth];
|
||||
animation.fromValue = @(0);
|
||||
animation.toValue = @(1);
|
||||
animation.duration = 0.3f;
|
||||
animation.delegate = self;
|
||||
[self.checkLayer addAnimation:animation forKey:@"strokeEnd"];
|
||||
} else if (self.isRotated) {
|
||||
UIBezierPath *crossPath = [[UIBezierPath alloc] init];
|
||||
[crossPath moveToPoint:CGPointMake(CGRectGetWidth(bounds) * 7/16, CGRectGetHeight(bounds) * 1/4)];
|
||||
[crossPath addLineToPoint:CGPointMake(CGRectGetWidth(bounds) * 7/16, CGRectGetHeight(bounds) * 7/16)];
|
||||
[crossPath addLineToPoint:CGPointMake(CGRectGetWidth(bounds) * 1/4, CGRectGetHeight(bounds) * 7/16)];
|
||||
[crossPath addLineToPoint:CGPointMake(CGRectGetWidth(bounds) * 1/4, CGRectGetHeight(bounds) * 9/16)];
|
||||
[crossPath addLineToPoint:CGPointMake(CGRectGetWidth(bounds) * 7/16, CGRectGetHeight(bounds) * 9/16)];
|
||||
[crossPath addLineToPoint:CGPointMake(CGRectGetWidth(bounds) * 7/16, CGRectGetHeight(bounds) * 3/4)];
|
||||
[crossPath addLineToPoint:CGPointMake(CGRectGetWidth(bounds) * 9/16, CGRectGetHeight(bounds) * 3/4)];
|
||||
[crossPath addLineToPoint:CGPointMake(CGRectGetWidth(bounds) * 9/16, CGRectGetHeight(bounds) * 9/16)];
|
||||
[crossPath addLineToPoint:CGPointMake(CGRectGetWidth(bounds) * 3/4, CGRectGetHeight(bounds) * 9/16)];
|
||||
[crossPath addLineToPoint:CGPointMake(CGRectGetWidth(bounds) * 3/4, CGRectGetHeight(bounds) * 7/16)];
|
||||
[crossPath addLineToPoint:CGPointMake(CGRectGetWidth(bounds) * 9/16, CGRectGetHeight(bounds) * 7/16)];
|
||||
[crossPath addLineToPoint:CGPointMake(CGRectGetWidth(bounds) * 9/16, CGRectGetHeight(bounds) * 1/4)];
|
||||
[crossPath closePath];
|
||||
|
||||
CAShapeLayer *crossLayer = [[CAShapeLayer alloc] init];
|
||||
crossLayer.path = crossPath.CGPath;
|
||||
crossLayer.bounds = CGPathGetBoundingBox(crossLayer.path);
|
||||
crossLayer.anchorPoint = CGPointMake(0.5, 0.5);
|
||||
crossLayer.fillColor = self.tintColor.CGColor;
|
||||
|
||||
CATransform3D transform = CATransform3DMakeTranslation(CGRectGetMidX(bounds), CGRectGetMidY(bounds), 1);
|
||||
transform = CATransform3DRotate(transform, ORKPlaceHoleViewRotation * (M_PI / 180), 0, 0, 1);
|
||||
crossLayer.transform = transform;
|
||||
|
||||
self.crossLayer = crossLayer;
|
||||
|
||||
[self.layer addSublayer:self.crossLayer];
|
||||
}
|
||||
|
||||
CGContextRestoreGState(context);
|
||||
}
|
||||
|
||||
- (void)animationDidStop:(CAAnimation *)theAnimation finished:(BOOL)flag {
|
||||
ORKWeakTypeOf(self) weakSelf = self;
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.7f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
ORKStrongTypeOf(self) strongSelf = weakSelf;
|
||||
strongSelf.success = NO;
|
||||
});
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
Copyright (c) 2015, Shazino SAS. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
|
||||
@import UIKit;
|
||||
#import "ORKDefines.h"
|
||||
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
ORK_CLASS_AVAILABLE
|
||||
@interface ORKHolePegTestPlacePegView : UIView
|
||||
|
||||
@property (nonatomic, assign, getter = isRotated) BOOL rotated;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
Copyright (c) 2015, Shazino SAS. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
|
||||
#import "ORKHolePegTestPlacePegView.h"
|
||||
|
||||
|
||||
@implementation ORKHolePegTestPlacePegView
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame
|
||||
{
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
self.opaque = NO;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (CGSize)intrinsicContentSize {
|
||||
return CGSizeMake(self.frame.size.width, self.frame.size.height);
|
||||
}
|
||||
|
||||
#pragma mark - drawing method
|
||||
|
||||
- (void)tintColorDidChange {
|
||||
[super tintColorDidChange];
|
||||
[self setNeedsDisplay];
|
||||
}
|
||||
|
||||
- (void)setRotated:(BOOL)rotated
|
||||
{
|
||||
_rotated = rotated;
|
||||
[self setNeedsDisplay];
|
||||
}
|
||||
|
||||
- (void)drawRect:(CGRect)rect {
|
||||
CGContextRef context = UIGraphicsGetCurrentContext();
|
||||
CGContextSaveGState(context);
|
||||
|
||||
CGRect bounds = self.bounds;
|
||||
UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:bounds];
|
||||
|
||||
if (self.isRotated) {
|
||||
[path moveToPoint:CGPointMake(CGRectGetWidth(bounds) * 7/16, CGRectGetHeight(bounds) * 1/4)];
|
||||
[path addLineToPoint:CGPointMake(CGRectGetWidth(bounds) * 7/16, CGRectGetHeight(bounds) * 7/16)];
|
||||
[path addLineToPoint:CGPointMake(CGRectGetWidth(bounds) * 1/4, CGRectGetHeight(bounds) * 7/16)];
|
||||
[path addLineToPoint:CGPointMake(CGRectGetWidth(bounds) * 1/4, CGRectGetHeight(bounds) * 9/16)];
|
||||
[path addLineToPoint:CGPointMake(CGRectGetWidth(bounds) * 7/16, CGRectGetHeight(bounds) * 9/16)];
|
||||
[path addLineToPoint:CGPointMake(CGRectGetWidth(bounds) * 7/16, CGRectGetHeight(bounds) * 3/4)];
|
||||
[path addLineToPoint:CGPointMake(CGRectGetWidth(bounds) * 9/16, CGRectGetHeight(bounds) * 3/4)];
|
||||
[path addLineToPoint:CGPointMake(CGRectGetWidth(bounds) * 9/16, CGRectGetHeight(bounds) * 9/16)];
|
||||
[path addLineToPoint:CGPointMake(CGRectGetWidth(bounds) * 3/4, CGRectGetHeight(bounds) * 9/16)];
|
||||
[path addLineToPoint:CGPointMake(CGRectGetWidth(bounds) * 3/4, CGRectGetHeight(bounds) * 7/16)];
|
||||
[path addLineToPoint:CGPointMake(CGRectGetWidth(bounds) * 9/16, CGRectGetHeight(bounds) * 7/16)];
|
||||
[path addLineToPoint:CGPointMake(CGRectGetWidth(bounds) * 9/16, CGRectGetHeight(bounds) * 1/4)];
|
||||
[path closePath];
|
||||
}
|
||||
|
||||
[self.tintColor setFill];
|
||||
[path fill];
|
||||
|
||||
CGContextRestoreGState(context);
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
Copyright (c) 2015, Shazino SAS. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
|
||||
@import Foundation;
|
||||
#import <ResearchKit/ORKDefines.h>
|
||||
#import <ResearchKit/ORKActiveStep.h>
|
||||
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
ORK_CLASS_AVAILABLE
|
||||
@interface ORKHolePegTestPlaceStep : ORKActiveStep
|
||||
|
||||
@property (nonatomic, assign) ORKBodySagittal movingDirection;
|
||||
@property (nonatomic, assign, getter = isDominantHandTested) BOOL dominantHandTested;
|
||||
@property (nonatomic, assign) NSInteger numberOfPegs;
|
||||
@property (nonatomic, assign) double threshold;
|
||||
@property (nonatomic, assign, getter = isRotated) BOOL rotated;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
Copyright (c) 2015, Shazino SAS. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
|
||||
#import "ORKHolePegTestPlaceStep.h"
|
||||
#import "ORKHolePegTestPlaceStepViewController.h"
|
||||
#import "ORKHelpers_Internal.h"
|
||||
|
||||
|
||||
@implementation ORKHolePegTestPlaceStep
|
||||
|
||||
+ (Class)stepViewControllerClass {
|
||||
return [ORKHolePegTestPlaceStepViewController class];
|
||||
}
|
||||
|
||||
- (instancetype)initWithIdentifier:(NSString *)identifier {
|
||||
self = [super initWithIdentifier:identifier];
|
||||
if (self) {
|
||||
self.shouldShowDefaultTimer = NO;
|
||||
self.shouldContinueOnFinish = YES;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)validateParameters {
|
||||
[super validateParameters];
|
||||
|
||||
int const ORKHolePegTestMinimumNumberOfPegs = 1;
|
||||
|
||||
double const ORKHolePegTestMinimumThreshold = 0.0f;
|
||||
double const ORKHolePegTestMaximumThreshold = 1.0f;
|
||||
|
||||
NSTimeInterval const ORKHolePegTestMinimumDuration = 1.0f;
|
||||
|
||||
if (self.movingDirection != ORKBodySagittalLeft &&
|
||||
self.movingDirection != ORKBodySagittalRight) {
|
||||
@throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"moving direction should be left or right."] userInfo:nil];
|
||||
}
|
||||
|
||||
if (self.numberOfPegs < ORKHolePegTestMinimumNumberOfPegs) {
|
||||
@throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"number of pegs must be greater than or equal to %@.", @(ORKHolePegTestMinimumNumberOfPegs)] userInfo:nil];
|
||||
}
|
||||
|
||||
if (self.threshold < ORKHolePegTestMinimumThreshold ||
|
||||
self.threshold > ORKHolePegTestMaximumThreshold) {
|
||||
@throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"threshold must be greater than or equal to %@ and lower or equal to %@.", @(ORKHolePegTestMinimumThreshold), @(ORKHolePegTestMaximumThreshold)] userInfo:nil];
|
||||
}
|
||||
|
||||
if (self.stepDuration < ORKHolePegTestMinimumDuration) {
|
||||
@throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"duration cannot be shorter than %@ seconds.", @(ORKHolePegTestMinimumDuration)] userInfo:nil];
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)allowsBackNavigation {
|
||||
return NO;
|
||||
}
|
||||
|
||||
+ (BOOL)supportsSecureCoding {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
|
||||
self = [super initWithCoder:aDecoder];
|
||||
if (self) {
|
||||
ORK_DECODE_ENUM(aDecoder, movingDirection);
|
||||
ORK_DECODE_BOOL(aDecoder, dominantHandTested);
|
||||
ORK_DECODE_INTEGER(aDecoder, numberOfPegs);
|
||||
ORK_DECODE_DOUBLE(aDecoder, threshold);
|
||||
ORK_DECODE_BOOL(aDecoder, rotated);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)encodeWithCoder:(NSCoder *)aCoder {
|
||||
[super encodeWithCoder:aCoder];
|
||||
ORK_ENCODE_ENUM(aCoder, movingDirection);
|
||||
ORK_ENCODE_BOOL(aCoder, dominantHandTested);
|
||||
ORK_ENCODE_INTEGER(aCoder, numberOfPegs);
|
||||
ORK_ENCODE_DOUBLE(aCoder, threshold);
|
||||
ORK_ENCODE_BOOL(aCoder, rotated);
|
||||
}
|
||||
|
||||
- (instancetype)copyWithZone:(NSZone *)zone {
|
||||
__typeof(self) step = [super copyWithZone:zone];
|
||||
step.movingDirection = self.movingDirection;
|
||||
step.dominantHandTested = self.dominantHandTested;
|
||||
step.numberOfPegs = self.numberOfPegs;
|
||||
step.threshold = self.threshold;
|
||||
step.rotated = self.rotated;
|
||||
return step;
|
||||
}
|
||||
|
||||
- (NSUInteger)hash {
|
||||
return [super hash] ^ self.movingDirection ^ self.dominantHandTested ^ self.numberOfPegs ^ (NSInteger)(self.threshold * 100) ^ self.rotated;
|
||||
}
|
||||
|
||||
- (BOOL)isEqual:(id)object {
|
||||
BOOL isParentSame = [super isEqual:object];
|
||||
|
||||
__typeof(self) castObject = object;
|
||||
return (isParentSame &&
|
||||
(self.movingDirection == castObject.movingDirection) &&
|
||||
(self.dominantHandTested == castObject.dominantHandTested) &&
|
||||
(self.numberOfPegs == castObject.numberOfPegs) &&
|
||||
(self.threshold == castObject.threshold) &&
|
||||
(self.rotated == castObject.rotated));
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
Copyright (c) 2015, Shazino SAS. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
|
||||
@import UIKit;
|
||||
#import <ResearchKit/ORKDefines.h>
|
||||
#import <ResearchKit/ORKActiveStepViewController.h>
|
||||
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
ORK_CLASS_AVAILABLE
|
||||
@interface ORKHolePegTestPlaceStepViewController : ORKActiveStepViewController
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,181 @@
|
||||
/*
|
||||
Copyright (c) 2015, Shazino SAS. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
|
||||
#import "ORKHolePegTestPlaceStepViewController.h"
|
||||
|
||||
#import "ORKActiveStepView.h"
|
||||
#import "ORKHolePegTestPlaceContentView.h"
|
||||
|
||||
#import "ORKActiveStepViewController_Internal.h"
|
||||
#import "ORKStepViewController_Internal.h"
|
||||
#import "ORKTaskViewController.h"
|
||||
|
||||
#import "ORKHolePegTestPlaceStep.h"
|
||||
#import "ORKNavigableOrderedTask.h"
|
||||
#import "ORKResult.h"
|
||||
|
||||
#import "ORKHelpers_Internal.h"
|
||||
|
||||
|
||||
@interface ORKHolePegTestPlaceStepViewController () <ORKHolePegTestPlaceContentViewDelegate>
|
||||
|
||||
@property (nonatomic, strong) NSMutableArray *samples;
|
||||
@property (nonatomic, strong) ORKHolePegTestPlaceContentView *holePegTestPlaceContentView;
|
||||
@property (nonatomic, assign) NSTimeInterval sampleStart;
|
||||
@property (nonatomic, assign) NSUInteger successes;
|
||||
@property (nonatomic, assign) NSUInteger failures;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation ORKHolePegTestPlaceStepViewController
|
||||
|
||||
- (instancetype)initWithStep:(ORKStep *)step {
|
||||
self = [super initWithStep:step];
|
||||
if (self) {
|
||||
self.suspendIfInactive = YES;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (ORKHolePegTestPlaceStep *)holePegTestPlaceStep {
|
||||
return (ORKHolePegTestPlaceStep *)self.step;
|
||||
}
|
||||
|
||||
- (void)initializeInternalButtonItems {
|
||||
[super initializeInternalButtonItems];
|
||||
|
||||
// Don't show next button
|
||||
self.internalContinueButtonItem = nil;
|
||||
self.internalDoneButtonItem = nil;
|
||||
}
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
|
||||
self.holePegTestPlaceContentView = [[ORKHolePegTestPlaceContentView alloc] initWithMovingDirection:[self holePegTestPlaceStep].movingDirection
|
||||
rotated:[self holePegTestPlaceStep].rotated];
|
||||
self.holePegTestPlaceContentView.threshold = [self holePegTestPlaceStep].threshold;
|
||||
self.holePegTestPlaceContentView.delegate = self;
|
||||
self.activeStepView.activeCustomView = self.holePegTestPlaceContentView;
|
||||
self.activeStepView.stepViewFillsAvailableSpace = YES;
|
||||
}
|
||||
|
||||
#pragma mark - step life cycle methods
|
||||
|
||||
- (void)start {
|
||||
self.successes = 0;
|
||||
self.failures = 0;
|
||||
self.samples = [NSMutableArray array];
|
||||
[self.holePegTestPlaceContentView setProgress:0.001f animated:NO];
|
||||
|
||||
[super start];
|
||||
}
|
||||
|
||||
#pragma mark - result methods
|
||||
|
||||
- (ORKStepResult *)result {
|
||||
ORKStepResult *sResult = [super result];
|
||||
|
||||
NSMutableArray *results = [NSMutableArray arrayWithArray:sResult.results];
|
||||
|
||||
ORKHolePegTestResult *holePegTestResult = [[ORKHolePegTestResult alloc] initWithIdentifier:self.step.identifier];
|
||||
holePegTestResult.movingDirection = [self holePegTestPlaceStep].movingDirection;
|
||||
holePegTestResult.dominantHandTested = [self holePegTestPlaceStep].isDominantHandTested;
|
||||
holePegTestResult.numberOfPegs = [self holePegTestPlaceStep].numberOfPegs;
|
||||
holePegTestResult.threshold = [self holePegTestPlaceStep].threshold;
|
||||
holePegTestResult.rotated = [self holePegTestPlaceStep].isRotated;
|
||||
holePegTestResult.totalSuccesses = self.successes;
|
||||
holePegTestResult.totalFailures = self.failures;
|
||||
holePegTestResult.totalTime = [self holePegTestPlaceStep].stepDuration - self.timeRemaining;
|
||||
double totalDistance = 0.0;
|
||||
for (ORKHolePegTestSample *sample in self.samples) {
|
||||
totalDistance += sample.distance;
|
||||
}
|
||||
holePegTestResult.totalDistance = totalDistance;
|
||||
holePegTestResult.samples = self.samples;
|
||||
|
||||
[results addObject:holePegTestResult];
|
||||
|
||||
sResult.results = [results copy];
|
||||
|
||||
return sResult;
|
||||
}
|
||||
|
||||
- (void)saveSampleWithDistance:(CGFloat)distance {
|
||||
ORKHolePegTestSample *sample = [[ORKHolePegTestSample alloc] init];
|
||||
sample.time = CACurrentMediaTime() - self.sampleStart;
|
||||
sample.distance = distance;
|
||||
self.sampleStart = CACurrentMediaTime();
|
||||
|
||||
[self.samples addObject:sample];
|
||||
}
|
||||
|
||||
#pragma mark - hole peg test content view delegate
|
||||
|
||||
- (NSString *)stepTitle {
|
||||
NSString *title = ([self holePegTestPlaceStep].movingDirection == ORKBodySagittalLeft) ? ORKLocalizedString(@"HOLE_PEG_TEST_PLACE_INSTRUCTION_LEFT_HAND", nil) : ORKLocalizedString(@"HOLE_PEG_TEST_PLACE_INSTRUCTION_RIGHT_HAND", nil);
|
||||
return title;
|
||||
}
|
||||
|
||||
- (void)holePegTestPlaceDidProgress:(ORKHolePegTestPlaceContentView *)holePegTestPlaceContentView {
|
||||
if (!self.isStarted) {
|
||||
self.sampleStart = CACurrentMediaTime();
|
||||
[self start];
|
||||
}
|
||||
|
||||
[self.activeStepView updateTitle:[self stepTitle]
|
||||
text:ORKLocalizedString(@"HOLE_PEG_TEST_TEXT_2", nil)];
|
||||
}
|
||||
|
||||
- (void)holePegTestPlaceDidSucceed:(ORKHolePegTestPlaceContentView *)holePegTestPlaceContentView withDistance:(CGFloat)distance {
|
||||
self.successes++;
|
||||
|
||||
[self saveSampleWithDistance:distance];
|
||||
|
||||
[holePegTestPlaceContentView setProgress:((CGFloat)self.successes / [self holePegTestPlaceStep].numberOfPegs) animated:YES];
|
||||
[self.activeStepView updateTitle:[self stepTitle]
|
||||
text:ORKLocalizedString(@"HOLE_PEG_TEST_TEXT", nil)];
|
||||
|
||||
if (self.successes >= [self holePegTestPlaceStep].numberOfPegs) {
|
||||
[((ORKNavigableOrderedTask *)self.taskViewController.task) removeNavigationRuleForTriggerStepIdentifier:[self holePegTestPlaceStep].identifier];
|
||||
[self finish];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)holePegTestPlaceDidFail:(ORKHolePegTestPlaceContentView *)holePegTestPlaceContentView {
|
||||
self.failures++;
|
||||
|
||||
[self.activeStepView updateTitle:[self stepTitle]
|
||||
text:ORKLocalizedString(@"HOLE_PEG_TEST_TEXT", nil)];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
Copyright (c) 2015, Shazino SAS. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
|
||||
@import UIKit;
|
||||
#import "ORKCustomStepView_Internal.h"
|
||||
#import "ORKTypes.h"
|
||||
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@protocol ORKHolePegTestRemoveContentViewDelegate;
|
||||
|
||||
ORK_CLASS_AVAILABLE
|
||||
@interface ORKHolePegTestRemoveContentView : ORKActiveStepCustomView
|
||||
|
||||
- (instancetype)initWithCoder:(NSCoder *)aDecoder NS_UNAVAILABLE;
|
||||
- (instancetype)initWithFrame:(CGRect)frame NS_UNAVAILABLE;
|
||||
- (instancetype)initWithMovingDirection:(ORKBodySagittal)movingDirection NS_DESIGNATED_INITIALIZER;
|
||||
|
||||
@property (nonatomic, assign) ORKBodySagittal movingDirection;
|
||||
@property (nonatomic, assign) double threshold;
|
||||
@property (nonatomic, weak) id<ORKHolePegTestRemoveContentViewDelegate> delegate;
|
||||
|
||||
- (void)setProgress:(CGFloat)progress animated:(BOOL)animated;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@protocol ORKHolePegTestRemoveContentViewDelegate <NSObject>
|
||||
|
||||
- (void)holePegTestRemoveDidProgress:(ORKHolePegTestRemoveContentView *)holePegTestRemoveContentView;
|
||||
- (void)holePegTestRemoveDidSucceed:(ORKHolePegTestRemoveContentView *)holePegTestRemoveContentView withDistance:(CGFloat)distance;
|
||||
- (void)holePegTestRemoveDidFail:(ORKHolePegTestRemoveContentView *)holePegTestRemoveContentView;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,358 @@
|
||||
/*
|
||||
Copyright (c) 2015, Shazino SAS. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
|
||||
#import "ORKHolePegTestRemoveContentView.h"
|
||||
|
||||
#import "ORKDirectionView.h"
|
||||
#import "ORKHolePegTestRemovePegView.h"
|
||||
#import "ORKSeparatorView.h"
|
||||
|
||||
#import "ORKHelpers_Internal.h"
|
||||
#import "ORKSkin.h"
|
||||
|
||||
|
||||
static const CGFloat PegViewDiameter = 88.0f;
|
||||
static const CGFloat PegViewSeparatorWidth = 2.0f;
|
||||
|
||||
|
||||
@interface ORKHolePegTestRemoveContentView () <UIGestureRecognizerDelegate>
|
||||
|
||||
@property (nonatomic, strong) UIProgressView *progressView;
|
||||
@property (nonatomic, strong) ORKHolePegTestRemovePegView *pegView;
|
||||
@property (nonatomic, strong) ORKSeparatorView *separatorView;
|
||||
@property (nonatomic, strong) ORKDirectionView *directionView;
|
||||
@property (nonatomic, strong) UIView *container;
|
||||
@property (nonatomic, copy) NSArray *constraints;
|
||||
|
||||
@property (nonatomic, strong) UIPinchGestureRecognizer *pinchRecognizer;
|
||||
@property (nonatomic, strong) UIPanGestureRecognizer *panRecognizer;
|
||||
@property (nonatomic, assign, getter = isMovable) BOOL movable;
|
||||
@property (nonatomic, assign, getter = hasMoveEnded) BOOL moveEnded;
|
||||
@property (nonatomic, assign) CGPoint translation;
|
||||
@property (nonatomic, assign) CGPoint translationOffset;
|
||||
@property (nonatomic, assign) CGPoint startPoint;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation ORKHolePegTestRemoveContentView
|
||||
|
||||
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
|
||||
ORKThrowMethodUnavailableException();
|
||||
}
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
ORKThrowMethodUnavailableException();
|
||||
}
|
||||
|
||||
- (instancetype)initWithMovingDirection:(ORKBodySagittal)movingDirection {
|
||||
self = [super initWithFrame:CGRectZero];
|
||||
if (self) {
|
||||
self.movingDirection = movingDirection;
|
||||
self.opaque = NO;
|
||||
|
||||
self.container = [UIView new];
|
||||
self.container.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
|
||||
self.progressView = [UIProgressView new];
|
||||
self.progressView.progressTintColor = self.tintColor;
|
||||
[self.progressView setTranslatesAutoresizingMaskIntoConstraints:NO];
|
||||
[self.progressView setAlpha:0];
|
||||
[self addSubview:self.progressView];
|
||||
|
||||
self.pegView = [[ORKHolePegTestRemovePegView alloc] initWithFrame:CGRectMake(0, 0, PegViewDiameter, PegViewDiameter)];
|
||||
[self.pegView setTranslatesAutoresizingMaskIntoConstraints:NO];
|
||||
[self.container addSubview:self.pegView];
|
||||
|
||||
self.separatorView = [[ORKSeparatorView alloc] init];
|
||||
[self.separatorView setTranslatesAutoresizingMaskIntoConstraints:NO];
|
||||
[self.container addSubview:self.separatorView];
|
||||
|
||||
self.directionView = [[ORKDirectionView alloc] initWithOrientation:(self.movingDirection == ORKBodySagittalLeft) ? ORKBodySagittalRight : ORKBodySagittalLeft];
|
||||
[self.directionView setTranslatesAutoresizingMaskIntoConstraints:NO];
|
||||
[self addSubview:self.directionView];
|
||||
|
||||
[self addSubview:self.container];
|
||||
|
||||
[self setTranslatesAutoresizingMaskIntoConstraints:NO];
|
||||
[self setNeedsUpdateConstraints];
|
||||
|
||||
self.movable = NO;
|
||||
self.moveEnded = NO;
|
||||
self.startPoint = CGPointZero;
|
||||
|
||||
self.pinchRecognizer = [[UIPinchGestureRecognizer alloc] initWithTarget:self
|
||||
action:@selector(handlePinch:)];
|
||||
self.pinchRecognizer.delegate = self;
|
||||
[self addGestureRecognizer:self.pinchRecognizer];
|
||||
|
||||
self.panRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self
|
||||
action:@selector(handlePan:)];
|
||||
self.panRecognizer.delegate = self;
|
||||
[self addGestureRecognizer:self.panRecognizer];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)tintColorDidChange {
|
||||
[super tintColorDidChange];
|
||||
self.progressView.progressTintColor = self.tintColor;
|
||||
}
|
||||
|
||||
- (void)setProgress:(CGFloat)progress animated:(BOOL)animated {
|
||||
[self.progressView setProgress:progress animated:animated];
|
||||
[UIView animateWithDuration:animated ? 0.2 : 0 animations:^{
|
||||
[self.progressView setAlpha:(progress == 0) ? 0 : 1];
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)updateLayoutMargins {
|
||||
CGFloat margin = ORKStandardHorizontalMarginForView(self);
|
||||
self.layoutMargins = (UIEdgeInsets){.left = margin * 2, .right = margin * 2};
|
||||
}
|
||||
|
||||
- (void)setFrame:(CGRect)frame {
|
||||
[super setFrame:frame];
|
||||
[self updateLayoutMargins];
|
||||
}
|
||||
|
||||
- (void)setBounds:(CGRect)bounds {
|
||||
[super setBounds:bounds];
|
||||
[self updateLayoutMargins];
|
||||
}
|
||||
|
||||
- (void)updateConstraints {
|
||||
if ([self.constraints count]) {
|
||||
[NSLayoutConstraint deactivateConstraints:self.constraints];
|
||||
self.constraints = nil;
|
||||
}
|
||||
|
||||
NSMutableArray *constraintsArray = [NSMutableArray array];
|
||||
|
||||
NSDictionary *views = NSDictionaryOfVariableBindings(_progressView, _container, _pegView, _separatorView, _directionView);
|
||||
NSDictionary *metrics = @{@"diameter": @(PegViewDiameter), @"separator": @(PegViewSeparatorWidth), @"margin": @((1 + self.threshold) * PegViewDiameter)};
|
||||
|
||||
[constraintsArray addObjectsFromArray:
|
||||
[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-[_progressView]-|"
|
||||
options:(NSLayoutFormatOptions)0
|
||||
metrics:nil views:views]];
|
||||
|
||||
[constraintsArray addObjectsFromArray:
|
||||
[NSLayoutConstraint constraintsWithVisualFormat:(self.movingDirection == ORKBodySagittalLeft) ? @"H:|-[_pegView(diameter)]->=0-[_separatorView(separator)]-(margin)-|" : @"H:|-(margin)-[_separatorView(separator)]->=0-[_pegView(diameter)]-|"
|
||||
options:NSLayoutFormatAlignAllCenterY
|
||||
metrics:metrics views:views]];
|
||||
|
||||
[constraintsArray addObjectsFromArray:
|
||||
[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[_container]|"
|
||||
options:(NSLayoutFormatOptions)0
|
||||
metrics:nil views:views]];
|
||||
|
||||
[constraintsArray addObjectsFromArray:
|
||||
[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-(>=0)-[_pegView(diameter)]-(>=0)-|"
|
||||
options:(NSLayoutFormatOptions)0
|
||||
metrics:metrics views:views]];
|
||||
|
||||
[constraintsArray addObjectsFromArray:
|
||||
[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-[_separatorView]-|"
|
||||
options:(NSLayoutFormatOptions)0
|
||||
metrics:nil views:views]];
|
||||
|
||||
[constraintsArray addObjectsFromArray:
|
||||
[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[_progressView][_container]|"
|
||||
options:(NSLayoutFormatOptions)0
|
||||
metrics:metrics views:views]];
|
||||
|
||||
[constraintsArray addObject:[NSLayoutConstraint constraintWithItem:self.directionView
|
||||
attribute:NSLayoutAttributeCenterX
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:self
|
||||
attribute:NSLayoutAttributeCenterX
|
||||
multiplier:1
|
||||
constant:0]];
|
||||
|
||||
[constraintsArray addObject:[NSLayoutConstraint constraintWithItem:self.directionView
|
||||
attribute:NSLayoutAttributeCenterY
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:self
|
||||
attribute:NSLayoutAttributeCenterY
|
||||
multiplier:1
|
||||
constant:0]];
|
||||
|
||||
self.constraints = constraintsArray;
|
||||
[self addConstraints:self.constraints];
|
||||
|
||||
[NSLayoutConstraint activateConstraints:self.constraints];
|
||||
[super updateConstraints];
|
||||
}
|
||||
|
||||
#pragma mark - gesture recognizer methods
|
||||
|
||||
- (void)pickupPegWithGestureRecognizer:(UIGestureRecognizer *)gestureRecognizer {
|
||||
CGPoint touch = [gestureRecognizer locationInView:self];
|
||||
CGPoint touch1 = [gestureRecognizer locationOfTouch:0 inView:self];
|
||||
CGPoint touch2 = [gestureRecognizer locationOfTouch:1 inView:self];
|
||||
double distance = hypot(touch1.x - touch2.x, touch1.y - touch2.y);
|
||||
|
||||
if (distance < 3 * CGRectGetWidth(self.pegView.frame) &&
|
||||
CGRectContainsPoint(self.pegView.frame, touch)) {
|
||||
self.movable = YES;
|
||||
} else {
|
||||
self.movable = NO;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)handlePinch:(UIPinchGestureRecognizer *)pinchGestureRecognizer {
|
||||
if ([pinchGestureRecognizer numberOfTouches] == 2) {
|
||||
[self pickupPegWithGestureRecognizer:pinchGestureRecognizer];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)handlePan:(UIPanGestureRecognizer *)panGestureRecognizer {
|
||||
if ([panGestureRecognizer numberOfTouches] != 2 ||
|
||||
panGestureRecognizer.state == UIGestureRecognizerStateEnded ||
|
||||
panGestureRecognizer.state == UIGestureRecognizerStateCancelled ||
|
||||
panGestureRecognizer.state == UIGestureRecognizerStateFailed) {
|
||||
[self resetTransformAtPoint:[panGestureRecognizer locationInView:self]];
|
||||
} else {
|
||||
if (self.isMovable) {
|
||||
self.translation = CGPointMake([panGestureRecognizer translationInView:self].x - self.translationOffset.x,
|
||||
[panGestureRecognizer translationInView:self].y - self.translationOffset.y);
|
||||
[self updateTransformAtPoint:[panGestureRecognizer locationInView:self]];
|
||||
} else {
|
||||
self.translationOffset = CGPointMake([panGestureRecognizer translationInView:self].x - self.translation.x,
|
||||
[panGestureRecognizer translationInView:self].y - self.translation.y);
|
||||
if (CGPointEqualToPoint(self.startPoint, CGPointZero)) {
|
||||
[self pickupPegWithGestureRecognizer:panGestureRecognizer];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)updateTransformAtPoint:(CGPoint)point {
|
||||
self.pegView.transform = CGAffineTransformMakeTranslation(self.translation.x, self.translation.y);
|
||||
[self pegViewDidMoveAtPoint:point];
|
||||
}
|
||||
|
||||
- (void)resetTransformAtPoint:(CGPoint)point {
|
||||
if (!self.hasMoveEnded) {
|
||||
self.movable = NO;
|
||||
self.moveEnded = YES;
|
||||
|
||||
self.pinchRecognizer.enabled = NO;
|
||||
self.panRecognizer.enabled = NO;
|
||||
|
||||
BOOL animated = ![self pegViewMoveDidEndAtPoint:point];
|
||||
|
||||
[UIView animateWithDuration:animated ? 0.15f : 0.0f
|
||||
delay:animated ? 0.0f : 0.30f
|
||||
options:UIViewAnimationOptionCurveEaseOut
|
||||
animations:^(){
|
||||
self.pegView.transform = CGAffineTransformIdentity;
|
||||
self.pegView.alpha = 1.0f;
|
||||
}
|
||||
completion:^(BOOL finished){
|
||||
self.translation = CGPointZero;
|
||||
self.translationOffset = CGPointZero;
|
||||
self.pinchRecognizer.enabled = YES;
|
||||
self.panRecognizer.enabled = YES;
|
||||
self.moveEnded = NO;
|
||||
self.pegView.hidden = NO;
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
|
||||
shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
|
||||
return YES;
|
||||
}
|
||||
|
||||
#pragma mark - peg view delegate
|
||||
|
||||
- (void)pegViewDidMoveAtPoint:(CGPoint)point {
|
||||
self.directionView.hidden = YES;
|
||||
|
||||
if (CGPointEqualToPoint(self.startPoint, CGPointZero)) {
|
||||
self.startPoint = point;
|
||||
}
|
||||
|
||||
if ([self.delegate respondsToSelector:@selector(holePegTestRemoveDidProgress:)]) {
|
||||
[self.delegate holePegTestRemoveDidProgress:self];
|
||||
}
|
||||
|
||||
if (self.pegView.isSuccess) {
|
||||
self.pegView.success = NO;
|
||||
}
|
||||
|
||||
if ([self pegViewBehindLine]) {
|
||||
self.pegView.alpha = 1.0f;
|
||||
} else {
|
||||
self.pegView.alpha = 0.2f;
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)pegViewMoveDidEndAtPoint:(CGPoint)point {
|
||||
self.directionView.hidden = NO;
|
||||
|
||||
BOOL succeeded = NO;
|
||||
if ([self pegViewBehindLine]) {
|
||||
if ([self.delegate respondsToSelector:@selector(holePegTestRemoveDidSucceed:withDistance:)]) {
|
||||
CGFloat distance = hypotf(point.x - self.startPoint.x, point.y - self.startPoint.y);
|
||||
[self.delegate holePegTestRemoveDidSucceed:self withDistance:distance];
|
||||
}
|
||||
self.pegView.success = YES;
|
||||
succeeded = YES;
|
||||
} else {
|
||||
if ([self.delegate respondsToSelector:@selector(holePegTestRemoveDidFail:)]) {
|
||||
[self.delegate holePegTestRemoveDidFail:self];
|
||||
}
|
||||
self.pegView.success = NO;
|
||||
}
|
||||
self.startPoint = CGPointZero;
|
||||
return succeeded;
|
||||
}
|
||||
|
||||
- (BOOL)pegViewBehindLine {
|
||||
if (self.movingDirection == ORKBodySagittalLeft) {
|
||||
if (CGRectGetMinX(self.pegView.frame) > CGRectGetMaxX(self.separatorView.frame)) {
|
||||
return YES;
|
||||
} else {
|
||||
return NO;
|
||||
}
|
||||
} else {
|
||||
if (CGRectGetMaxX(self.pegView.frame) < CGRectGetMinX(self.separatorView.frame)) {
|
||||
return YES;
|
||||
} else {
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
Copyright (c) 2015, Shazino SAS. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
|
||||
@import UIKit;
|
||||
#import "ORKDefines.h"
|
||||
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
ORK_CLASS_AVAILABLE
|
||||
@interface ORKHolePegTestRemovePegView : UIView <CAAnimationDelegate>
|
||||
|
||||
@property (nonatomic, assign, getter = isSuccess) BOOL success;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
Copyright (c) 2015, Shazino SAS. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
|
||||
#import "ORKHolePegTestRemovePegView.h"
|
||||
|
||||
|
||||
@interface ORKHolePegTestRemovePegView ()
|
||||
|
||||
@property (nonatomic, strong) CAShapeLayer *checkLayer;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation ORKHolePegTestRemovePegView
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame
|
||||
{
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
UIBezierPath *path = [[UIBezierPath alloc] init];
|
||||
[path moveToPoint:CGPointMake(27.7f, 46.9f)];
|
||||
[path addLineToPoint:CGPointMake(36.1f, 56.3f)];
|
||||
[path addLineToPoint:CGPointMake(62.8f, 30.3f)];
|
||||
path.lineCapStyle = kCGLineCapRound;
|
||||
path.lineWidth = 3.6f;
|
||||
|
||||
CAShapeLayer *checkLayer = [CAShapeLayer new];
|
||||
checkLayer.path = path.CGPath;
|
||||
checkLayer.lineWidth = 3.6f;
|
||||
checkLayer.lineCap = kCALineCapRound;
|
||||
checkLayer.lineJoin = kCALineJoinRound;
|
||||
checkLayer.frame = self.layer.bounds;
|
||||
checkLayer.strokeColor = [UIColor whiteColor].CGColor;
|
||||
checkLayer.backgroundColor = [UIColor clearColor].CGColor;
|
||||
checkLayer.fillColor = nil;
|
||||
self.checkLayer = checkLayer;
|
||||
|
||||
self.opaque = NO;
|
||||
self.success = NO;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)tintColorDidChange {
|
||||
[super tintColorDidChange];
|
||||
[self setNeedsDisplay];
|
||||
}
|
||||
|
||||
#pragma mark - drawing method
|
||||
|
||||
- (void)setSuccess:(BOOL)success
|
||||
{
|
||||
_success = success;
|
||||
[self.checkLayer removeFromSuperlayer];
|
||||
[self setNeedsDisplay];
|
||||
}
|
||||
|
||||
- (void)drawRect:(CGRect)rect {
|
||||
CGContextRef context = UIGraphicsGetCurrentContext();
|
||||
CGContextSaveGState(context);
|
||||
|
||||
CGRect bounds = self.bounds;
|
||||
UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:bounds];
|
||||
path.lineWidth = 2.0f;
|
||||
[self.tintColor setFill];
|
||||
[path fill];
|
||||
|
||||
if (self.isSuccess) {
|
||||
[self.layer addSublayer:self.checkLayer];
|
||||
|
||||
CAMediaTimingFunction *timing = [[CAMediaTimingFunction alloc] initWithControlPoints:0.180739998817444
|
||||
:0
|
||||
:0.577960014343262
|
||||
:0.918200016021729];
|
||||
|
||||
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
|
||||
[animation setTimingFunction:timing];
|
||||
[animation setFillMode:kCAFillModeBoth];
|
||||
animation.fromValue = @(0);
|
||||
animation.toValue = @(1);
|
||||
animation.duration = 0.25f;
|
||||
animation.delegate = self;
|
||||
[self.checkLayer addAnimation:animation forKey:@"strokeEnd"];
|
||||
}
|
||||
|
||||
CGContextRestoreGState(context);
|
||||
}
|
||||
|
||||
- (void)animationDidStop:(CAAnimation *)theAnimation finished:(BOOL)flag {
|
||||
self.success = NO;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
Copyright (c) 2015, Shazino SAS. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
|
||||
@import Foundation;
|
||||
#import <ResearchKit/ORKDefines.h>
|
||||
#import <ResearchKit/ORKActiveStep.h>
|
||||
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
ORK_CLASS_AVAILABLE
|
||||
@interface ORKHolePegTestRemoveStep : ORKActiveStep
|
||||
|
||||
@property (nonatomic, assign) ORKBodySagittal movingDirection;
|
||||
@property (nonatomic, assign, getter = isDominantHandTested) BOOL dominantHandTested;
|
||||
@property (nonatomic, assign) NSInteger numberOfPegs;
|
||||
@property (nonatomic, assign) double threshold;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
Copyright (c) 2015, Shazino SAS. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
|
||||
#import "ORKHolePegTestRemoveStep.h"
|
||||
|
||||
#import "ORKHolePegTestRemoveStepViewController.h"
|
||||
|
||||
#import "ORKHelpers_Internal.h"
|
||||
|
||||
|
||||
@implementation ORKHolePegTestRemoveStep
|
||||
|
||||
+ (Class)stepViewControllerClass {
|
||||
return [ORKHolePegTestRemoveStepViewController class];
|
||||
}
|
||||
|
||||
- (instancetype)initWithIdentifier:(NSString *)identifier {
|
||||
self = [super initWithIdentifier:identifier];
|
||||
if (self) {
|
||||
self.shouldShowDefaultTimer = NO;
|
||||
self.shouldContinueOnFinish = YES;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)validateParameters {
|
||||
[super validateParameters];
|
||||
|
||||
int const ORKHolePegTestMinimumNumberOfPegs = 1;
|
||||
|
||||
double const ORKHolePegTestMinimumThreshold = 0.0f;
|
||||
double const ORKHolePegTestMaximumThreshold = 1.0f;
|
||||
|
||||
NSTimeInterval const ORKHolePegTestMinimumDuration = 1.0f;
|
||||
|
||||
if (self.movingDirection != ORKBodySagittalLeft &&
|
||||
self.movingDirection != ORKBodySagittalRight) {
|
||||
@throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"moving direction should be left or right."] userInfo:nil];
|
||||
}
|
||||
|
||||
if (self.numberOfPegs < ORKHolePegTestMinimumNumberOfPegs) {
|
||||
@throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"number of pegs must be greater than or equal to %@.", @(ORKHolePegTestMinimumNumberOfPegs)] userInfo:nil];
|
||||
}
|
||||
|
||||
if (self.threshold < ORKHolePegTestMinimumThreshold ||
|
||||
self.threshold > ORKHolePegTestMaximumThreshold) {
|
||||
@throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"threshold must be greater than or equal to %@ and lower or equal to %@.", @(ORKHolePegTestMinimumThreshold), @(ORKHolePegTestMaximumThreshold)] userInfo:nil];
|
||||
}
|
||||
|
||||
if (self.stepDuration < ORKHolePegTestMinimumDuration) {
|
||||
@throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"duration cannot be shorter than %@ seconds.", @(ORKHolePegTestMinimumDuration)] userInfo:nil];
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)allowsBackNavigation {
|
||||
return NO;
|
||||
}
|
||||
|
||||
+ (BOOL)supportsSecureCoding {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
|
||||
self = [super initWithCoder:aDecoder];
|
||||
if (self) {
|
||||
ORK_DECODE_ENUM(aDecoder, movingDirection);
|
||||
ORK_DECODE_BOOL(aDecoder, dominantHandTested);
|
||||
ORK_DECODE_INTEGER(aDecoder, numberOfPegs);
|
||||
ORK_DECODE_DOUBLE(aDecoder, threshold);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)encodeWithCoder:(NSCoder *)aCoder {
|
||||
[super encodeWithCoder:aCoder];
|
||||
ORK_ENCODE_ENUM(aCoder, movingDirection);
|
||||
ORK_ENCODE_BOOL(aCoder, dominantHandTested);
|
||||
ORK_ENCODE_INTEGER(aCoder, numberOfPegs);
|
||||
ORK_ENCODE_DOUBLE(aCoder, threshold);
|
||||
}
|
||||
|
||||
- (instancetype)copyWithZone:(NSZone *)zone {
|
||||
__typeof(self) step = [super copyWithZone:zone];
|
||||
step.movingDirection = self.movingDirection;
|
||||
step.dominantHandTested = self.dominantHandTested;
|
||||
step.numberOfPegs = self.numberOfPegs;
|
||||
step.threshold = self.threshold;
|
||||
return step;
|
||||
}
|
||||
|
||||
- (NSUInteger)hash {
|
||||
return [super hash] ^ self.movingDirection ^ self.dominantHandTested ^ self.numberOfPegs ^ (NSInteger)(self.threshold * 100);
|
||||
}
|
||||
|
||||
- (BOOL)isEqual:(id)object {
|
||||
BOOL isParentSame = [super isEqual:object];
|
||||
|
||||
__typeof(self) castObject = object;
|
||||
return (isParentSame &&
|
||||
(self.movingDirection == castObject.movingDirection) &&
|
||||
(self.dominantHandTested == castObject.dominantHandTested) &&
|
||||
(self.numberOfPegs == castObject.numberOfPegs) &&
|
||||
(self.threshold == castObject.threshold));
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
Copyright (c) 2015, Shazino SAS. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
|
||||
@import UIKit;
|
||||
#import <ResearchKit/ORKDefines.h>
|
||||
#import <ResearchKit/ORKActiveStepViewController.h>
|
||||
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
ORK_CLASS_AVAILABLE
|
||||
@interface ORKHolePegTestRemoveStepViewController : ORKActiveStepViewController
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,180 @@
|
||||
/*
|
||||
Copyright (c) 2015, Shazino SAS. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
|
||||
#import "ORKHolePegTestRemoveStepViewController.h"
|
||||
|
||||
#import "ORKActiveStepView.h"
|
||||
#import "ORKHolePegTestRemoveContentView.h"
|
||||
|
||||
#import "ORKActiveStepViewController_Internal.h"
|
||||
#import "ORKStepViewController_Internal.h"
|
||||
#import "ORKTaskViewController.h"
|
||||
|
||||
#import "ORKHolePegTestRemoveStep.h"
|
||||
|
||||
#import "ORKHelpers_Internal.h"
|
||||
#import "ORKResult.h"
|
||||
|
||||
|
||||
@interface ORKHolePegTestRemoveStepViewController () <ORKHolePegTestRemoveContentViewDelegate>
|
||||
|
||||
@property (nonatomic, strong) NSMutableArray *samples;
|
||||
@property (nonatomic, strong) ORKHolePegTestRemoveContentView *holePegTestRemoveContentView;
|
||||
@property (nonatomic, assign) NSTimeInterval sampleStart;
|
||||
@property (nonatomic, assign) NSUInteger successes;
|
||||
@property (nonatomic, assign) NSUInteger failures;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation ORKHolePegTestRemoveStepViewController
|
||||
|
||||
- (instancetype)initWithStep:(ORKStep *)step {
|
||||
self = [super initWithStep:step];
|
||||
if (self) {
|
||||
self.suspendIfInactive = YES;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (ORKHolePegTestRemoveStep *)holePegTestRemoveStep {
|
||||
return (ORKHolePegTestRemoveStep *)self.step;
|
||||
}
|
||||
|
||||
- (void)initializeInternalButtonItems {
|
||||
[super initializeInternalButtonItems];
|
||||
|
||||
// Don't show next button
|
||||
self.internalContinueButtonItem = nil;
|
||||
self.internalDoneButtonItem = nil;
|
||||
}
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
|
||||
self.holePegTestRemoveContentView = [[ORKHolePegTestRemoveContentView alloc] initWithMovingDirection:[self holePegTestRemoveStep].movingDirection];
|
||||
self.holePegTestRemoveContentView.threshold = [self holePegTestRemoveStep].threshold;
|
||||
self.holePegTestRemoveContentView.delegate = self;
|
||||
self.activeStepView.activeCustomView = self.holePegTestRemoveContentView;
|
||||
self.activeStepView.stepViewFillsAvailableSpace = YES;
|
||||
|
||||
NSString *identifier = [[self holePegTestRemoveStep].identifier stringByReplacingOccurrencesOfString:@"remove" withString:@"place"];
|
||||
NSTimeInterval placeStepDuration = ((ORKHolePegTestResult *)[[self.taskViewController.result stepResultForStepIdentifier:identifier].results firstObject]).totalTime;
|
||||
[self holePegTestRemoveStep].stepDuration -= placeStepDuration;
|
||||
|
||||
[self start];
|
||||
}
|
||||
|
||||
#pragma mark - step life cycle methods
|
||||
|
||||
- (void)start {
|
||||
self.sampleStart = CACurrentMediaTime();
|
||||
self.successes = 0;
|
||||
self.failures = 0;
|
||||
self.samples = [NSMutableArray array];
|
||||
[self.holePegTestRemoveContentView setProgress:0.001f animated:NO];
|
||||
|
||||
[super start];
|
||||
}
|
||||
|
||||
#pragma mark - result methods
|
||||
|
||||
- (ORKStepResult *)result {
|
||||
ORKStepResult *sResult = [super result];
|
||||
|
||||
NSMutableArray *results = [NSMutableArray arrayWithArray:sResult.results];
|
||||
|
||||
ORKHolePegTestResult *holePegTestResult = [[ORKHolePegTestResult alloc] initWithIdentifier:self.step.identifier];
|
||||
holePegTestResult.movingDirection = [self holePegTestRemoveStep].movingDirection;
|
||||
holePegTestResult.dominantHandTested = [self holePegTestRemoveStep].isDominantHandTested;
|
||||
holePegTestResult.numberOfPegs = [self holePegTestRemoveStep].numberOfPegs;
|
||||
holePegTestResult.threshold = [self holePegTestRemoveStep].threshold;
|
||||
holePegTestResult.rotated = NO;
|
||||
holePegTestResult.totalSuccesses = self.successes;
|
||||
holePegTestResult.totalFailures = self.failures;
|
||||
holePegTestResult.totalTime = [self holePegTestRemoveStep].stepDuration - self.timeRemaining;
|
||||
double totalDistance = 0.0;
|
||||
for (ORKHolePegTestSample *sample in self.samples) {
|
||||
totalDistance += sample.distance;
|
||||
}
|
||||
holePegTestResult.totalDistance = totalDistance;
|
||||
holePegTestResult.samples = self.samples;
|
||||
|
||||
[results addObject:holePegTestResult];
|
||||
|
||||
sResult.results = [results copy];
|
||||
|
||||
return sResult;
|
||||
}
|
||||
|
||||
- (void)saveSampleWithDistance:(CGFloat)distance {
|
||||
ORKHolePegTestSample *sample = [[ORKHolePegTestSample alloc] init];
|
||||
sample.time = CACurrentMediaTime() - self.sampleStart;
|
||||
sample.distance = distance;
|
||||
self.sampleStart = CACurrentMediaTime();
|
||||
|
||||
[self.samples addObject:sample];
|
||||
}
|
||||
|
||||
#pragma mark - hole peg test content view delegate
|
||||
|
||||
- (NSString *)stepTitle {
|
||||
NSString *title = ([self holePegTestRemoveStep].movingDirection == ORKBodySagittalLeft) ? ORKLocalizedString(@"HOLE_PEG_TEST_REMOVE_INSTRUCTION_RIGHT_HAND", nil) : ORKLocalizedString(@"HOLE_PEG_TEST_REMOVE_INSTRUCTION_LEFT_HAND", nil);
|
||||
return title;
|
||||
}
|
||||
|
||||
- (void)holePegTestRemoveDidProgress:(ORKHolePegTestRemoveContentView *)holePegTestRemoveContentView {
|
||||
[self.activeStepView updateTitle:[self stepTitle]
|
||||
text:ORKLocalizedString(@"HOLE_PEG_TEST_TEXT_2", nil)];
|
||||
}
|
||||
|
||||
- (void)holePegTestRemoveDidSucceed:(ORKHolePegTestRemoveContentView *)holePegTestRemoveContentView withDistance:(CGFloat)distance {
|
||||
self.successes++;
|
||||
|
||||
[self saveSampleWithDistance:distance];
|
||||
|
||||
[holePegTestRemoveContentView setProgress:((CGFloat)self.successes / [self holePegTestRemoveStep].numberOfPegs) animated:YES];
|
||||
[self.activeStepView updateTitle:[self stepTitle]
|
||||
text:ORKLocalizedString(@"HOLE_PEG_TEST_TEXT", nil)];
|
||||
|
||||
if (self.successes >= [self holePegTestRemoveStep].numberOfPegs) {
|
||||
[self finish];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)holePegTestRemoveDidFail:(ORKHolePegTestRemoveContentView *)holePegTestRemoveContentView {
|
||||
self.failures++;
|
||||
|
||||
[self.activeStepView updateTitle:[self stepTitle]
|
||||
text:ORKLocalizedString(@"HOLE_PEG_TEST_TEXT", nil)];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -29,8 +29,9 @@
|
||||
*/
|
||||
|
||||
|
||||
@import UIKit;
|
||||
@import CoreLocation;
|
||||
#import <ResearchKit/ORKRecorder.h>
|
||||
#import <CoreLocation/CoreLocation.h>
|
||||
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@@ -30,11 +30,14 @@
|
||||
|
||||
|
||||
#import "ORKLocationRecorder.h"
|
||||
#import <CoreLocation/CoreLocation.h>
|
||||
#import "CLLocation+ORKJSONDictionary.h"
|
||||
|
||||
#import "ORKDataLogger.h"
|
||||
|
||||
#import "ORKRecorder_Internal.h"
|
||||
#import "ORKRecorder_Private.h"
|
||||
|
||||
#import "CLLocation+ORKJSONDictionary.h"
|
||||
|
||||
#import <CoreLocation/CoreLocation.h>
|
||||
|
||||
|
||||
@interface ORKLocationRecorder () <CLLocationManagerDelegate> {
|
||||
@@ -75,11 +78,11 @@
|
||||
- (void)start {
|
||||
[super start];
|
||||
|
||||
if (! _logger) {
|
||||
NSError *err = nil;
|
||||
_logger = [self makeJSONDataLoggerWithError:&err];
|
||||
if (! _logger) {
|
||||
[self finishRecordingWithError:err];
|
||||
if (!_logger) {
|
||||
NSError *error = nil;
|
||||
_logger = [self makeJSONDataLoggerWithError:&error];
|
||||
if (!_logger) {
|
||||
[self finishRecordingWithError:error];
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -91,10 +94,10 @@
|
||||
self.locationManager.pausesLocationUpdatesAutomatically = NO;
|
||||
self.locationManager.delegate = self;
|
||||
|
||||
if (! self.locationManager) {
|
||||
if (!self.locationManager) {
|
||||
NSError *error = [NSError errorWithDomain:NSCocoaErrorDomain
|
||||
code:NSFeatureUnsupportedError
|
||||
userInfo:@{@"recorder" : self}];
|
||||
userInfo:@{@"recorder": self}];
|
||||
[self finishRecordingWithError:error];
|
||||
return;
|
||||
}
|
||||
@@ -128,10 +131,10 @@
|
||||
- (void)locationManager:(CLLocationManager *)manager
|
||||
didUpdateLocations:(NSArray *)locations {
|
||||
BOOL success = YES;
|
||||
NSParameterAssert([locations count] >= 0);
|
||||
NSParameterAssert(locations.count >= 0);
|
||||
NSError *error = nil;
|
||||
if (locations) {
|
||||
NSMutableArray *dictionaries = [NSMutableArray arrayWithCapacity:[locations count]];
|
||||
NSMutableArray *dictionaries = [NSMutableArray arrayWithCapacity:locations.count];
|
||||
[locations enumerateObjectsUsingBlock:^(CLLocation *obj, NSUInteger idx, BOOL *stop) {
|
||||
NSDictionary *d = [obj ork_JSONDictionary];
|
||||
[dictionaries addObject:d];
|
||||
@@ -169,11 +172,6 @@
|
||||
@end
|
||||
|
||||
|
||||
@interface ORKLocationRecorderConfiguration ()
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation ORKLocationRecorderConfiguration
|
||||
|
||||
- (instancetype)initWithIdentifier:(NSString *)identifier {
|
||||
|
||||
@@ -29,7 +29,8 @@
|
||||
*/
|
||||
|
||||
|
||||
#import <ResearchKit/ResearchKit_Private.h>
|
||||
@import UIKit;
|
||||
#import "ORKTypes.h"
|
||||
#import "ORKCustomStepView_Internal.h"
|
||||
|
||||
|
||||
|
||||
@@ -30,12 +30,14 @@
|
||||
|
||||
|
||||
#import "ORKPSATContentView.h"
|
||||
#import "ORKSkin.h"
|
||||
|
||||
#import "ORKBorderedButton.h"
|
||||
#import "ORKPSATKeyboardView.h"
|
||||
#import "ORKTapCountLabel.h"
|
||||
#import "ORKBorderedButton.h"
|
||||
#import "ORKVoiceEngine.h"
|
||||
#import "ORKHelpers.h"
|
||||
|
||||
#import "ORKHelpers_Internal.h"
|
||||
#import "ORKSkin.h"
|
||||
|
||||
|
||||
@interface ORKPSATContentView ()
|
||||
@@ -43,8 +45,6 @@
|
||||
@property (nonatomic, assign, getter = isAuditory) BOOL auditory;
|
||||
@property (nonatomic, strong) UIProgressView *progressView;
|
||||
@property (nonatomic, strong) ORKTapCountLabel *digitLabel;
|
||||
@property (nonatomic, assign) ORKScreenType screenType;
|
||||
@property (nonatomic, strong) NSArray *constraints;
|
||||
|
||||
@end
|
||||
|
||||
@@ -65,8 +65,6 @@
|
||||
|
||||
if (self) {
|
||||
|
||||
_screenType = ORKGetVerticalScreenTypeForWindow(self.window);
|
||||
|
||||
_digitLabel = [ORKTapCountLabel new];
|
||||
_digitLabel.textAlignment = NSTextAlignmentCenter;
|
||||
_digitLabel.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
@@ -113,6 +111,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
- (void)tintColorDidChange {
|
||||
[super tintColorDidChange];
|
||||
self.progressView.progressTintColor = self.tintColor;
|
||||
}
|
||||
|
||||
- (void)setProgress:(CGFloat)progress animated:(BOOL)animated {
|
||||
[self.progressView setProgress:progress animated:animated];
|
||||
[UIView animateWithDuration:animated ? 0.2 : 0 animations:^{
|
||||
@@ -121,42 +124,40 @@
|
||||
}
|
||||
|
||||
- (void)updateConstraints {
|
||||
if ([self.constraints count]) {
|
||||
[NSLayoutConstraint deactivateConstraints:self.constraints];
|
||||
self.constraints = nil;
|
||||
}
|
||||
[NSLayoutConstraint deactivateConstraints:self.constraints];
|
||||
|
||||
const CGFloat ORKPSATKeyboardWidth = ORKGetMetricForScreenType(ORKScreenMetricPSATKeyboardViewWidth, self.screenType);
|
||||
const CGFloat ORKPSATKeyboardHeight = ORKGetMetricForScreenType(ORKScreenMetricPSATKeyboardViewHeight, self.screenType);
|
||||
const CGFloat ORKPSATKeyboardWidth = ORKGetMetricForWindow(ORKScreenMetricPSATKeyboardViewWidth, self.window);
|
||||
const CGFloat ORKPSATKeyboardHeight = ORKGetMetricForWindow(ORKScreenMetricPSATKeyboardViewHeight, self.window);
|
||||
|
||||
NSMutableArray *constraintsArray = [NSMutableArray array];
|
||||
NSMutableArray *constraints = [NSMutableArray array];
|
||||
|
||||
NSDictionary *views = NSDictionaryOfVariableBindings(_progressView, _digitLabel, _keyboardView);
|
||||
|
||||
[constraintsArray addObjectsFromArray:
|
||||
[constraints addObjectsFromArray:
|
||||
[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-[_progressView]-|"
|
||||
options:(NSLayoutFormatOptions)0
|
||||
metrics:nil views:views]];
|
||||
metrics:nil
|
||||
views:views]];
|
||||
|
||||
[constraintsArray addObjectsFromArray:
|
||||
[NSLayoutConstraint constraintsWithVisualFormat:[NSString stringWithFormat:@"H:|-[_keyboardView(==%f)]-|", ORKPSATKeyboardWidth]
|
||||
[constraints addObjectsFromArray:
|
||||
[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-[_keyboardView(==keyboardWidth)]-|"
|
||||
options:(NSLayoutFormatOptions)0
|
||||
metrics:nil views:views]];
|
||||
metrics:@{ @"keyboardWidth": @(ORKPSATKeyboardWidth) }
|
||||
views:views]];
|
||||
|
||||
[constraintsArray addObjectsFromArray:
|
||||
[NSLayoutConstraint constraintsWithVisualFormat:[NSString stringWithFormat:@"V:[_keyboardView(==%f)]", ORKPSATKeyboardHeight]
|
||||
[constraints addObjectsFromArray:
|
||||
[NSLayoutConstraint constraintsWithVisualFormat:@"V:[_keyboardView(==keyboardHeight)]"
|
||||
options:(NSLayoutFormatOptions)0
|
||||
metrics:nil views:views]];
|
||||
metrics:@{ @"keyboardHeight": @(ORKPSATKeyboardHeight) }
|
||||
views:views]];
|
||||
|
||||
[constraintsArray addObjectsFromArray:
|
||||
[constraints addObjectsFromArray:
|
||||
[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[_progressView]-[_digitLabel]-(>=10)-[_keyboardView]-|"
|
||||
options:NSLayoutFormatAlignAllCenterX
|
||||
metrics:nil views:views]];
|
||||
metrics:nil
|
||||
views:views]];
|
||||
|
||||
self.constraints = constraintsArray;
|
||||
[self addConstraints:self.constraints];
|
||||
|
||||
[NSLayoutConstraint activateConstraints:self.constraints];
|
||||
[NSLayoutConstraint activateConstraints:constraints];
|
||||
[super updateConstraints];
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,8 @@
|
||||
*/
|
||||
|
||||
|
||||
#import <ResearchKit/ResearchKit.h>
|
||||
@import UIKit;
|
||||
#import "ORKDefines.h"
|
||||
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
|
||||
|
||||
#import "ORKPSATKeyboardView.h"
|
||||
|
||||
#import "ORKBorderedButton.h"
|
||||
|
||||
|
||||
@@ -83,12 +84,9 @@ NSUInteger const ORKPSATMaximumAnswer = 17;
|
||||
}
|
||||
|
||||
- (void)updateConstraints {
|
||||
if ([self.constraints count]) {
|
||||
[NSLayoutConstraint deactivateConstraints:self.constraints];
|
||||
self.constraints = nil;
|
||||
}
|
||||
[NSLayoutConstraint deactivateConstraints:self.constraints];
|
||||
|
||||
NSMutableArray *constraintsArray = [NSMutableArray array];
|
||||
NSMutableArray *constraints = [NSMutableArray array];
|
||||
|
||||
ORKBorderedButton *answer3Button = self.answerButtons[0];
|
||||
ORKBorderedButton *answer4Button = self.answerButtons[1];
|
||||
@@ -110,33 +108,30 @@ NSUInteger const ORKPSATMaximumAnswer = 17;
|
||||
NSDictionary *views = NSDictionaryOfVariableBindings(answer3Button, answer4Button, answer5Button, answer6Button, answer7Button, answer8Button, answer9Button, answer10Button, answer11Button, answer12Button, answer13Button, answer14Button, answer15Button, answer16Button, answer17Button);
|
||||
|
||||
// First line of answer buttons
|
||||
[constraintsArray addObjectsFromArray:
|
||||
[constraints addObjectsFromArray:
|
||||
[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-[answer3Button]-[answer4Button(==answer3Button)]-[answer5Button(==answer3Button)]-[answer6Button(==answer3Button)]-[answer7Button(==answer3Button)]-|"
|
||||
options:NSLayoutFormatAlignAllCenterY|NSLayoutFormatAlignAllTop|NSLayoutFormatAlignAllBottom
|
||||
metrics:nil views:views]];
|
||||
|
||||
// Second line of answer buttons
|
||||
[constraintsArray addObjectsFromArray:
|
||||
[constraints addObjectsFromArray:
|
||||
[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-[answer8Button]-[answer9Button(==answer8Button)]-[answer10Button(==answer8Button)]-[answer11Button(==answer8Button)]-[answer12Button(==answer8Button)]-|"
|
||||
options:NSLayoutFormatAlignAllCenterY|NSLayoutFormatAlignAllTop|NSLayoutFormatAlignAllBottom
|
||||
metrics:nil views:views]];
|
||||
|
||||
// Third line of answer buttons
|
||||
[constraintsArray addObjectsFromArray:
|
||||
[constraints addObjectsFromArray:
|
||||
[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-[answer13Button]-[answer14Button(==answer13Button)]-[answer15Button(==answer13Button)]-[answer16Button(==answer13Button)]-[answer17Button(==answer13Button)]-|"
|
||||
options:NSLayoutFormatAlignAllCenterY|NSLayoutFormatAlignAllTop|NSLayoutFormatAlignAllBottom
|
||||
metrics:nil views:views]];
|
||||
|
||||
// Align vertically
|
||||
[constraintsArray addObjectsFromArray:
|
||||
[constraints addObjectsFromArray:
|
||||
[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-[answer3Button]-[answer8Button(==answer3Button)]-[answer13Button(==answer3Button)]-|"
|
||||
options:(NSLayoutFormatOptions)0
|
||||
metrics:nil views:views]];
|
||||
|
||||
self.constraints = constraintsArray;
|
||||
[self addConstraints:self.constraints];
|
||||
|
||||
[NSLayoutConstraint activateConstraints:self.constraints];
|
||||
[NSLayoutConstraint activateConstraints:constraints];
|
||||
[super updateConstraints];
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,9 @@
|
||||
*/
|
||||
|
||||
|
||||
#import <ResearchKit/ResearchKit_Private.h>
|
||||
@import Foundation;
|
||||
#import <ResearchKit/ORKDefines.h>
|
||||
#import <ResearchKit/ORKActiveStep.h>
|
||||
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@@ -30,8 +30,11 @@
|
||||
|
||||
|
||||
#import "ORKPSATStep.h"
|
||||
|
||||
#import "ORKPSATStepViewController.h"
|
||||
|
||||
#import "ORKHelpers_Internal.h"
|
||||
|
||||
|
||||
@implementation ORKPSATStep
|
||||
|
||||
@@ -90,4 +93,51 @@
|
||||
return NO;
|
||||
}
|
||||
|
||||
+ (BOOL)supportsSecureCoding {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
|
||||
self = [super initWithCoder:aDecoder];
|
||||
if (self) {
|
||||
ORK_DECODE_ENUM(aDecoder, presentationMode);
|
||||
ORK_DECODE_DOUBLE(aDecoder, interStimulusInterval);
|
||||
ORK_DECODE_DOUBLE(aDecoder, stimulusDuration);
|
||||
ORK_DECODE_INTEGER(aDecoder, seriesLength);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)encodeWithCoder:(NSCoder *)aCoder {
|
||||
[super encodeWithCoder:aCoder];
|
||||
ORK_ENCODE_ENUM(aCoder, presentationMode);
|
||||
ORK_ENCODE_DOUBLE(aCoder, interStimulusInterval);
|
||||
ORK_ENCODE_DOUBLE(aCoder, stimulusDuration);
|
||||
ORK_ENCODE_INTEGER(aCoder, seriesLength);
|
||||
}
|
||||
|
||||
- (instancetype)copyWithZone:(NSZone *)zone {
|
||||
ORKPSATStep *step = [super copyWithZone:zone];
|
||||
step.presentationMode = self.presentationMode;
|
||||
step.interStimulusInterval = self.interStimulusInterval;
|
||||
step.stimulusDuration = self.stimulusDuration;
|
||||
step.seriesLength = self.seriesLength;
|
||||
return step;
|
||||
}
|
||||
|
||||
- (NSUInteger)hash {
|
||||
return [super hash] ^ self.presentationMode ^ (NSInteger)(self.interStimulusInterval*100) ^ (NSInteger)(self.stimulusDuration*100) ^ self.seriesLength;
|
||||
}
|
||||
|
||||
- (BOOL)isEqual:(id)object {
|
||||
BOOL isParentSame = [super isEqual:object];
|
||||
|
||||
__typeof(self) castObject = object;
|
||||
return (isParentSame &&
|
||||
(self.presentationMode == castObject.presentationMode) &&
|
||||
(self.interStimulusInterval == castObject.interStimulusInterval) &&
|
||||
(self.stimulusDuration == castObject.stimulusDuration) &&
|
||||
(self.seriesLength == castObject.seriesLength));
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -29,7 +29,9 @@
|
||||
*/
|
||||
|
||||
|
||||
#import <ResearchKit/ResearchKit_Private.h>
|
||||
@import UIKit;
|
||||
#import <ResearchKit/ORKDefines.h>
|
||||
#import <ResearchKit/ORKActiveStepViewController.h>
|
||||
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@@ -30,20 +30,26 @@
|
||||
|
||||
|
||||
#import "ORKPSATStepViewController.h"
|
||||
#import "ORKActiveStepViewController_Internal.h"
|
||||
#import "ORKStepViewController_Internal.h"
|
||||
#import "ORKPSATContentView.h"
|
||||
#import "ORKPSATStep.h"
|
||||
#import "ORKVerticalContainerView.h"
|
||||
|
||||
#import "ORKActiveStepTimer.h"
|
||||
#import "ORKActiveStepView.h"
|
||||
#import "ORKPSATContentView.h"
|
||||
#import "ORKPSATKeyboardView.h"
|
||||
#import "ORKVerticalContainerView.h"
|
||||
|
||||
#import "ORKActiveStepViewController_Internal.h"
|
||||
#import "ORKPSATStep.h"
|
||||
#import "ORKResult.h"
|
||||
#import "ORKStepViewController_Internal.h"
|
||||
|
||||
#import "ORKHelpers_Internal.h"
|
||||
|
||||
|
||||
@interface ORKPSATStepViewController () <ORKPSATKeyboardViewDelegate>
|
||||
|
||||
@property (nonatomic, strong) NSMutableArray *samples;
|
||||
@property (nonatomic, strong) ORKPSATContentView *psatContentView;
|
||||
@property (nonatomic, strong) NSArray *digits;
|
||||
@property (nonatomic, strong) NSArray<NSNumber *> *digits;
|
||||
@property (nonatomic, assign) NSUInteger currentDigitIndex;
|
||||
@property (nonatomic, assign) NSInteger currentAnswer;
|
||||
@property (nonatomic, strong) ORKActiveStepTimer *clearDigitsTimer;
|
||||
@@ -52,6 +58,7 @@
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation ORKPSATStepViewController
|
||||
|
||||
- (instancetype)initWithStep:(ORKStep *)step {
|
||||
@@ -65,6 +72,7 @@
|
||||
}
|
||||
|
||||
- (ORKPSATStep *)psatStep {
|
||||
NSAssert(self.step == nil || [self.step isKindOfClass:[ORKPSATStep class]], @"Step class must be subclass of ORKPSATStep.");
|
||||
return (ORKPSATStep *)self.step;
|
||||
}
|
||||
|
||||
@@ -107,7 +115,7 @@
|
||||
|
||||
NSMutableArray *results = [NSMutableArray arrayWithArray:sResult.results];
|
||||
|
||||
ORKPSATResult *PSATResult = [[ORKPSATResult alloc] initWithIdentifier:(NSString *__nonnull)self.step.identifier];
|
||||
ORKPSATResult *PSATResult = [[ORKPSATResult alloc] initWithIdentifier:self.step.identifier];
|
||||
PSATResult.presentationMode = [self psatStep].presentationMode;
|
||||
PSATResult.interStimulusInterval = [self psatStep].interStimulusInterval;
|
||||
if ([self psatStep].presentationMode & ORKPSATPresentationModeVisual) {
|
||||
@@ -116,7 +124,7 @@
|
||||
PSATResult.stimulusDuration = 0.0;
|
||||
}
|
||||
PSATResult.length = [self psatStep].seriesLength;
|
||||
PSATResult.initialDigit = [(NSNumber *)[self.digits objectAtIndex:0] integerValue];
|
||||
PSATResult.initialDigit = self.digits[0].integerValue;
|
||||
NSInteger totalCorrect = 0;
|
||||
BOOL previousAnswerCorrect = NO;
|
||||
NSInteger totalDyad = 0;
|
||||
@@ -146,7 +154,7 @@
|
||||
- (void)start {
|
||||
self.digits = [self arrayWithPSATDigits];
|
||||
self.currentDigitIndex = 0;
|
||||
[self.psatContentView setAddition:self.currentDigitIndex forTotal:[self psatStep].seriesLength withDigit:[self.digits objectAtIndex:self.currentDigitIndex]];
|
||||
[self.psatContentView setAddition:self.currentDigitIndex forTotal:[self psatStep].seriesLength withDigit:self.digits[self.currentDigitIndex]];
|
||||
[self.psatContentView setProgress:0.001 animated:NO];
|
||||
self.currentAnswer = -1;
|
||||
self.samples = [NSMutableArray array];
|
||||
@@ -155,12 +163,12 @@
|
||||
([self psatStep].interStimulusInterval - [self psatStep].stimulusDuration) > 0.05 ) {
|
||||
|
||||
// Don't show `-` if the difference between stimulusDuration and interStimulusInterval is less than timer's resolution.
|
||||
__weak typeof(self) weakSelf = self;
|
||||
ORKWeakTypeOf(self) weakSelf = self;
|
||||
self.clearDigitsTimer = [[ORKActiveStepTimer alloc] initWithDuration:[self psatStep].stepDuration
|
||||
interval:[self psatStep].interStimulusInterval
|
||||
runtime:-[self psatStep].stimulusDuration
|
||||
handler:^(ORKActiveStepTimer *timer, BOOL finished) {
|
||||
typeof(self) strongSelf = weakSelf;
|
||||
ORKStrongTypeOf(self) strongSelf = weakSelf;
|
||||
[strongSelf clearDigitsTimerFired];
|
||||
}];
|
||||
[self.clearDigitsTimer resume];
|
||||
@@ -198,7 +206,7 @@
|
||||
self.answerEnd = 0;
|
||||
|
||||
if (self.currentDigitIndex <= [self psatStep].seriesLength) {
|
||||
[self.psatContentView setAddition:self.currentDigitIndex forTotal:[self psatStep].seriesLength withDigit:[self.digits objectAtIndex:self.currentDigitIndex]];
|
||||
[self.psatContentView setAddition:self.currentDigitIndex forTotal:[self psatStep].seriesLength withDigit:self.digits[self.currentDigitIndex]];
|
||||
}
|
||||
|
||||
self.currentAnswer = -1;
|
||||
@@ -215,8 +223,8 @@
|
||||
|
||||
- (void)saveSample {
|
||||
ORKPSATSample *sample = [[ORKPSATSample alloc] init];
|
||||
NSInteger previousDigit = [(NSNumber *)[self.digits objectAtIndex:self.currentDigitIndex - 1] integerValue];
|
||||
NSInteger currentDigit = [(NSNumber *)[self.digits objectAtIndex:self.currentDigitIndex] integerValue];
|
||||
NSInteger previousDigit = self.digits[self.currentDigitIndex - 1].integerValue;
|
||||
NSInteger currentDigit = self.digits[self.currentDigitIndex].integerValue;;
|
||||
sample.correct = previousDigit + currentDigit == self.currentAnswer ? YES : NO;
|
||||
sample.digit = currentDigit;
|
||||
sample.answer = self.currentAnswer;
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
*/
|
||||
|
||||
|
||||
@import UIKit;
|
||||
#import <ResearchKit/ORKRecorder.h>
|
||||
|
||||
|
||||
|
||||
@@ -30,10 +30,13 @@
|
||||
|
||||
|
||||
#import "ORKPedometerRecorder.h"
|
||||
|
||||
#import "ORKDataLogger.h"
|
||||
#import "CMPedometerData+ORKJSONDictionary.h"
|
||||
|
||||
#import "ORKRecorder_Internal.h"
|
||||
#import "ORKRecorder_Private.h"
|
||||
|
||||
#import "ORKHelpers_Internal.h"
|
||||
#import "CMPedometerData+ORKJSONDictionary.h"
|
||||
|
||||
|
||||
@interface ORKPedometerRecorder () {
|
||||
@@ -66,9 +69,9 @@
|
||||
|
||||
- (void)updateStatisticsWithData:(CMPedometerData *)pedometerData {
|
||||
_lastUpdateDate = pedometerData.endDate;
|
||||
_totalNumberOfSteps = [pedometerData.numberOfSteps integerValue];
|
||||
_totalNumberOfSteps = pedometerData.numberOfSteps.integerValue;
|
||||
if (pedometerData.distance) {
|
||||
_totalDistance = [pedometerData.distance doubleValue];
|
||||
_totalDistance = pedometerData.distance.doubleValue;
|
||||
} else {
|
||||
_totalDistance = -1;
|
||||
}
|
||||
@@ -90,39 +93,39 @@
|
||||
_totalNumberOfSteps = 0;
|
||||
_totalDistance = -1;
|
||||
|
||||
if (! _logger) {
|
||||
NSError *err = nil;
|
||||
_logger = [self makeJSONDataLoggerWithError:&err];
|
||||
if (! _logger) {
|
||||
[self finishRecordingWithError:err];
|
||||
if (!_logger) {
|
||||
NSError *error = nil;
|
||||
_logger = [self makeJSONDataLoggerWithError:&error];
|
||||
if (!_logger) {
|
||||
[self finishRecordingWithError:error];
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
self.pedometer = [self createPedometer];
|
||||
|
||||
if (! [[self.pedometer class] isStepCountingAvailable]) {
|
||||
if (![[self.pedometer class] isStepCountingAvailable]) {
|
||||
[self finishRecordingWithError:[NSError errorWithDomain:NSCocoaErrorDomain
|
||||
code:NSFeatureUnsupportedError
|
||||
userInfo:@{@"recorder" : self}]];
|
||||
userInfo:@{@"recorder": self}]];
|
||||
return;
|
||||
}
|
||||
|
||||
_isRecording = YES;
|
||||
__weak __typeof(self) weakSelf = self;
|
||||
ORKWeakTypeOf(self) weakSelf = self;
|
||||
[self.pedometer startPedometerUpdatesFromDate:[NSDate date] withHandler:^(CMPedometerData *pedometerData, NSError *error) {
|
||||
|
||||
BOOL success = NO;
|
||||
if (pedometerData) {
|
||||
success = [_logger append:[pedometerData ork_JSONDictionary] error:&error];
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
__typeof(self) strongSelf = weakSelf;
|
||||
ORKStrongTypeOf(self) strongSelf = weakSelf;
|
||||
[strongSelf updateStatisticsWithData:pedometerData];
|
||||
});
|
||||
}
|
||||
if (!success || error) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
__typeof(self) strongSelf = weakSelf;
|
||||
ORKStrongTypeOf(self) strongSelf = weakSelf;
|
||||
[strongSelf finishRecordingWithError:error];
|
||||
});
|
||||
}
|
||||
@@ -179,11 +182,6 @@
|
||||
@end
|
||||
|
||||
|
||||
@interface ORKPedometerRecorderConfiguration ()
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation ORKPedometerRecorderConfiguration
|
||||
|
||||
- (instancetype)initWithIdentifier:(NSString *)identifier {
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
Copyright (c) 2016, Darren Levy. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
|
||||
@import Foundation;
|
||||
#import <ResearchKit/ORKDefines.h>
|
||||
#import <ResearchKit/ORKActiveStep.h>
|
||||
#import <ResearchKit/ORKOrderedTask.h>
|
||||
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/**
|
||||
The `ORKRangeOfMotionStep` class represents a step that takes a range of motion measurement.
|
||||
*/
|
||||
ORK_CLASS_AVAILABLE
|
||||
@interface ORKRangeOfMotionStep : ORKActiveStep
|
||||
|
||||
@property (nonatomic, assign) ORKPredefinedTaskLimbOption limbOption; //The left and/or right limb to be tested during the task
|
||||
|
||||
- (instancetype)initWithIdentifier:(NSString *)identifier limbOption:(ORKPredefinedTaskLimbOption)limbOption;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
Copyright (c) 2016, Darren Levy. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
|
||||
#import "ORKRangeOfMotionStep.h"
|
||||
#import "ORKRangeOfMotionStepViewController.h"
|
||||
#import "ORKHelpers_Internal.h"
|
||||
|
||||
|
||||
@implementation ORKRangeOfMotionStep
|
||||
|
||||
+ (Class)stepViewControllerClass {
|
||||
return [ORKRangeOfMotionStepViewController class];
|
||||
}
|
||||
|
||||
- (instancetype)initWithIdentifier:(NSString *)identifier limbOption:(ORKPredefinedTaskLimbOption)limbOption {
|
||||
self = [super initWithIdentifier:identifier];
|
||||
if (self) {
|
||||
self.shouldVibrateOnStart = YES;
|
||||
self.shouldPlaySoundOnStart = YES;
|
||||
self.shouldVibrateOnFinish = YES;
|
||||
self.shouldPlaySoundOnFinish = YES;
|
||||
self.shouldContinueOnFinish = YES;
|
||||
self.shouldStartTimerAutomatically = YES;
|
||||
self.limbOption = limbOption;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)validateParameters {
|
||||
[super validateParameters];
|
||||
|
||||
if (self.limbOption != ORKPredefinedTaskLimbOptionLeft && self.limbOption != ORKPredefinedTaskLimbOptionRight) {
|
||||
@throw [NSException exceptionWithName:NSInvalidArgumentException
|
||||
reason:ORKLocalizedString(@"LIMB_OPTION_LEFT_OR_RIGHT_ERROR", nil)
|
||||
userInfo:nil];
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)startsFinished {
|
||||
return NO;
|
||||
}
|
||||
|
||||
+ (BOOL)supportsSecureCoding {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (instancetype)copyWithZone:(NSZone *)zone {
|
||||
ORKRangeOfMotionStep *step = [super copyWithZone:zone];
|
||||
step.limbOption = self.limbOption;
|
||||
return step;
|
||||
}
|
||||
|
||||
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
|
||||
self = [super initWithCoder:aDecoder];
|
||||
if (self) {
|
||||
ORK_DECODE_INTEGER(aDecoder, limbOption);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)encodeWithCoder:(NSCoder *)aCoder {
|
||||
[super encodeWithCoder:aCoder];
|
||||
ORK_ENCODE_INTEGER(aCoder, limbOption);
|
||||
}
|
||||
|
||||
- (BOOL)isEqual:(id)object {
|
||||
BOOL isParentSame = [super isEqual:object];
|
||||
|
||||
__typeof(self) castObject = object;
|
||||
return (isParentSame && (self.limbOption == castObject.limbOption));
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
Copyright (c) 2016, Darren Levy. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
|
||||
#import <ResearchKit/ResearchKit.h>
|
||||
#import <CoreMotion/CMDeviceMotion.h>
|
||||
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/**
|
||||
This class is used by the `ORKRangeOfMotionStep.` Its result corresponds to the device's orientation
|
||||
as recorded by CoreMotion.
|
||||
*/
|
||||
ORK_CLASS_AVAILABLE
|
||||
@interface ORKRangeOfMotionStepViewController : ORKActiveStepViewController {
|
||||
double _flexedAngle;
|
||||
double _rangeOfMotionAngle;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user