Compare commits
1549 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
3d57f5ccc4 | |
|
|
e8780d0238 | |
|
|
b7862cb891 | |
|
|
e8ca5ad039 | |
|
|
8fe2bfd6e8 | |
|
|
18e5421e14 | |
|
|
5549854c75 | |
|
|
af7c5bc9fd | |
|
|
6a7af59ffc | |
|
|
36b4f84306 | |
|
|
5c7df9373d | |
|
|
5c44471dbb | |
|
|
7a7987239d | |
|
|
b65029056f | |
|
|
815d749bd9 | |
|
|
03059969b2 | |
|
|
1a9ed8353b | |
|
|
03aa25e529 | |
|
|
48a2b47428 | |
|
|
d06c1c35d4 | |
|
|
9fab98b560 | |
|
|
94cf900f7c | |
|
|
c1e96eee35 | |
|
|
ed8a2113c4 | |
|
|
8652795ddb | |
|
|
38dc604cbc | |
|
|
abe9ad5dc1 | |
|
|
abd18d848c | |
|
|
4a076b8865 | |
|
|
767c19d17b | |
|
|
90cd941eff | |
|
|
2bf1fc052a | |
|
|
a03fc1f7ee | |
|
|
7945aa3a62 | |
|
|
93b0f61b00 | |
|
|
a0c7faa4a3 | |
|
|
60734996f6 | |
|
|
eaea4abd75 | |
|
|
b8611321fb | |
|
|
13a5925443 | |
|
|
c485434f51 | |
|
|
843a887ec7 | |
|
|
8007532351 | |
|
|
0ef1edfacb | |
|
|
27d5a3a9ca | |
|
|
4e8f60543d | |
|
|
c2b31a90d6 | |
|
|
1a4c42fa46 | |
|
|
094c0c40d8 | |
|
|
b22fd239e9 | |
|
|
65ae079e62 | |
|
|
7c8a29a7f9 | |
|
|
df2faa4cd5 | |
|
|
36019f7429 | |
|
|
5245d48a8a | |
|
|
83655d2bac | |
|
|
6b7be340f5 | |
|
|
6084dd5fec | |
|
|
85b206bf18 | |
|
|
da527644a7 | |
|
|
45c060403f | |
|
|
a79ff67a38 | |
|
|
c631053131 | |
|
|
c55b8f73a9 | |
|
|
25c0d04d11 | |
|
|
b97ea5bc8f | |
|
|
06a83190ab | |
|
|
3f112d2d26 | |
|
|
808d40eca5 | |
|
|
46ecd6970f | |
|
|
0776c99e38 | |
|
|
a06f4952b9 | |
|
|
77559babdb | |
|
|
86413b3bb4 | |
|
|
9056b2fe8c | |
|
|
0e45bb462d | |
|
|
e8b026302e | |
|
|
818d4dbe8d | |
|
|
86fddafcdf | |
|
|
919423ecda | |
|
|
c06bb56964 | |
|
|
b141dc5a45 | |
|
|
1e3b986c83 | |
|
|
2ea88a94aa | |
|
|
1be28959bc | |
|
|
bf613b99e8 | |
|
|
23c17c9d85 | |
|
|
f50bb09ad8 | |
|
|
005d80c531 | |
|
|
33cc31b957 | |
|
|
19a319f03c | |
|
|
193a060cff | |
|
|
a5bc2dc8f0 | |
|
|
5a74c342d9 | |
|
|
5bb3092726 | |
|
|
83a3e5b491 | |
|
|
7b9e8b0885 | |
|
|
529277d098 | |
|
|
ecfb83bafa | |
|
|
c0189dc7ae | |
|
|
a0401bc9fa | |
|
|
43a12e322f | |
|
|
5ca564476a | |
|
|
0e0a8d733e | |
|
|
7fd33b6157 | |
|
|
4f0c9a8ed1 | |
|
|
e7517c23f8 | |
|
|
975435bb90 | |
|
|
fd0365a370 | |
|
|
4ea76a8499 | |
|
|
6358386303 | |
|
|
dd4c9072a9 | |
|
|
5aff5f99bb | |
|
|
4fe395d295 | |
|
|
a925f70c04 | |
|
|
41d18bf3a6 | |
|
|
fc6a735d94 | |
|
|
77ce6a1c95 | |
|
|
cf54d8c798 | |
|
|
93a8ee68f3 | |
|
|
5a14c61a9a | |
|
|
4a9355fd80 | |
|
|
45e2b9ff19 | |
|
|
26866427a9 | |
|
|
26a7578dc8 | |
|
|
0612370587 | |
|
|
fe80863656 | |
|
|
3dd0c34d27 | |
|
|
56527b6dba | |
|
|
af044aa591 | |
|
|
c442ee2623 | |
|
|
3ccadd07b4 | |
|
|
9a9b57df4b | |
|
|
06b687c47e | |
|
|
a99c29ea73 | |
|
|
270ac1a4d5 | |
|
|
22b133648b | |
|
|
2245765b27 | |
|
|
30479ae8b8 | |
|
|
8d253d1458 | |
|
|
dcc9d23676 | |
|
|
d9e4ea5ae8 | |
|
|
775f95a931 | |
|
|
39bad32c49 | |
|
|
05a6236425 | |
|
|
7f45ce0594 | |
|
|
c407dabdf5 | |
|
|
29d7a6ca65 | |
|
|
2d0819064a | |
|
|
76da4ab223 | |
|
|
66508d505d | |
|
|
c8985cde1e | |
|
|
1bc200034c | |
|
|
cde2420f7d | |
|
|
026700b4c7 | |
|
|
4ccebe8e8b | |
|
|
f3ed27e83f | |
|
|
a234943394 | |
|
|
1374c4df1f | |
|
|
318fd40f0b | |
|
|
5ebb97de4c | |
|
|
507fc8fa05 | |
|
|
1932262ad5 | |
|
|
2c23e86852 | |
|
|
164edf9a5d | |
|
|
2f71b10dc0 | |
|
|
9d99d4e4e3 | |
|
|
9cc412208c | |
|
|
a8fc13ff1e | |
|
|
22fc660e56 | |
|
|
5d2bea19fb | |
|
|
e2c9c6c102 | |
|
|
332e895659 | |
|
|
9dca21afd9 | |
|
|
511c2b9653 | |
|
|
2b1511657d | |
|
|
f91453a065 | |
|
|
23b74ec3d5 | |
|
|
4c973b393d | |
|
|
0090c83f87 | |
|
|
144ea7b703 | |
|
|
278e175f3a | |
|
|
6af9a64135 | |
|
|
efde6153a8 | |
|
|
e3ae781f1d | |
|
|
23c2cbacea | |
|
|
40aa2876d1 | |
|
|
2250b1b4d9 | |
|
|
19bb08aa66 | |
|
|
597755474c | |
|
|
cb29a3e9ca | |
|
|
eafb434c88 | |
|
|
cfd5d5f2f8 | |
|
|
9c8510af14 | |
|
|
e942d08503 | |
|
|
2c081d508f | |
|
|
f01644b408 | |
|
|
b533eaaae6 | |
|
|
9db353b360 | |
|
|
e4c84ca511 | |
|
|
ce2f3ca064 | |
|
|
cbd38b84e2 | |
|
|
c8a4b0bd51 | |
|
|
7a0747843a | |
|
|
343d36cb85 | |
|
|
29347d77e1 | |
|
|
64604bdce0 | |
|
|
e27c844f92 | |
|
|
01d99cb246 | |
|
|
48f6655efc | |
|
|
bc9ac01463 | |
|
|
dda06ece3c | |
|
|
5ea58f3746 | |
|
|
2061050c78 | |
|
|
e199cd4220 | |
|
|
6e30957fe5 | |
|
|
84e7093903 | |
|
|
1da2d4d501 | |
|
|
118bca1c9d | |
|
|
fbab4a491b | |
|
|
fd2fa45909 | |
|
|
e471bae469 | |
|
|
698243ee39 | |
|
|
59ef1093c7 | |
|
|
7442884856 | |
|
|
6e506aa385 | |
|
|
9f5d7387d7 | |
|
|
e9b32ce326 | |
|
|
a9a8ddde9e | |
|
|
893f6f191d | |
|
|
56d1ee998d | |
|
|
55fe6b7126 | |
|
|
5f7e0bf273 | |
|
|
3a321a7fbf | |
|
|
275afb655f | |
|
|
f3c5002f4e | |
|
|
3406962d21 | |
|
|
f1d5b27f3d | |
|
|
f4a516bf86 | |
|
|
46be3ce9de | |
|
|
30c5b72b26 | |
|
|
f67005df71 | |
|
|
05fa3dad31 | |
|
|
0c3b987370 | |
|
|
7765c01074 | |
|
|
f2c390f71a | |
|
|
7e41552521 | |
|
|
bd7d31cf67 | |
|
|
4e1270205e | |
|
|
c5209dc9f6 | |
|
|
3ef71a6892 | |
|
|
04ee83b8df | |
|
|
6ff2c8bf37 | |
|
|
2d3e12164d | |
|
|
fffe5bd8d4 | |
|
|
f6e00bb53a | |
|
|
a67c42886d | |
|
|
407995db35 | |
|
|
63777fef99 | |
|
|
79a5cc3665 | |
|
|
0204aa9b10 | |
|
|
79a5ed4149 | |
|
|
3bab367ce2 | |
|
|
caeded9561 | |
|
|
3d5aa7a41d | |
|
|
47217f7a59 | |
|
|
e4c8118b6b | |
|
|
f885183499 | |
|
|
aff54859eb | |
|
|
3e7307424a | |
|
|
a7e44a3d9a | |
|
|
c75ff4c1d0 | |
|
|
b98678b235 | |
|
|
c7ba0d1f30 | |
|
|
9d3bbc9c71 | |
|
|
7add7c46ab | |
|
|
724ed400c4 | |
|
|
ad5fbc96fb | |
|
|
d12e07484d | |
|
|
5017e7e7b7 | |
|
|
6e442ce37f | |
|
|
e6864f3911 | |
|
|
18cd001ab9 | |
|
|
8c696af6d9 | |
|
|
1fb3ca883b | |
|
|
3730736319 | |
|
|
9dcdd1c63c | |
|
|
ba929fd344 | |
|
|
2a89de15e3 | |
|
|
f7596a73d8 | |
|
|
39ddcf498f | |
|
|
98ee540aac | |
|
|
4778f2e70d | |
|
|
1c1fa1290b | |
|
|
d2ed1e837a | |
|
|
401d9365d0 | |
|
|
5159ee5a4d | |
|
|
dffb4c6015 | |
|
|
f8b7934204 | |
|
|
9fa4b7c058 | |
|
|
e1c9596010 | |
|
|
d0576acd95 | |
|
|
13cf92c9c1 | |
|
|
d6069db9df | |
|
|
69c2a85718 | |
|
|
4ac0eace66 | |
|
|
77abc2c5a5 | |
|
|
8ffd2b589b | |
|
|
6ccb8fcdc3 | |
|
|
8e0043cd48 | |
|
|
31ddf69dc0 | |
|
|
592036951a | |
|
|
01931a9e62 | |
|
|
ea13f317b9 | |
|
|
7e62f9b571 | |
|
|
7cfd6f8424 | |
|
|
4f0a652e09 | |
|
|
da98e5ff35 | |
|
|
83e5caa13a | |
|
|
a53c015b79 | |
|
|
873b2ecbcb | |
|
|
3157cac5d5 | |
|
|
f60a443eed | |
|
|
f230add1ed | |
|
|
444d3b159d | |
|
|
f4dc72b61f | |
|
|
54a01db216 | |
|
|
72aabd4412 | |
|
|
070d7c199a | |
|
|
2d9bb4a5d5 | |
|
|
3af9cc5b35 | |
|
|
f6f4d2f214 | |
|
|
e9edf3ab21 | |
|
|
ff720fca0d | |
|
|
31582b3bc5 | |
|
|
3bb9e74461 | |
|
|
37818d3153 | |
|
|
e2f9b481ab | |
|
|
fffa5aa6eb | |
|
|
826c66f8af | |
|
|
91097e2f65 | |
|
|
da4389aa03 | |
|
|
81792aba72 | |
|
|
a8785b35e8 | |
|
|
ca7bf6326d | |
|
|
116d2154f8 | |
|
|
dde0eba7a8 | |
|
|
f3081861a0 | |
|
|
698b79d10f | |
|
|
21a73a8f7c | |
|
|
3696269635 | |
|
|
92f06c83a4 | |
|
|
2eea25b4af | |
|
|
090b86563d | |
|
|
c25ff8c431 | |
|
|
77f6208ff5 | |
|
|
eaa787e00f | |
|
|
8cbf5b96e6 | |
|
|
fa130aecdb | |
|
|
635a9bb9b0 | |
|
|
d16fc21d7e | |
|
|
4880dcad27 | |
|
|
0bc8574a32 | |
|
|
ec1fe892ad | |
|
|
d9cdfdec0f | |
|
|
6abac15fc7 | |
|
|
0171f9d64f | |
|
|
fc6ec80a29 | |
|
|
9eaf42aa4e | |
|
|
0abda665bf | |
|
|
95651b2b58 | |
|
|
60d1fd2044 | |
|
|
a253639073 | |
|
|
901cdc571f | |
|
|
08fdb69d68 | |
|
|
ab9fafeb19 | |
|
|
50d3d5a36b | |
|
|
53a0ced7f6 | |
|
|
3cd6f0a9d0 | |
|
|
048b5c1f83 | |
|
|
95fc92dfca | |
|
|
73abd949eb | |
|
|
e28d71caf7 | |
|
|
5eb0183d92 | |
|
|
373b0db19e | |
|
|
95a0045582 | |
|
|
2c8fc0a8a5 | |
|
|
9888bb22d5 | |
|
|
6ef2c83990 | |
|
|
4c525b8dad | |
|
|
ae52ec03c6 | |
|
|
7bef631668 | |
|
|
2748d5e3b0 | |
|
|
47280c7cd7 | |
|
|
c583b8a98a | |
|
|
7f6883c7b7 | |
|
|
ef1eb2d8ac | |
|
|
0534d41bae | |
|
|
7c479ff428 | |
|
|
b6321d3b19 | |
|
|
82d52f4a40 | |
|
|
af1c9da523 | |
|
|
cead71df3d | |
|
|
18b48957a4 | |
|
|
edec8bc94f | |
|
|
276fbc8c10 | |
|
|
62aecb0941 | |
|
|
c6d17e96ab | |
|
|
8466a4a4db | |
|
|
30cf6856b1 | |
|
|
b7b95f0ef4 | |
|
|
617ebbfde1 | |
|
|
651892d182 | |
|
|
28126dae4b | |
|
|
b57f205812 | |
|
|
50d59857a7 | |
|
|
8cfd3b466c | |
|
|
1db3bdb944 | |
|
|
d5479f745c | |
|
|
eb66a4e8ca | |
|
|
76f6635cbc | |
|
|
de3aad7147 | |
|
|
343c40888d | |
|
|
179a368fe5 | |
|
|
50f6670710 | |
|
|
104116fd36 | |
|
|
48ac99c7d8 | |
|
|
fa29f08d2d | |
|
|
9e96b83c1e | |
|
|
c92b9bf563 | |
|
|
c85ab9113b | |
|
|
761e4b98bf | |
|
|
ea1392f279 | |
|
|
01c4ef6f44 | |
|
|
b52d57a5e7 | |
|
|
cddae04f2e | |
|
|
826471c825 | |
|
|
6810fac839 | |
|
|
a6b63c237b | |
|
|
6eefc607e5 | |
|
|
db19e24bc4 | |
|
|
af73b2964f | |
|
|
b2c6f7b852 | |
|
|
0803060787 | |
|
|
17a70d613b | |
|
|
a917723dcb | |
|
|
174d472f1b | |
|
|
9a427adab7 | |
|
|
cfbf53faf8 | |
|
|
bf46b602a3 | |
|
|
a88a85fe75 | |
|
|
34c7407ecb | |
|
|
ffa66048b0 | |
|
|
8138291153 | |
|
|
7b79d6b250 | |
|
|
693843bb70 | |
|
|
24d1384e0f | |
|
|
b96478c248 | |
|
|
37b7224264 | |
|
|
c223a026f2 | |
|
|
a89c620d91 | |
|
|
000e88f98a | |
|
|
52751dc801 | |
|
|
88da2ab508 | |
|
|
7e319dcb03 | |
|
|
9828284c20 | |
|
|
5e43ca7fe2 | |
|
|
8fc1ebab77 | |
|
|
e3fbfbd981 | |
|
|
784743082d | |
|
|
c5db547b82 | |
|
|
9230450bb7 | |
|
|
3d3d94412a | |
|
|
4608de4779 | |
|
|
b39b937eaf | |
|
|
c4a4714d37 | |
|
|
2050382308 | |
|
|
ac977d35ed | |
|
|
2181f5f0a3 | |
|
|
f61bb8ef12 | |
|
|
23b9e8ac7a | |
|
|
cfa80a3747 | |
|
|
e18db61746 | |
|
|
d568963784 | |
|
|
e31283cb67 | |
|
|
be0681397d | |
|
|
47ff4d949c | |
|
|
ceab75e3b5 | |
|
|
70c4b4e00d | |
|
|
532e54fe9e | |
|
|
19134573fa | |
|
|
cb50db9f34 | |
|
|
241e7c211e | |
|
|
0cf3e4473c | |
|
|
274baa8b90 | |
|
|
25265d1797 | |
|
|
0dffbcbb26 | |
|
|
9a3d061c63 | |
|
|
98f55093ac | |
|
|
d31476aaec | |
|
|
10ec9408ad | |
|
|
e67136013c | |
|
|
ab4faa2868 | |
|
|
fa5fffc593 | |
|
|
692f448b46 | |
|
|
f01096dfb7 | |
|
|
db81ff7567 | |
|
|
8ce9a0183a | |
|
|
5f551fd799 | |
|
|
8ff4199ed0 | |
|
|
11f6b5dbd0 | |
|
|
8a482cc186 | |
|
|
0ad47b3f34 | |
|
|
3e5d8d13fb | |
|
|
1dded1a7d2 | |
|
|
2bb6e7d9b2 | |
|
|
f0f29a4464 | |
|
|
7def493512 | |
|
|
8f6e43f9de | |
|
|
1c38c41507 | |
|
|
373a61835b | |
|
|
aacd025382 | |
|
|
b42fcd596f | |
|
|
b52dd87f55 | |
|
|
203e2e9091 | |
|
|
aa2dc3880e | |
|
|
6ba07b25ad | |
|
|
27c2897b60 | |
|
|
68e5cb55b1 | |
|
|
51a40a60f2 | |
|
|
6cf16b74a7 | |
|
|
7c5c76ba0a | |
|
|
72dab661f2 | |
|
|
05c3273fd8 | |
|
|
89a0bf247f | |
|
|
4c7b7a76c1 | |
|
|
3c6a98a582 | |
|
|
c1212bb3b0 | |
|
|
9df3ccb30d | |
|
|
4b818afa85 | |
|
|
8397f15ec5 | |
|
|
b8550bd8d5 | |
|
|
4082ddb30e | |
|
|
c2df8511f2 | |
|
|
902120a483 | |
|
|
fc4ae9f9e2 | |
|
|
b4ad6165ff | |
|
|
c25dd3656d | |
|
|
f1b72bad8c | |
|
|
77a8d43499 | |
|
|
d66620d906 | |
|
|
d23257e963 | |
|
|
1fcc478372 | |
|
|
3dd6fa9830 | |
|
|
ff47d2c855 | |
|
|
89a3bfe697 | |
|
|
e249e64ec6 | |
|
|
f9858286d8 | |
|
|
fee9d4ac62 | |
|
|
762b5352bc | |
|
|
596a549595 | |
|
|
4e667225bb | |
|
|
91c15c21ed | |
|
|
97e3b15ad8 | |
|
|
518d8e7f41 | |
|
|
1c97cb629e | |
|
|
58bd8b1eb9 | |
|
|
c7a3c46b47 | |
|
|
252e00198f | |
|
|
b1cc752af3 | |
|
|
4e7d4356fe | |
|
|
ffcec6e4de | |
|
|
4b908f1e32 | |
|
|
88bc9798b1 | |
|
|
fb0e1090e5 | |
|
|
8b32bcd89e | |
|
|
eea8727d98 | |
|
|
17f9a79e0a | |
|
|
033335a0b5 | |
|
|
225605b0ac | |
|
|
fa7c06fcc9 | |
|
|
5720af623a | |
|
|
564b4e23d8 | |
|
|
28c3808f1a | |
|
|
ce426d1f49 | |
|
|
6acf0f0b0f | |
|
|
5e06035236 | |
|
|
c327047bc4 | |
|
|
c873366e7f | |
|
|
67488af9c6 | |
|
|
c68f6fa69f | |
|
|
6580042f03 | |
|
|
80199ccce0 | |
|
|
71f516af98 | |
|
|
b39c217c30 | |
|
|
cd91364c75 | |
|
|
1b58ff5aaa | |
|
|
effad1e51c | |
|
|
a9c7bd6abd | |
|
|
e38ee51275 | |
|
|
ba3211e613 | |
|
|
68914f0ddc | |
|
|
49e6172edf | |
|
|
e20a4b8924 | |
|
|
2da25414da | |
|
|
4f02836cf4 | |
|
|
fc679c5020 | |
|
|
0810070370 | |
|
|
2bd3144922 | |
|
|
cf81e228ef | |
|
|
0095829e31 | |
|
|
44b41a6618 | |
|
|
2de6bc2df3 | |
|
|
954080891d | |
|
|
d27c8ab97c | |
|
|
77ca9d05fa | |
|
|
ee78f008eb | |
|
|
529dd9edf8 | |
|
|
d560c035c6 | |
|
|
1ad5eed642 | |
|
|
a6461bd77e | |
|
|
e3a6897083 | |
|
|
67d31b108b | |
|
|
4a1e84773e | |
|
|
ea00667963 | |
|
|
e86dcc81f9 | |
|
|
e87173b166 | |
|
|
6c8ff3e682 | |
|
|
8655c90f1d | |
|
|
3e64b5cc4e | |
|
|
2e1cf62e6f | |
|
|
614e574596 | |
|
|
5d449ce92f | |
|
|
6472f8a3bb | |
|
|
0a195e3309 | |
|
|
616a6ec9ed | |
|
|
1225b190f0 | |
|
|
44306a5863 | |
|
|
d3e07fd550 | |
|
|
f3dcef4690 | |
|
|
1efc4c2bf0 | |
|
|
7b5b6ed945 | |
|
|
5fd69cae98 | |
|
|
3d683f44bc | |
|
|
151022a22c | |
|
|
50ca1c7d79 | |
|
|
9b115370f7 | |
|
|
f9af35e558 | |
|
|
d264b23089 | |
|
|
c5403c0ba9 | |
|
|
1ab3993c7e | |
|
|
31c0f2c93a | |
|
|
5afbc2ffa5 | |
|
|
7ffac03d73 | |
|
|
6700252ef6 | |
|
|
ffe4098937 | |
|
|
e2c9c937b8 | |
|
|
57c010f4f2 | |
|
|
9113f0b7df | |
|
|
7408446b18 | |
|
|
2e84f5cb2d | |
|
|
102c15ca07 | |
|
|
f3fe5e08d8 | |
|
|
4f8d0b537d | |
|
|
6fbf1e0eff | |
|
|
c997eb2bfb | |
|
|
7102cccc60 | |
|
|
0f518c3c88 | |
|
|
00c72992ca | |
|
|
6b3bf25f0a | |
|
|
8b1e20d790 | |
|
|
53e25e3a13 | |
|
|
18d7e5c5b6 | |
|
|
6433233d9e | |
|
|
0a7f68bd23 | |
|
|
281f0b72b9 | |
|
|
dfbe18fedd | |
|
|
e92b46eb51 | |
|
|
64cb766542 | |
|
|
4af10aa107 | |
|
|
6fdb6af732 | |
|
|
55977e39f5 | |
|
|
7b7fcab29e | |
|
|
22ba1fb8f7 | |
|
|
e1ba5e5788 | |
|
|
f797ea2d8f | |
|
|
4c81de418f | |
|
|
1e903ad14d | |
|
|
76b6822ce5 | |
|
|
6b735be733 | |
|
|
5427b0f1be | |
|
|
c3827fcaf2 | |
|
|
702f518e50 | |
|
|
451d99d36d | |
|
|
9ae23ecef4 | |
|
|
7011435905 | |
|
|
0a1b2547b5 | |
|
|
9991408b32 | |
|
|
16a6f63ba2 | |
|
|
6d8ba62316 | |
|
|
12a47d8b24 | |
|
|
f7b9732d9d | |
|
|
a56ecd5f79 | |
|
|
ab09d9e289 | |
|
|
98e0e34afe | |
|
|
cf92942e61 | |
|
|
45d88b4cf6 | |
|
|
690c5f8a8c | |
|
|
9a9ebcb627 | |
|
|
32c711823b | |
|
|
7d4c8e5294 | |
|
|
5be71ec033 | |
|
|
6c75bbe884 | |
|
|
bd8542fe85 | |
|
|
4ba7e639e8 | |
|
|
2f8c5aa7b3 | |
|
|
e45b566ed2 | |
|
|
5e15fbc857 | |
|
|
79d8f2a05e | |
|
|
d884203af5 | |
|
|
f9f62b3fe7 | |
|
|
ec02ac7be6 | |
|
|
80f26d932c | |
|
|
3c90aff617 | |
|
|
445ff1a644 | |
|
|
419685ae2d | |
|
|
75796c0ade | |
|
|
fd4402b368 | |
|
|
94bd1e9c36 | |
|
|
52b7485755 | |
|
|
bba6c965c7 | |
|
|
7f05a98894 | |
|
|
a3d0131197 | |
|
|
942ad1fb57 | |
|
|
eaea21c27b | |
|
|
86581223ea | |
|
|
4ac04996ac | |
|
|
d595e0331f | |
|
|
bb6633f3ce | |
|
|
24898b5ed1 | |
|
|
4d590e9bc5 | |
|
|
a51e31ec96 | |
|
|
d29903d51b | |
|
|
b2b386185c | |
|
|
af43eb9da7 | |
|
|
0aacf470cb | |
|
|
a4dfe52351 | |
|
|
fba3271181 | |
|
|
3db011bb3f | |
|
|
ab2f849d2b | |
|
|
95923bc9ca | |
|
|
ebd8ab23a3 | |
|
|
cda62768dd | |
|
|
5b644e1ed3 | |
|
|
11942c327b | |
|
|
a4ddd994cd | |
|
|
46be8249d8 | |
|
|
2164d4fb3b | |
|
|
61e796f278 | |
|
|
6a8f050e0c | |
|
|
cba4833d46 | |
|
|
bb125aa428 | |
|
|
285383e595 | |
|
|
be8ba32d46 | |
|
|
265860c966 | |
|
|
37c8d736db | |
|
|
90b82edd64 | |
|
|
eabfa7ec01 | |
|
|
eaf58da24a | |
|
|
ec29cb0e27 | |
|
|
00c60e6fb0 | |
|
|
2808d015bf | |
|
|
b6ef182d56 | |
|
|
16e2d222a4 | |
|
|
91e6906145 | |
|
|
a227d74db5 | |
|
|
058f50215d | |
|
|
dd4e8e2c7b | |
|
|
fc82a8539c | |
|
|
2032c17556 | |
|
|
eb86d72394 | |
|
|
f84a5a643d | |
|
|
ae76c806ae | |
|
|
dfa4a7c0f9 | |
|
|
b273d2c118 | |
|
|
f3cd8bec97 | |
|
|
7924886b70 | |
|
|
f51d26311d | |
|
|
6b2230b4ce | |
|
|
e21ba045fa | |
|
|
59367c1eb0 | |
|
|
153a404d5f | |
|
|
b6079e175c | |
|
|
d99f926222 | |
|
|
6c94eb8948 | |
|
|
fa7260cab2 | |
|
|
174885d172 | |
|
|
a962e3f9f2 | |
|
|
1c98858aa9 | |
|
|
d1d5f46378 | |
|
|
b4b8785cdc | |
|
|
0f5a0d165c | |
|
|
a524ff7855 | |
|
|
5fc4625bf6 | |
|
|
2c7c2c4ba5 | |
|
|
f0387692bc | |
|
|
fcc5805dac | |
|
|
e9ad33785a | |
|
|
ab52e832dc | |
|
|
5fb90f6069 | |
|
|
622c722a59 | |
|
|
74d4be3635 | |
|
|
f0e25196a2 | |
|
|
0172b222b0 | |
|
|
e2ab9d817c | |
|
|
f7a66a5dba | |
|
|
3430344703 | |
|
|
80544b342a | |
|
|
b668f76c4a | |
|
|
3d0b1af04f | |
|
|
34d75efff8 | |
|
|
d4d8c3b9b1 | |
|
|
b6d4a5f7ad | |
|
|
1850366709 | |
|
|
5a39fe10cd | |
|
|
1bd8941af4 | |
|
|
b6be1163b3 | |
|
|
dc102b1f73 | |
|
|
0833982bf0 | |
|
|
a516deef00 | |
|
|
d8868d1aca | |
|
|
7cffa29c7f | |
|
|
f7410622b1 | |
|
|
8f26e41b52 | |
|
|
5da96e5a8f | |
|
|
9e82df42c5 | |
|
|
44ccb8dd28 | |
|
|
16b209cd59 | |
|
|
a023e164e1 | |
|
|
54bb41e5ff | |
|
|
1f6370fd73 | |
|
|
986e2d0cf1 | |
|
|
0de81939ad | |
|
|
6de58594ea | |
|
|
cdebfbebdd | |
|
|
090c066a75 | |
|
|
8b13419da4 | |
|
|
87d5f91d2d | |
|
|
7237efe641 | |
|
|
31ea1157ce | |
|
|
21563377f4 | |
|
|
1832520a6d | |
|
|
f01730d4c8 | |
|
|
31bb4d9798 | |
|
|
6e1e4de0b1 | |
|
|
dac88ebe98 | |
|
|
97ec620d8c | |
|
|
bd0bc54fdd | |
|
|
0ff4c685d4 | |
|
|
44bc8dfefc | |
|
|
4d21fe0577 | |
|
|
b3dcd06298 | |
|
|
3ed9d949a4 | |
|
|
6f1d4a0dba | |
|
|
c12131de39 | |
|
|
2700848d50 | |
|
|
67a02f62e6 | |
|
|
9f96b934de | |
|
|
5a053b410e | |
|
|
8cfaede34e | |
|
|
0332d9848e | |
|
|
d0b2cbac5a | |
|
|
b2016fa157 | |
|
|
cdc9668b1a | |
|
|
970f8c6987 | |
|
|
419ce202a8 | |
|
|
4ca23cf3d0 | |
|
|
d4fa2d1109 | |
|
|
2c2935c892 | |
|
|
683d3e9797 | |
|
|
d05b1b00c9 | |
|
|
971e495b6d | |
|
|
f8026bcd66 | |
|
|
6449f99e89 | |
|
|
ef2e351eae | |
|
|
88a7fbee81 | |
|
|
ddd008398f | |
|
|
09e97500fc | |
|
|
cfb8259dbe | |
|
|
b99870183b | |
|
|
469073a5e9 | |
|
|
c4798d6015 | |
|
|
a3f43a93e8 | |
|
|
d8bb1ce6b0 | |
|
|
f1aefb5378 | |
|
|
b82c547f06 | |
|
|
b594edfc13 | |
|
|
ac291e4ef1 | |
|
|
08b87a0782 | |
|
|
223ca6a67e | |
|
|
544d5bc248 | |
|
|
0df9be960f | |
|
|
39aa498072 | |
|
|
fb465b99cb | |
|
|
9337532f6f | |
|
|
185d5a838d | |
|
|
8271ffc4b9 | |
|
|
8c61bfab9e | |
|
|
6366f70b9a | |
|
|
35a39c149c | |
|
|
42feb7471f | |
|
|
fc3708c793 | |
|
|
8ad7e7162f | |
|
|
05ce8e9d4f | |
|
|
8d0320fe26 | |
|
|
00383039b7 | |
|
|
abca33e3ae | |
|
|
12f49c9b51 | |
|
|
8e589a695b | |
|
|
9297de6171 | |
|
|
9cce0b095f | |
|
|
2ba53e3366 | |
|
|
87b23ff8da | |
|
|
3c745e5058 | |
|
|
23a2cfb113 | |
|
|
2d4beef7f0 | |
|
|
0447c53709 | |
|
|
c25c92e9da | |
|
|
287955fdba | |
|
|
c50a171030 | |
|
|
e4e5dcfa30 | |
|
|
79bca12a00 | |
|
|
aa00c43f57 | |
|
|
0a490b243d | |
|
|
3be1d793a2 | |
|
|
223be00d74 | |
|
|
6f8c4e2b67 | |
|
|
053c08443b | |
|
|
1dfa0b4f48 | |
|
|
7d96e07b4e | |
|
|
23058232ce | |
|
|
b30447e877 | |
|
|
5088c20e82 | |
|
|
d3d8136477 | |
|
|
f82158959f | |
|
|
c713e786f0 | |
|
|
9938f1fc49 | |
|
|
df2f969dc4 | |
|
|
29de30cd37 | |
|
|
d9f7da35f1 | |
|
|
2df2979d8f | |
|
|
6556d8368b | |
|
|
517e9619ae | |
|
|
a73baf8253 | |
|
|
a21847b7ff | |
|
|
1b6d77f34c | |
|
|
49445a4056 | |
|
|
c529ae736e | |
|
|
e9eac22a3b | |
|
|
c71626ba92 | |
|
|
512f854296 | |
|
|
192a30bfa6 | |
|
|
f6b39ddcf7 | |
|
|
f2519ad3fc | |
|
|
cb6d5c4232 | |
|
|
caa93bfd1f | |
|
|
fb7063e870 | |
|
|
1c97a22a6f | |
|
|
09da4ce234 | |
|
|
95202a4ff1 | |
|
|
a53dad9777 | |
|
|
5389eda15d | |
|
|
72fd83785a | |
|
|
73e2f0743a | |
|
|
dee27dd0a9 | |
|
|
5970fd2f69 | |
|
|
8cb1771e5d | |
|
|
470247f6d8 | |
|
|
f6569fc395 | |
|
|
8f3b75297a | |
|
|
482d8b10e8 | |
|
|
fd9d50b8de | |
|
|
dbc5bf9a17 | |
|
|
7cb1ea350b | |
|
|
3c1ee70ff3 | |
|
|
d9a4cf03c9 | |
|
|
299fbe37e2 | |
|
|
6950b1746b | |
|
|
8e295d701c | |
|
|
71ef4d6895 | |
|
|
fb813b259f | |
|
|
8c4eda2f66 | |
|
|
71115f3651 | |
|
|
0c37bea3cf | |
|
|
c1c0f470d2 | |
|
|
6b04a0fb32 | |
|
|
9517e1151e | |
|
|
15ed08d7cf | |
|
|
727162f58d | |
|
|
4402cd963c | |
|
|
2b4259d08e | |
|
|
26efa9a4d8 | |
|
|
e3ddab4f19 | |
|
|
35131cc336 | |
|
|
707d4bc6f4 | |
|
|
c19f56ecf6 | |
|
|
e1f79f3304 | |
|
|
373fee95d0 | |
|
|
598ae1adad | |
|
|
e01861af80 | |
|
|
bd47336cfb | |
|
|
c49fce14a2 | |
|
|
a27ec284ac | |
|
|
fed7d6c9d4 | |
|
|
f5ec3ca438 | |
|
|
0987c40e25 | |
|
|
c76b2e1745 | |
|
|
445600534d | |
|
|
1a5e37f527 | |
|
|
65c3fba12a | |
|
|
7fe405539e | |
|
|
50f24e82b6 | |
|
|
acf7876f71 | |
|
|
615ca1d4b4 | |
|
|
652c55a936 | |
|
|
ad8fe894bf | |
|
|
22ce7654db | |
|
|
9bc73b8a3e | |
|
|
15043bff09 | |
|
|
2df0dd1a6b | |
|
|
8ff37b175b | |
|
|
7c6acd5c58 | |
|
|
729bbaa8ef | |
|
|
c087a11c3a | |
|
|
2bccc006d7 | |
|
|
83454dfef4 | |
|
|
937b43d887 | |
|
|
4425ff6f21 | |
|
|
7bf65f0b36 | |
|
|
e7eb1bd51f | |
|
|
03701b4d82 | |
|
|
ecbf71aec0 | |
|
|
b796702b4e | |
|
|
2e70e8248d | |
|
|
48dd620542 | |
|
|
e0d13abe6c | |
|
|
857c747bfa | |
|
|
233db5c048 | |
|
|
b27bd9d0e2 | |
|
|
30bec8e9c7 | |
|
|
f2d523a9d2 | |
|
|
079a723ef1 | |
|
|
c1fe170c51 | |
|
|
acd5b39c33 | |
|
|
a90586c46a | |
|
|
1d1ef692b9 | |
|
|
d1fab2c69c | |
|
|
e804b92765 | |
|
|
0f0b37cd06 | |
|
|
d8a258f05d | |
|
|
0b5b6e8358 | |
|
|
667c15aa33 | |
|
|
8b9f3edace | |
|
|
e11b129a75 | |
|
|
a20aa5affa | |
|
|
2cebfd7ffd | |
|
|
3a8be1bc8c | |
|
|
27806bc9e9 | |
|
|
922b14dfcb | |
|
|
2013fb7d59 | |
|
|
7c553ed516 | |
|
|
662010fc0f | |
|
|
3901e38f81 | |
|
|
25c2edda35 | |
|
|
f69f4a12ae | |
|
|
cf017b7cda | |
|
|
f90bf4a77a | |
|
|
41db20ffac | |
|
|
3d2a53c906 | |
|
|
0d0ae71916 | |
|
|
b856a2f65e | |
|
|
d31ec29152 | |
|
|
12b4efe6a9 | |
|
|
14f6a9e6cc | |
|
|
f7ab9718c4 | |
|
|
5e311a01a4 | |
|
|
e06af6d4ee | |
|
|
f9691a6716 | |
|
|
0eba1a5865 | |
|
|
7db711c209 | |
|
|
65dbcb6ad3 | |
|
|
037bb831d4 | |
|
|
bd3753fa62 | |
|
|
f85910e5fe | |
|
|
249dd03e51 | |
|
|
10755d2d22 | |
|
|
f076b4aac1 | |
|
|
387fb83e63 | |
|
|
492d375aea | |
|
|
f20f355a42 | |
|
|
37b4c72374 | |
|
|
690c0532d1 | |
|
|
03f61ead48 | |
|
|
2c2386b555 | |
|
|
06f4121b69 | |
|
|
87abcde0ef | |
|
|
754875556d | |
|
|
d408d1eed6 | |
|
|
3ef5795bfc | |
|
|
ca9b2eaca3 | |
|
|
bba41c7122 | |
|
|
996081f65e | |
|
|
6db6258db5 | |
|
|
0996d94814 | |
|
|
85e58127b7 | |
|
|
e647052af2 | |
|
|
475e801393 | |
|
|
840f15b47e | |
|
|
dcc095a233 | |
|
|
3dc7b4fe9d | |
|
|
1d21338de9 | |
|
|
69e28eaf5b | |
|
|
20bf8bf861 | |
|
|
6cc6971d60 | |
|
|
32eefa77a6 | |
|
|
3edfe50164 | |
|
|
f88c8efb14 | |
|
|
0ee26e3e8c | |
|
|
35270f2b32 | |
|
|
3931c09d7f | |
|
|
2a98ba3d47 | |
|
|
4c0579bfef | |
|
|
3c99b31da0 | |
|
|
61962b58bd | |
|
|
34c0ce78db | |
|
|
1e366ba95d | |
|
|
609e1a9ff4 | |
|
|
a65a55bd35 | |
|
|
36f059da06 | |
|
|
0d81d07700 | |
|
|
53c9e23b21 | |
|
|
9004c5f553 | |
|
|
87e4789625 | |
|
|
116f6b6836 | |
|
|
cbb647afe7 | |
|
|
62240cc591 | |
|
|
11d529f4c0 | |
|
|
f71c882738 | |
|
|
787fc3a935 | |
|
|
a9536d3cff | |
|
|
d7d62a6ee8 | |
|
|
3e73ff7658 | |
|
|
e3e060eda4 | |
|
|
360bcde3f9 | |
|
|
c49a05270f | |
|
|
f71237c032 | |
|
|
6982b95659 | |
|
|
a6ce1288ba | |
|
|
f890fb58e0 | |
|
|
15a77a8bdb | |
|
|
6417b43df8 | |
|
|
a28805846b | |
|
|
8c3c3cb7f5 | |
|
|
d600d57260 | |
|
|
2733a159ed | |
|
|
b181344f28 | |
|
|
98ef4ca86e | |
|
|
91b49d8f94 | |
|
|
83b8f40e93 | |
|
|
9d3b3bd223 | |
|
|
5a60cf68a5 | |
|
|
2046be1282 | |
|
|
4c6990883a | |
|
|
fd775f4e78 | |
|
|
2a190e810e | |
|
|
fb34bdb51b | |
|
|
0209e1b021 | |
|
|
17cd260d1e | |
|
|
d53db408fd | |
|
|
bc4f9835a5 | |
|
|
7afa723ed5 | |
|
|
7249bf66dc | |
|
|
ad0ed37095 | |
|
|
538138ada5 | |
|
|
f0fb443f6b | |
|
|
aff508ed44 | |
|
|
bb7fe4f7f2 | |
|
|
d06ede6a19 | |
|
|
588b097d42 | |
|
|
aba4068fb5 | |
|
|
e9521f1e0f | |
|
|
9473e46d1b | |
|
|
d87890c83e | |
|
|
7a329feeca | |
|
|
828eb56560 | |
|
|
03b4dc6ac5 | |
|
|
c168b64d2c | |
|
|
9275ef6b6b | |
|
|
8f9588d791 | |
|
|
83e1eae7d0 | |
|
|
f408af05e1 | |
|
|
5f513b5bad | |
|
|
3fdbc72324 | |
|
|
fc591bba0c | |
|
|
0c27f3b81d | |
|
|
f1da683009 | |
|
|
d15a10857f | |
|
|
a9be9b8f13 | |
|
|
f8e9fd8973 | |
|
|
b320615154 | |
|
|
74b61cc921 | |
|
|
e51a585ed9 | |
|
|
46d0fd57da | |
|
|
b364ac6d61 | |
|
|
d267f0a58e | |
|
|
14e793cf09 | |
|
|
66567bb811 | |
|
|
ed13f119f3 | |
|
|
85f568fd44 | |
|
|
c450d2c143 | |
|
|
79c2cf70b9 | |
|
|
437ab6ff6b | |
|
|
ea829b7418 | |
|
|
b6ddf8c285 | |
|
|
596ef59d1a | |
|
|
35d81b0e2e | |
|
|
e5d50f11d5 | |
|
|
0706e36860 | |
|
|
63324eec7e | |
|
|
828d63838c | |
|
|
11cce2d920 | |
|
|
e6bdba0504 | |
|
|
ee15a4f1a6 | |
|
|
9a4d934cc2 | |
|
|
e296f5ee77 | |
|
|
934121690d | |
|
|
8f16df4e91 | |
|
|
03301eec3d | |
|
|
b0cc7c5dfd | |
|
|
74d3e69b37 | |
|
|
ba596d826b | |
|
|
5c88f34333 | |
|
|
1a34bebe69 | |
|
|
3fe60ef94a | |
|
|
78f82e51d2 | |
|
|
7c548954fb | |
|
|
3d3736bf8f | |
|
|
4d938fc384 | |
|
|
5d206ea718 | |
|
|
4507f8cd5d | |
|
|
384180443b | |
|
|
c7e5687f0b | |
|
|
01e93cc4e3 | |
|
|
463279d286 | |
|
|
f875207362 | |
|
|
9dec8a651e | |
|
|
409b614e98 | |
|
|
26a5b9dd96 | |
|
|
c3337fdf22 | |
|
|
e3e424bdf7 | |
|
|
7a905bb9b6 | |
|
|
751e0a51d8 | |
|
|
24695b1dae | |
|
|
a1a8ed5366 | |
|
|
cb694dcbcf | |
|
|
bfaf668191 | |
|
|
368b90ddc8 | |
|
|
6b74825fd4 | |
|
|
cd00fb2f2f | |
|
|
b0cee2dee6 | |
|
|
7c012db927 | |
|
|
8ab4000b95 | |
|
|
a503259eb1 | |
|
|
96a59c809e | |
|
|
323300be63 | |
|
|
435fbace1d | |
|
|
0b472a1cdc | |
|
|
d20f795db7 | |
|
|
03619df2f1 | |
|
|
8839d029a9 | |
|
|
1477043499 | |
|
|
3118fc84c8 | |
|
|
c99881288a | |
|
|
3799d11e0a | |
|
|
0c7c968f14 | |
|
|
08f21469e1 | |
|
|
3ddd8162d3 | |
|
|
451bbccc74 | |
|
|
ea1cf7b864 | |
|
|
fa5aba357e | |
|
|
287f44ffc2 | |
|
|
164380196d | |
|
|
0a95f4ae31 | |
|
|
d8e1417e88 | |
|
|
224acadf7b | |
|
|
3e75237ec3 | |
|
|
310bec29d1 | |
|
|
e961be9cb6 | |
|
|
dff3edfc8f | |
|
|
4bd3779535 | |
|
|
7a12073d59 | |
|
|
c8403eb639 | |
|
|
7c92ef47e0 | |
|
|
3969199df7 | |
|
|
c7dc3c812e | |
|
|
3fafda7132 | |
|
|
a6768619a0 | |
|
|
88e020e6e0 | |
|
|
7d41698b59 | |
|
|
bb9723bb28 | |
|
|
19524c09b1 | |
|
|
b4d4ab5bec | |
|
|
5a438fc5f7 | |
|
|
54097dc302 | |
|
|
506608b9a4 | |
|
|
29e5150337 | |
|
|
b26932fbfe | |
|
|
a4c4cd00ae | |
|
|
d7b6141e77 | |
|
|
49b7324e7c | |
|
|
69f77e951d | |
|
|
17c15bc2aa | |
|
|
69bdcceb4f | |
|
|
74e8aa959f | |
|
|
1e6550ff3e | |
|
|
71721c5bb1 | |
|
|
f17c712b76 | |
|
|
3be37b403f | |
|
|
15975756f9 | |
|
|
378db7c577 | |
|
|
9c3610ebfa | |
|
|
3c747015e9 | |
|
|
683fdb1593 | |
|
|
ac7820bdd2 | |
|
|
556078e7d0 | |
|
|
0352976901 | |
|
|
c22433459e | |
|
|
8186aaff0f | |
|
|
4772993e9b | |
|
|
a1b9cc762d | |
|
|
63c6a5b029 | |
|
|
001a6f206a | |
|
|
ba33d47e2c | |
|
|
921cfb909e | |
|
|
c47c12f4dc | |
|
|
425e8f6dad | |
|
|
13485bea57 | |
|
|
f55876b6a2 | |
|
|
38c1e72049 | |
|
|
f60c94986a | |
|
|
a0511290c6 | |
|
|
c23a9beb45 | |
|
|
85919e2a0a | |
|
|
e65f85a538 | |
|
|
3c78926f14 | |
|
|
148a964fb0 | |
|
|
2c637702e4 | |
|
|
56136eee4b | |
|
|
81e87efb3c | |
|
|
ee2fb6d796 | |
|
|
5e9a20d81a | |
|
|
cc26bdbd68 | |
|
|
9ab7dc8f52 | |
|
|
6c29696367 | |
|
|
06a208bb76 | |
|
|
5c85bf2e5d | |
|
|
f7868ad827 | |
|
|
1ba179028e | |
|
|
fa2838bb9c | |
|
|
26ebaa48e9 | |
|
|
68733e0d68 | |
|
|
cdef1fe5d8 | |
|
|
6f15f3082a | |
|
|
4e92792153 | |
|
|
2aa33589ee | |
|
|
866d7394d7 | |
|
|
7c089f0d85 | |
|
|
e9fbae40b3 | |
|
|
060356c123 | |
|
|
0851ba9b2d | |
|
|
21bdc476b5 | |
|
|
9ed0964452 | |
|
|
f2f6e628f9 | |
|
|
84e4155929 | |
|
|
5102f4adf1 | |
|
|
d6b0272300 | |
|
|
763efd8968 | |
|
|
dca12659d1 | |
|
|
827d8a3ca2 | |
|
|
a5a6148bf1 | |
|
|
84d9953ac0 | |
|
|
e46f2f860e | |
|
|
5b2b892a59 | |
|
|
9e959a4fdf | |
|
|
54ba8531b7 | |
|
|
24ae2c5160 | |
|
|
1b16b2435a | |
|
|
47f942e3a5 | |
|
|
e97a357e02 | |
|
|
b7d639a3c8 | |
|
|
ab6af95df3 | |
|
|
5ace163223 | |
|
|
8a9a0cca4a | |
|
|
bc746eafb9 | |
|
|
08dbf13d7b | |
|
|
8d578e7f23 | |
|
|
1051033913 | |
|
|
0ef07e8bc6 | |
|
|
323675115c | |
|
|
f900148e15 | |
|
|
a7f324dd79 | |
|
|
ac82606f28 | |
|
|
79ebf34529 | |
|
|
6d16303908 | |
|
|
979a30febf | |
|
|
5420410222 | |
|
|
56ef65c7a8 | |
|
|
7735fd31d4 | |
|
|
6274a4e568 | |
|
|
716b306034 | |
|
|
1559798116 | |
|
|
ed31986477 | |
|
|
77fb73fd90 | |
|
|
300a9b4942 | |
|
|
d56fa86b28 | |
|
|
397829f07b | |
|
|
410d08ad7e | |
|
|
2b45e4a936 | |
|
|
2d70472ae6 | |
|
|
ef22ddb084 | |
|
|
52165cc2ce | |
|
|
ded9e878eb | |
|
|
18f137acb3 | |
|
|
a69d541fa2 | |
|
|
61be271f2f | |
|
|
557a3382f8 | |
|
|
1d154293dd | |
|
|
0fb3261f7a | |
|
|
45d93e303f | |
|
|
b7d6925b98 | |
|
|
782eb8037b | |
|
|
5c37528eb1 | |
|
|
161a669f83 | |
|
|
0baf215dd9 | |
|
|
76e5b701b9 | |
|
|
bba7d7f845 | |
|
|
e845758204 | |
|
|
73af67cc20 | |
|
|
1e34ad17ce | |
|
|
36676bac5b | |
|
|
0121cc40b7 | |
|
|
bc3d4f7901 | |
|
|
b48aa95e3b | |
|
|
398630764c | |
|
|
b799c1e352 | |
|
|
1ae3a8398c | |
|
|
58674279ca | |
|
|
2dd801b055 | |
|
|
629830e64e | |
|
|
d76825deb5 | |
|
|
f3b24792ad | |
|
|
e384df2ebe | |
|
|
ccac5294fc | |
|
|
c1885b1e75 | |
|
|
fbba41fa81 | |
|
|
19a277d22a | |
|
|
7042707b5e | |
|
|
d1e649c4ef | |
|
|
680e588e4d | |
|
|
e53241696b | |
|
|
bd4d423aec | |
|
|
10e459dd40 | |
|
|
8779a3955b | |
|
|
57c5383300 | |
|
|
bf69f5e782 | |
|
|
c2f310c800 | |
|
|
29a8d16688 | |
|
|
e4c2fd8f67 | |
|
|
72c0d4fc6e | |
|
|
e289214e21 | |
|
|
7814b67ce3 | |
|
|
3d6af86bdc | |
|
|
56535b5835 | |
|
|
9bdbd3ea67 | |
|
|
f0320d13ea | |
|
|
0bb7c41cc2 | |
|
|
81f9a4fa53 | |
|
|
64b8bd17e0 | |
|
|
66cd8dbb9f | |
|
|
98805c1d83 | |
|
|
fc0bf2f36e | |
|
|
044a05b4b6 | |
|
|
fba5894390 | |
|
|
020ea512f4 | |
|
|
b74a516720 | |
|
|
9b73354cc0 | |
|
|
e509f2d4a7 | |
|
|
29b3bb3215 | |
|
|
75fbeda92a | |
|
|
267f7c6b4c | |
|
|
2ddad9ee08 | |
|
|
f9680dab9f | |
|
|
bebef20e54 | |
|
|
1387f00744 | |
|
|
4aaf6dc048 | |
|
|
94969f65f7 | |
|
|
ab9921da53 | |
|
|
844a7d1c67 | |
|
|
a86ae5aed3 | |
|
|
673db680b7 | |
|
|
14d7eb3be5 | |
|
|
d99dc6ce86 | |
|
|
5030a9b5fa | |
|
|
470ef0a880 | |
|
|
fd8239801d | |
|
|
f0d4fd9505 | |
|
|
b3dface0c8 | |
|
|
7355ebc858 | |
|
|
5db2c2bd5e | |
|
|
b1c657dd82 | |
|
|
48ecdcc336 | |
|
|
ded59ff2d9 | |
|
|
f9fbfb2735 | |
|
|
908ea53d3e | |
|
|
cb07c482d1 | |
|
|
f1658f3380 | |
|
|
fa069c422a | |
|
|
04a558b70f | |
|
|
00b7d11b37 | |
|
|
192e2cdd27 | |
|
|
f3c90a220d | |
|
|
7319594d7b | |
|
|
12984838a4 | |
|
|
159c45cc30 | |
|
|
3dbcd8891b | |
|
|
cc3b3bfed7 | |
|
|
3937622881 | |
|
|
3b4ed37a43 | |
|
|
c7177a6ac7 | |
|
|
60f2b1cefb | |
|
|
e9609abaa9 | |
|
|
a6a5942e8f | |
|
|
0dcc8763b5 | |
|
|
d533e098f8 | |
|
|
a4683f131f | |
|
|
de07ebbff5 | |
|
|
924712c1c1 | |
|
|
bdbe58dceb | |
|
|
da17fa24ed |
|
|
@ -0,0 +1,2 @@
|
|||
---
|
||||
BUNDLE_PATH: ".gem"
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import sys, re, os
|
||||
from subprocess import check_output
|
||||
from sys import getdefaultencoding
|
||||
|
||||
getdefaultencoding() # utf-8
|
||||
|
||||
valid_commit_style = '^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style)(\(\S+\))?\!?: .+'
|
||||
merge_commit_style = '^(m|M)erge .+'
|
||||
|
||||
success_title = 'SUCCESS'
|
||||
success_color = '92m'
|
||||
|
||||
error_title = 'ERROR'
|
||||
error_message = 'Incorrect commit message style!\nThe commit pattern:'
|
||||
error_commit_pattern = ' type(scope): message | type: message \n'
|
||||
error_color = '91m'
|
||||
|
||||
breaking_changes_message = 'If commit include Breaking changes use ! after type or scope:'
|
||||
colored_breaking_changes_message = 'If commit include \033[91mBreaking changes\033[00m use \033[91m!\033[00m after type or scope:'
|
||||
breaking_changes_commit_pattern = ' type(scope)!: message | type!: message \n'
|
||||
|
||||
available_types_message = 'Available commit types:'
|
||||
available_commit_types = ['build: Changes that affect the build system or external dependencies',
|
||||
'ci: Changes to our CI configuration files and scripts',
|
||||
'docs: Documentation only changes',
|
||||
'feat: A new feature. Correlates with MINOR in SemVer',
|
||||
'fix: A bug fix. Correlates with PATCH in SemVer',
|
||||
'perf: A code change that improves performance',
|
||||
'refactor: A code change that neither fixes',
|
||||
'revert: A revert to previous commit',
|
||||
'style: Changes that do not affect the meaning of the code (white-space, formatting, etc)']
|
||||
|
||||
is_GUI_client = False
|
||||
|
||||
def print_result_header(result_title, color):
|
||||
if not is_GUI_client:
|
||||
print("[\033[96mcommit lint\033[00m] [\033[{}{}\033[00m]\n".format(color, result_title))
|
||||
|
||||
def print_pattern(pattern):
|
||||
if is_GUI_client:
|
||||
print(pattern)
|
||||
else:
|
||||
print("\033[96m{}\033[00m".format(pattern))
|
||||
|
||||
def print_error_message():
|
||||
print_result_header(error_title, error_color)
|
||||
|
||||
print(error_message)
|
||||
print_pattern(error_commit_pattern)
|
||||
|
||||
if is_GUI_client:
|
||||
print(breaking_changes_message)
|
||||
else:
|
||||
print(colored_breaking_changes_message)
|
||||
|
||||
print_pattern(breaking_changes_commit_pattern)
|
||||
print_available_commit_types()
|
||||
|
||||
def print_available_commit_types():
|
||||
print(available_types_message)
|
||||
|
||||
for commit_type in available_commit_types:
|
||||
print(" - %s" %commit_type)
|
||||
|
||||
def write_commit_message(fh, commit_msg):
|
||||
fh.seek(0, 0)
|
||||
fh.write(commit_msg)
|
||||
|
||||
def lint_commit_message(fh, commit_msg):
|
||||
is_merge_commit = re.findall(merge_commit_style, commit_msg)
|
||||
is_valid_commit = re.findall(valid_commit_style, commit_msg)
|
||||
|
||||
if is_valid_commit or is_merge_commit:
|
||||
print_result_header(success_title, success_color)
|
||||
write_commit_message(fh, commit_msg)
|
||||
sys.exit(os.EX_OK)
|
||||
else:
|
||||
print_error_message()
|
||||
sys.exit(os.EX_DATAERR)
|
||||
|
||||
def run_script():
|
||||
commit_msg_filepath = sys.argv[1]
|
||||
|
||||
with open(commit_msg_filepath, 'r+') as fh:
|
||||
commit_msg = fh.read()
|
||||
lint_commit_message(fh, commit_msg)
|
||||
|
||||
try:
|
||||
sys.stdin = open("/dev/tty", "r")
|
||||
is_GUI_client = False
|
||||
except:
|
||||
is_GUI_client = True
|
||||
|
||||
run_script()
|
||||
|
|
@ -1,16 +1,18 @@
|
|||
# ================
|
||||
# Swift.gitignore
|
||||
# ================
|
||||
|
||||
# Xcode
|
||||
#
|
||||
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
|
||||
|
||||
## Build generated
|
||||
build/
|
||||
DerivedData
|
||||
## User settings
|
||||
xcuserdata/
|
||||
|
||||
## Various settings
|
||||
## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
|
||||
*.xcscmblueprint
|
||||
*.xccheckout
|
||||
|
||||
## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
|
||||
build/
|
||||
DerivedData/
|
||||
*.moved-aside
|
||||
*.pbxuser
|
||||
!default.pbxuser
|
||||
*.mode1v3
|
||||
|
|
@ -19,17 +21,14 @@ DerivedData
|
|||
!default.mode2v3
|
||||
*.perspectivev3
|
||||
!default.perspectivev3
|
||||
xcuserdata
|
||||
|
||||
## Other
|
||||
*.xccheckout
|
||||
*.moved-aside
|
||||
*.xcuserstate
|
||||
*.xcscmblueprint
|
||||
|
||||
## Obj-C/Swift specific
|
||||
*.hmap
|
||||
|
||||
## App packaging
|
||||
*.ipa
|
||||
*.dSYM.zip
|
||||
*.dSYM
|
||||
|
||||
## Playgrounds
|
||||
timeline.xctimeline
|
||||
|
|
@ -39,6 +38,14 @@ playground.xcworkspace
|
|||
#
|
||||
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
|
||||
# Packages/
|
||||
# Package.pins
|
||||
# Package.resolved
|
||||
# *.xcodeproj
|
||||
#
|
||||
# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
|
||||
# hence it is not needed unless you have added a package configuration file to your project
|
||||
.swiftpm
|
||||
|
||||
.build/
|
||||
|
||||
# CocoaPods
|
||||
|
|
@ -48,28 +55,56 @@ playground.xcworkspace
|
|||
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
|
||||
#
|
||||
Pods/
|
||||
#
|
||||
# Add this line if you want to avoid checking in source code from the Xcode workspace
|
||||
# *.xcworkspace
|
||||
|
||||
# Carthage
|
||||
#
|
||||
# Add this line if you want to avoid checking in source code from Carthage dependencies.
|
||||
Carthage/Checkouts
|
||||
|
||||
Carthage/Build
|
||||
Carthage/Build/
|
||||
|
||||
# Accio dependency management
|
||||
Dependencies/
|
||||
.accio/
|
||||
|
||||
# fastlane
|
||||
#
|
||||
# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
|
||||
# screenshots whenever they are needed.
|
||||
# It is recommended to not store the screenshots in the git repo.
|
||||
# Instead, use fastlane to re-generate the screenshots whenever they are needed.
|
||||
# For more information about the recommended setup visit:
|
||||
# https://github.com/fastlane/fastlane/blob/master/docs/Gitignore.md
|
||||
# https://docs.fastlane.tools/best-practices/source-control/#source-control
|
||||
|
||||
fastlane/report.xml
|
||||
fastlane/screenshots
|
||||
fastlane/Preview.html
|
||||
fastlane/screenshots/**/*.png
|
||||
fastlane/test_output
|
||||
|
||||
# Code Injection
|
||||
#
|
||||
# After new code Injection tools there's a generated folder /iOSInjectionProject
|
||||
# https://github.com/johnno1962/injectionforxcode
|
||||
|
||||
# AppCode
|
||||
# https://intellij-support.jetbrains.com/hc/en-us/articles/206544839-How-to-manage-projects-under-Version-Control-Systems
|
||||
iOSInjectionProject/
|
||||
|
||||
.idea/workspace.xml
|
||||
.idea/tasks.xml
|
||||
# homebrew-bundle
|
||||
Brewfile.lock.json
|
||||
|
||||
# Node.js
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
|
||||
# Touch Instinct custom
|
||||
Downloads/
|
||||
fastlane/README.md
|
||||
Templates/
|
||||
cpd-output.xml
|
||||
*.swp
|
||||
*IDEWorkspaceChecks.plist
|
||||
|
||||
# Gem
|
||||
.gem/
|
||||
|
||||
.DS_Store
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
[submodule "build-scripts"]
|
||||
path = build-scripts
|
||||
url = https://git.svc.touchin.ru/TouchInstinct/BuildScripts.git
|
||||
|
|
@ -1 +0,0 @@
|
|||
3.0
|
||||
|
|
@ -0,0 +1,941 @@
|
|||
# Changelog
|
||||
|
||||
### 1.56.0
|
||||
- **Update**: `ViewSkeletonsConfiguration`. It's possible to enable or disable animation for specific skeletons now.
|
||||
- **Added**: `HolderViewSkeletonsConfiguration` for skeleton root view configuration
|
||||
- **Added**: `DashedBoundsLayer` can now be applied to `CALayer`
|
||||
|
||||
### 1.55.1
|
||||
- **Update**: revert `TextSkeletonsConfiguration` line height calculation
|
||||
|
||||
### 1.55.0
|
||||
|
||||
- **Update**: use TouchInstinct `TableKit` fork instead of original one
|
||||
- **Update**: remove default value from `BoolValueDefaultsStorage`
|
||||
|
||||
### 1.54.6
|
||||
|
||||
- **Added**: `xcprivacy` files
|
||||
- **Update**: Correctly detect app reinstall in `AppInstallLifetimeSingleValueStorage`
|
||||
- **Update**: use `xHeight` instead of `pointSize` for default skeleton line height calculation
|
||||
- **Update**: update `linkTextAttributes` in `UITextView` when setting interactive url parts
|
||||
|
||||
|
||||
### 1.54.5
|
||||
|
||||
- **Update**: Сhange `StatefulButton` event propogation avoidance method.
|
||||
|
||||
### 1.54.4
|
||||
|
||||
- **Update**: Fix `StatefulButton` state configuration for iOS 15+.
|
||||
|
||||
### 1.54.3
|
||||
|
||||
- **Update**: Set reasonable defaults for `SkeletonConfiguration`.
|
||||
|
||||
### 1.54.2
|
||||
|
||||
- **Update**: Changed access level from internal to public of title and subtitle view in `BaseTitleSubtitleView`.
|
||||
|
||||
### 1.54.1
|
||||
|
||||
- **Added**: `BaseTitleSubtitleView` which can be inherited for fine-tuning skeletons and other behavior.
|
||||
- **Update**: Changed lines number calculation method in `TextSkeletonsConfiguration`.
|
||||
|
||||
|
||||
### 1.54.0
|
||||
|
||||
- **Added**: `maxWidth` parameter to `BaseViewSkeletonsConfiguration`.
|
||||
- **Added**: custom `SkeletonConfigurations` for nested `SkeletonPresenters`.
|
||||
- **Update**: Many fixes and improvenments to `TextSkeletonsConfiguration`.
|
||||
|
||||
### 1.53.3
|
||||
|
||||
- **Update**: `Skeletonable` can now control custom geometry change notification.
|
||||
- **Update**: Filter hidden views from skeletonable views by default.
|
||||
|
||||
### 1.53.2
|
||||
|
||||
- **Update**: `DefaultTitleSubtitleView` support for separated configuration of title and subtitle labels layout.
|
||||
- **Update**: `BaseListItemView` fixed trailing insets when trailing view is hidden.
|
||||
|
||||
### 1.53.1
|
||||
|
||||
- **Update**: Insets layout heuristics for `WrappedViewHodler` implementations
|
||||
|
||||
### 1.53.0
|
||||
|
||||
- **Added**: Custom string attributes to `BaseTextAttributes`
|
||||
- **Added**: Customizeable `UIViewBackground` and `UIViewBorder` for `UIView.Appearance`
|
||||
- **Added**: Keychain single value storage for codable models -`CodableSingleValueKeychainStorage`
|
||||
- **Update**: Renamed methods `startAnimation` and `stopAnimation` of `SkeletonPresenter`, so it won't conflict with `Animatable` protocol anymore
|
||||
|
||||
### 1.52.0
|
||||
|
||||
- **Added**: `TIApplication` module with core dependencies of main application and its extension targets
|
||||
- **Added**: `DefaultHomogeneousItemsCollectionView` default collection view implementation with configurable identical-type cells
|
||||
- **Update**: Changed implementation of `AppInstallLifetimeSingleValueStorage`. Now it uses `SingleValueStorage<Bool>` to be able to migrate stored UserDefaults values
|
||||
- **Added**: `UserLocationFetcher.OnLocationFetchFailureCallback` and `ItemDistanceTo` in `TIMapUtils`
|
||||
- **Added**: Tap handler closure to `DefaultConfigurableStatefulButton.ViewModel`
|
||||
- **Added**: Universal DSL
|
||||
|
||||
|
||||
### 1.51.0
|
||||
|
||||
- **Added**: `BaseModalViewController` implementing `PanModalPresentable` with additional functionality
|
||||
- **Added**: `BaseModalWrapperViewController` for wrapping `UIViewController`s with `BaseModalViewController` functionality
|
||||
|
||||
### 1.50.0
|
||||
|
||||
- **Updated**: Fix activity indicator positioning for `StatefulButton` on iOS 15+ and disabled state touch handling
|
||||
- **Added**: iOS 15+ activity indicator placement support in `StatefulButton`
|
||||
- **Added**: `TICoreGraphicsUtils` module for drawing operations and other CoreGraphics related functionality
|
||||
- **Update**: `MarkerIconFactory` can now return optional `UIImage`. In this case MapManagers will show the default marker icon.
|
||||
|
||||
### 1.49.0
|
||||
|
||||
- **Added**: `BaseMigratingSingleValueKeychainStorage` and `BaseMigratingSingleValueDefaultsStorage` implementations for migrating keys from one storage to another.
|
||||
|
||||
### 1.48.0
|
||||
|
||||
- **Added**: `BaseStackView` with configurable items appearance
|
||||
- **Fixed**: `CollectionTableViewCell` self-sizing
|
||||
- **Added**: `ViewAppearance.WrappedViewLayout` support for all `WrappedViewHolders`
|
||||
- **Added**: `ViewCallbacks` support for all `BaseInitializeableViews`
|
||||
|
||||
### 1.47.0
|
||||
|
||||
- **Added**: `flatMap` operator for `AsyncOperation`
|
||||
- **Update**: `CodableKeyValueStorage` now returns `Swift.Result` with typed errors.
|
||||
- **Added**: `SingleValueExpirationStorage` for time aware entries (expirable api tokens, etc.)
|
||||
- **Added**: `AsyncOperation` variants of process methods in NetworkServices.
|
||||
|
||||
### 1.46.0
|
||||
|
||||
- **Added**: `AsyncSingleValueStorage` and `SingleValueStorageAsyncWrapper<SingleValueStorage>` for async access to SingleValue storages wtih swift concurrency support
|
||||
- **Added**: `BaseMapUISettings` used to configure map view of different providers + user location icon rendering for yandex maps
|
||||
- **Added**: `UserLocationFetcher` helper that requests authorization and subscribes to user location updates
|
||||
- **Update**: add `DEVELOPMENT_INSTALL` support for all podspecs and fix playground compilation issues
|
||||
|
||||
### 1.45.0
|
||||
|
||||
- **Added**: `SingleValueStorage` implementations + `AppInstallLifetimeSingleValueStorage` for automatically removing keychain items on app reinstall.
|
||||
- **Added**: `TILogging` with error logging types
|
||||
- **Update**: `DefaultRecoverableJsonNetworkService` supports iOS 12.
|
||||
- **Update**: `DefaultFingerprintsProvider` now uses `SingleValueStorage`
|
||||
|
||||
### 1.44.0
|
||||
|
||||
- **Added**: HTTP status codes to `EndpointErrorResult.apiError` responses
|
||||
- **Added**: SwiftLint pre-build SPM step to TINetworking module
|
||||
|
||||
### 1.43.1
|
||||
|
||||
- **Fixed**: build scripts submodule url
|
||||
|
||||
### 1.43.0
|
||||
|
||||
- **Added**: `TITextProcessing` for regex and text formatting added
|
||||
|
||||
### 1.42.1
|
||||
|
||||
- **Fixed**: Podspecs source and homepage urls
|
||||
|
||||
### 1.42.0
|
||||
|
||||
- **Added**: TIDeeplink to support deeplink API
|
||||
|
||||
### 1.41.0
|
||||
|
||||
- **Update**: added callbacks for views while skeletons change status to presented or hidden
|
||||
|
||||
### 1.40.0
|
||||
|
||||
- **Added**: `PlaceholderFactory` for creating `DefaultPlaceholderView` views
|
||||
- **Added**: `DefaultPlaceholderImageView`
|
||||
|
||||
### 1.39.0
|
||||
|
||||
- **Added**: UIButton Appearance model
|
||||
- **Added**: `SpacedWrappedViewLayout` for spacing configurations
|
||||
- **Update**: UIView appearance model with border configurations
|
||||
|
||||
### 1.38.0
|
||||
|
||||
- **Added**: Placemarks states for icon updating
|
||||
- **Added**: Selecting / deselecting markers through cluster manager
|
||||
|
||||
### 1.37.0
|
||||
|
||||
- **Added**: API for converting view hierarchy to skeletons
|
||||
|
||||
### 1.36.1
|
||||
|
||||
- **Update**: `YandexMapsMobile` version updated
|
||||
- **Fix**: Map manager memory leak removed
|
||||
|
||||
### 1.36.0
|
||||
|
||||
- **Removed**: `TILogger`module
|
||||
- **Updated**: moved `LoggingPresenter` to `TIDeveloperUtils` module.
|
||||
|
||||
### 1.35.1
|
||||
|
||||
- **Added**: Auto documentation generation for `TIFoundationUtils` playground and compile checks for playground before release
|
||||
- **Updated**: `AsyncOperation` fixed ordering of chain operations execution
|
||||
|
||||
### 1.35.0
|
||||
|
||||
- **Added**: `TIDeveloperUtils` framework, that contains different utils for development
|
||||
- **Added**: `UIView` and `UIViewController` extensions for showing SwiftUI previews
|
||||
- **Added**: `DashedBoundsLayer` for debugging views' frames visually
|
||||
|
||||
### 1.34.0
|
||||
|
||||
- **Added**: `BaseListItemView` for displaying three views horizontally
|
||||
- **Added**: `DefaultTitleSubtitleView` for displaying one or two labels vertically
|
||||
- **Update**: `StatefulButton` now can be configured with `ViewAppearance` model for each state
|
||||
|
||||
### 1.33.0
|
||||
|
||||
- **Added**: `ViewAppearance` and `ViewLayout` models for setting up Views' appearance and layout
|
||||
- **Added**: `TableKit.Row` extension for configuration inner View's appearance and layout
|
||||
- **Added**: `WrappableView` with typealiases for creating wrapped in the container views
|
||||
- **Added**: `CollectionTableViewCell` and `ContainerView`
|
||||
- **Update**: Separator appearance configureation for table views
|
||||
|
||||
### 1.32.0
|
||||
|
||||
- **Added**: `BaseInitializableWebView` with navigation and error handling api.
|
||||
|
||||
### 1.31.0
|
||||
|
||||
- **Added**: `URLInteractiveTextView` for terms and conditions hints in login flow
|
||||
|
||||
### 1.30.0
|
||||
|
||||
- **Added**: Base classes for encryption and decryption user token with pin code or biometry
|
||||
- **Added**: Pin code validators
|
||||
|
||||
### 1.29.1
|
||||
|
||||
- **Updated**: `BaseTextAttributes` correct detection of the necessity of using attributed string
|
||||
|
||||
### 1.29.0
|
||||
|
||||
- **Added**: `BaseTextAttributes`can now measure text size and provides paragraph style configuration API.
|
||||
- **Removed**: `ViewText`. Was fully replaced with `BaseTextAttributes`
|
||||
- **Fixed**: `Operation.flattenDependencies` used in `Operation.add(to:waitUntilFinished:)` now works correctly.
|
||||
- **Added**: Now it's possible to add dependent operation to start of the queue.
|
||||
|
||||
### 1.28.0
|
||||
|
||||
- **Add**: `LoggingPresenter`to present list of logs with ability of sharing it
|
||||
- **Add**: `TILogger` wrapper object to log events.
|
||||
|
||||
### 1.27.1
|
||||
|
||||
- **Fix**: Weak target reference in `RefreshControl`
|
||||
|
||||
### 1.27.0
|
||||
|
||||
- **Add**: Tag like filter collection view
|
||||
- **Add**: List like filter table view
|
||||
- **Add**: Range like filter view
|
||||
|
||||
### 1.26.3
|
||||
- **Update**: Add @escaping in `RequestExecutor.ExecutionClosure`
|
||||
|
||||
### 1.26.2
|
||||
- **Update**: Add failureCompletion in `RequestExecutor`
|
||||
|
||||
### 1.26.1
|
||||
- **Fix**: Use OperationQueue instead of NSLock in `DefaultTokenInterceptor`
|
||||
- **Update**: AsyncOperation refactoring
|
||||
|
||||
### 1.26.0
|
||||
|
||||
- **Add**: `TIEcommerce` module with Cart, products, promocodes, bonuses and other related actions.
|
||||
|
||||
### 1.25.0
|
||||
|
||||
- **Update**: `RequestError` cases now contain additional url assotiated value
|
||||
- **Update**: Network requests error catching now throws `RequestError` with url
|
||||
|
||||
### 1.24.0
|
||||
|
||||
- **Add**: `AlertFactory` for presenting alerts in SwiftUI and UIKit.
|
||||
|
||||
### 1.23.0
|
||||
|
||||
- **Update**: `UITextView` now support configuration with `BaseTextAttributes`
|
||||
- **Add**: `ReconfigurableView` & `ChangeableViewModel` for non-destructing state update
|
||||
- **Add**: `WrappedViewHolder` protocol with table/collection view cell implementations
|
||||
- **Add**: `UIViewPresenter` and `ReusableUIViewPresenter` protocols with default implementation for proper handling view/cells reuse
|
||||
|
||||
### 1.22.0
|
||||
|
||||
- **Update**: Asynchronous request preprocessing
|
||||
|
||||
### 1.21.0
|
||||
|
||||
- **Update**: `AsyncEventHandler` was replaced with `EndpointRequestRetrier`
|
||||
- **Add**: `FingerprintsTrustEvaluator` and `FingerprintsProvider` for fingerprint-based host trust evaluation
|
||||
- **Add**: `DefaultTokenInterceptor` for queue-based token refresh across all requests of single api interactor (network service).
|
||||
- **Update**: `DefaultRecoverableJsonNetworkService` now returns collection of errors in result
|
||||
- **Update**: `CancellableTask` was renamed to `Cancellable`. Cancellable implementations has been moved from `TIMoyaNetworking` to `TIFoundationUtils`.
|
||||
- **Add**: `ApiInteractor` protocol with basic request/response methods
|
||||
|
||||
|
||||
### 1.20.0
|
||||
|
||||
- **Add**: OpenAPI security schemes support for EndpointRequest's.
|
||||
- **Update**: Replace `AdditionalHeadersPlugin` with `SecuritySchemePreprocessor` and `EndpointRequestPreprocessor` (with default implementations)
|
||||
|
||||
### 1.19.0
|
||||
|
||||
- **Add**: Add presenter protocols to `TISwiftUICore` and `TIUIKitCore` modules
|
||||
- **Add**: `CodeConfirmPresenter` protocol and `DefaultCodeConfirmPresenter` implementation in `TIAuth` module
|
||||
|
||||
### 1.18.0
|
||||
|
||||
- **Add**: add MapManagers for routine maps configuration
|
||||
|
||||
### 1.17.0
|
||||
|
||||
- **Add**: add smooth CameraUpdate actions for supported maps
|
||||
|
||||
### 1.16.2
|
||||
|
||||
- **Update**: `DefaultRecoverableJsonNetworkService` now supports error forwarding from its error handlers to initial requests.
|
||||
|
||||
### 1.16.1
|
||||
|
||||
- **Update**: `DateFormattersReusePool` and `ISO8601DateFormattersReusePool` are now thread safe.
|
||||
|
||||
### 1.16.0
|
||||
|
||||
- **Add**: `TIMapUtils`, `TIAppleMapUtils`, `TIGoogleMapUtils` and `TIYandexMapUtils` modules for map items clustering and interacting with them.
|
||||
|
||||
### 1.15.0
|
||||
|
||||
- **Update**: Network services in TIMoyaNetworking now passes MoyaError in result of EnpointRequest execution.
|
||||
- **Add**: `TINetworkingCache` module - caching results of EndpointRequests.
|
||||
- **Important Note**: `TINetworkingCache` added via SPM may require you to add `DISABLE_DIAMOND_PROBLEM_DIAGNOSTIC=YES` flag to build settings of project target (see [probably related problem](https://forums.swift.org/t/adding-a-package-to-two-targets-in-one-projects-results-in-an-error/35007/18))
|
||||
|
||||
### 1.14.3
|
||||
|
||||
- **Fix**: Creating headerView and footerView when initializing a section with rows in `TITableKitUtils`.
|
||||
- **Add**: Empty table section initialization method in `TITableKitUtils`.
|
||||
|
||||
### 1.14.2
|
||||
|
||||
- **Update**: DateFormatters properties preset in reuse pools
|
||||
|
||||
### 1.14.1
|
||||
|
||||
- **Fix**: Array encoding for `QueryStringParameterEncoding`
|
||||
|
||||
### 1.14.0
|
||||
|
||||
- **Add**: [Date] coding methods
|
||||
|
||||
### 1.13.0
|
||||
|
||||
- **Update**: Change access modifiers in `DefaultJsonNetworkService` from `public` to `open`, added additional Moya plugins processing
|
||||
- **Add**: `DisplayDecodingErrorPlugin` for showing developer-frendly decoding error messages
|
||||
- **Add**: Gemfile for cocoapods versioning
|
||||
|
||||
### 1.12.3
|
||||
|
||||
- **Fix**: Try parse date in ISO8601 format appending `.withFractionalSeconds` if `.withInternetDateTime` fails
|
||||
|
||||
### 1.12.2
|
||||
|
||||
- **Fix**: HeaderParameterEncoding encodes array correctly
|
||||
|
||||
### 1.12.1
|
||||
|
||||
- **Update**: DefaultRecoverableNetworkService `request` parameter was renamed to prevent ambgious reference
|
||||
|
||||
### 1.12.0
|
||||
|
||||
- **Update**: EndpointRequest Body can take a nil value
|
||||
- **Update**: Parameter value can be nil as well
|
||||
- **Update**: observe operator of AsyncOperation now accepts callback queue parameter
|
||||
|
||||
### 1.11.1
|
||||
- **Fix**: `timeoutIntervalForRequest` parameter for `URLSessionConfiguration` in `NetworkServiceConfiguration` added.
|
||||
|
||||
### 1.11.0
|
||||
- **Breaking changes**: many method signatures was changes in `TIMoyaNetworking`.
|
||||
- **Add**: `ISO8601DateFormattersReusePool` and codable helpers for ISO8601 date (de)coding.
|
||||
- **Add**: Moya plugin protocol for adding HTTP headers with default implementation.
|
||||
|
||||
### 1.10.0
|
||||
- **Add**: `DefaultRecoverableJsonNetworkService` with error handling chain.
|
||||
|
||||
|
||||
### 1.9.0
|
||||
- **Add**: `TIMoyaNetworking` target - Moya + Swagger network service.
|
||||
- **Update**: `TISwiftUtils` - added async closure typealiases.
|
||||
- **Update**: `TIFoundationUtils` - added date formatting & decoding helpers.
|
||||
|
||||
### 1.8.0
|
||||
- **Add**: `TIFoundationUtils.AsyncOperation` - generic subclass of Operation with chaining and result observation support
|
||||
|
||||
### 1.7.0
|
||||
- **Add**: `TINetworking` - Swagger-frendly networking layer helpers
|
||||
|
||||
### 1.6.0
|
||||
- **Add**: the pretty timer - TITimer.
|
||||
|
||||
### 1.5.0
|
||||
- **Add**: `HeaderTransitionDelegate` - Helper for transition of TableView header and navigationBar title view
|
||||
|
||||
### 1.4.0
|
||||
- **Update**: update minor dependencies.
|
||||
- **Fix**: project's scripts.
|
||||
|
||||
### 1.3.0
|
||||
- **Add**: `TIPaginator` - realisation of paginating items from a data source.
|
||||
|
||||
### 1.2.0
|
||||
- **Add**: `TIKeychainUtils` - Set of helpers for Keychain classes.
|
||||
|
||||
### 1.1.1
|
||||
- **Fix**: `StatefullButton` propagation
|
||||
|
||||
### 1.1.0
|
||||
- **Add**: `BaseInitializeableViewController`, `BaseCustomViewController` and `BaseViewController` to TIUIKitCore.
|
||||
- **Add**: `TableKitTableView` and `TableDirectorHolder` to TITableKitUtils.
|
||||
|
||||
### 1.0.0
|
||||
- **API BreakingChanges**: up swift version to 5.1.
|
||||
- **Update**: build scripts.
|
||||
- **Update**: code with new swiftlint rules.
|
||||
- **Update**: RxSwift to 6.0.0.
|
||||
|
||||
### 0.13.1
|
||||
- **Fix**: LeadKit.podspec file.
|
||||
|
||||
### 0.13.0
|
||||
- **Add**: Githook `prepare-commit-msg` to check commit's style.
|
||||
- **Add**: Setup script.
|
||||
|
||||
### 0.12.0
|
||||
- **Add**: StatefulButton & RoundedStatefulButton to TIUIElements.
|
||||
- **Add**: added CACornerMask rounding extension to TIUIElements.
|
||||
- **Add**: UIControl.State dictionary extensions to TIUIKitCore.
|
||||
- **Add**: UIFont and CTFont extensions to TIUIKitCore.
|
||||
- **Breaking change**: reworked BaseTextAttributes & ViewText. Removed ViewTextConfigurable protocol & conformances.
|
||||
|
||||
### 0.11.0
|
||||
- **Add**: Cocoapods support for TI-family libraries.
|
||||
- **Add**: `SeparatorConfigurable` and all helper types for separator configuration.
|
||||
- **Add**: `BaseSeparatorCell` - `BaseInitializeableCell` subclass with separators support.
|
||||
- **Add**: `TITableKitUtils` - set of helpers for TableKit classes.
|
||||
- **Add**: `BaseTextAttributes` and `ViewText` implementation form LeadKit.
|
||||
- **Update**: `BaseInitializableView` and `BaseInitializableControl` are moved to `TIUIElements` from `TIUIKitCore`.
|
||||
|
||||
### 0.10.9
|
||||
- **Fix**: `change presentedOrTopViewController to open`.
|
||||
|
||||
### 0.10.8
|
||||
- **Fix**: `Add presentedOrTopViewController`.
|
||||
|
||||
### 0.10.7
|
||||
- **Fix**: `Add BaseOrientationController`.
|
||||
- **Fix**: `Add videoOrientation extension`.
|
||||
|
||||
### 0.10.6
|
||||
- **Fix**: `Add tvos exclude files`.
|
||||
|
||||
### 0.10.5
|
||||
- **Add**: `OrientationNavigationController` .
|
||||
- **Add**: `Forced Interface Orientation logic to BaseConfigurableController` .
|
||||
- **Fix**: `Exclude files to watchos and tvos`.
|
||||
|
||||
### 0.10.4
|
||||
- **Fix**: `noConnection` error.
|
||||
|
||||
### 0.10.3
|
||||
- **Fix**: `mappingQueue` of `SessionManager`.
|
||||
|
||||
### 0.10.2
|
||||
- **Add**: `RefreshControl` - a basic UIRefreshControl with fixed refresh action.
|
||||
|
||||
### 0.10.1
|
||||
- **Update**: Third party dependencies: `Alamofire` 5.2.2, `RxAlamofire` 5.6.1
|
||||
|
||||
### 0.10.0
|
||||
- **Update**: Third party dependencies: `RxSwift` (and all sub-dependencies) to 5.1.0, `Alamofire` 5.0, `SnapKit` 5.0
|
||||
- **Refactored**: NetworkManager to use new Alamofire API
|
||||
- **API BreakingChanges**: NetworkServiceConfiguration no longer accepts `ServerTrustPolicy`, it is now replaced by an instance of a `ServerTrustEvaluating` protocol. Full description and default implementations can be found at Alamofire [sources](https://github.com/Alamofire/Alamofire/blob/master/Source/ServerTrustEvaluation.swift). Since new evaluation is used, evaluation against self-signed certificates will now throw an AfError and abort any outcoming request. To support self-signed certificates use `DisabledTrustEvaluator` for specified host in configuration.
|
||||
- **Removed**: UIImage+SupportExtensions, UIScrollView+Support
|
||||
|
||||
### 0.9.44
|
||||
- **Add**: `TIFoundationUtils` - set of helpers for Foundation framework classes.
|
||||
|
||||
#### TISwiftUtils
|
||||
- **Add**: `BackingStore` - a property wrapper that wraps storage and defines getter and setter for accessing value from it.
|
||||
|
||||
#### TIFoundationUtils
|
||||
- **Add**: `CodableKeyValueStorage` - storage that can get and set codable objects by the key.
|
||||
|
||||
### 0.9.43
|
||||
- **Fix**: `OTPSwiftView`'s dependencies.
|
||||
|
||||
### 0.9.42
|
||||
- **Fix**: Logic bugs of `PaginationWrapper`.
|
||||
|
||||
### 0.9.41
|
||||
- **Add**: `OTPSwiftView` - a fully customizable OTP view.
|
||||
- **Add**: `BaseInitializableControl` UIControl conformance to InitializableView.
|
||||
- **Add**: `TISwiftUtils` a bunch of useful helpers for development.
|
||||
|
||||
### 0.9.40
|
||||
- **Fix**: Load more request repetion in `PaginationWrapper`.
|
||||
|
||||
### 0.9.39
|
||||
- **Add**: `Animatable` protocol to TIUIKitCore.
|
||||
- **Add**: `ActivityIndicator` protocol to TIUIKitCore.
|
||||
- **Add**: `ActivityIndicatorHolder` protocol to TIUIKitCore.
|
||||
- **Add**: `TIUIElements` for ui elements.
|
||||
|
||||
### 0.9.38
|
||||
- **Add**: `BaseRxTableViewCell` is subclass of `UITableViewCell` class with support `InitializableView` and `DisposeBagHolder` protocols.
|
||||
- **Add**: `ContainerTableCell` is container class that provides wrapping any `UIView` into `UITableViewCell`.
|
||||
- **Add**: `BaseTappableViewModel` is simplifies interaction between view and viewModel for events of tapping.
|
||||
- **Add**: `VoidTappableViewModel` is subclass of `BaseTappableViewModel` class with void payload type.
|
||||
|
||||
### 0.9.37
|
||||
- **Fix**: ScrollView content offset of `PaginationWrapper` for iOS 13.
|
||||
- **Fix**: Load more request crash of `PaginationWrapper`.
|
||||
|
||||
### 0.9.36
|
||||
- **Add**: SPM Package.swift.
|
||||
- **Add**: TITransitions via SPM.
|
||||
- **Add**: TIUIKitCore via SPM.
|
||||
- **Update**: Readme.
|
||||
|
||||
### 0.9.35
|
||||
- **Add**: Selector `refreshAction()` for refresh control of `PaginationWrapper`.
|
||||
|
||||
### 0.9.34
|
||||
- **Add**: `ButtonHolder` - protocol that contains button property.
|
||||
- **Add**: `ButtonHolderView` - view which contains button.
|
||||
- **Add**: Conformance `UIButton` to `ButtonHolder`.
|
||||
- **Add**: Conformance `BasePlaceholderView` to `ButtonHolderView`.
|
||||
- **[Breaking change]**: Replace functions `footerRetryButton() -> UIButton?` to `footerRetryView() -> ButtonHolderView?` and `footerRetryButtonHeight() -> CGFloat` to `footerRetryViewHeight() -> CGFloat` for `PaginationWrapperUIDelegate`.
|
||||
- **[Breaking change]**: Replace functions `footerRetryButtonWillAppear()` to `footerRetryViewWillAppear()` and `footerRetryButtonWillDisappear()` to `footerRetryViewWillDisappear()` for `PaginationWrapperUIDelegate`.
|
||||
|
||||
### 0.9.33
|
||||
- **Fix**: `CustomizableButtonView` container class that provides great customization.
|
||||
- **Fix**: `CustomizableButtonViewModel` viewModel class for `CustomizableButtonView` configuration.
|
||||
|
||||
### 0.9.32
|
||||
- **Fix**: `CustomizableButtonView` container class that provides great customization.
|
||||
|
||||
### 0.9.31
|
||||
- **Add**: `@discardableResult` to function - `replace(with:at:with:manualBeginEndUpdates)` in `TableDirector`.
|
||||
|
||||
### 0.9.30
|
||||
- **Add**: character `*` into a valid set of characters in the extension `telpromptURL` of String.
|
||||
|
||||
### 0.9.29
|
||||
- **Update**: remove Carthage binary dependencies, update build scripts.
|
||||
|
||||
### 0.9.28
|
||||
- **Add**: method `presentFullScreen(_ viewController:presentationStyle:animated:completion:)` for `UIViewController` that present any `viewController` modally in full screen mode by default (avoid problems with *iOS13* default presentation mode changed to `.automatic` stork)
|
||||
|
||||
### 0.9.27
|
||||
- **Add**: method `date(from string:formats:parsedIn:)` method for `DateFormattingService` that parses date from string in one of the given formats with current region.
|
||||
|
||||
### 0.9.26
|
||||
- **Add**: method `processResultFromConfigurationSingle` for `TotalCountCursor` that allows to get server response.
|
||||
- **Add**: possibility to inherit from `TotalCountCursor`.
|
||||
|
||||
### 0.9.25
|
||||
- **Add**: `queryItems` parameter for `ApiRequestParameters`.
|
||||
- **Add**: `asQueryItems` method for `Encodable` that converts model to query items array.
|
||||
|
||||
### 0.9.24
|
||||
- **Fix**: Make `ApiRequestParameters` properties public.
|
||||
|
||||
### 0.9.23
|
||||
- **Add**: Rounding for `Decimal`.
|
||||
- **Add**: `doubleValue` property for `Decimal`.
|
||||
- **Add**: `intValue` property for `Decimal`.
|
||||
- **Fix**: Rounding for `Double`.
|
||||
|
||||
### 0.9.22
|
||||
- **Fix**: Make `Initializable` protocol public.
|
||||
|
||||
### 0.9.21
|
||||
- **Add**: `Initializable` - common protocol for object types that can be initialized without params.
|
||||
- **Add**: `instantiateArray(count:)` function in `Initializable` extension to initialize an array of instances.
|
||||
|
||||
### 0.9.20
|
||||
- **Fix**: `bindBottomInsetBinding(from bottomInsetDriver:)` in `BaseScrollContentController` works correctly now.
|
||||
|
||||
### 0.9.19
|
||||
- **Add**: `hexString` property for `UIColor` that returns hex representation of color as string.
|
||||
|
||||
### 0.9.18
|
||||
- **Add**: `CustomizableButtonView` container class that provides great customization.
|
||||
- **Add**: `CustomizableButtonViewModel` viewModel class for `CustomizableButtonView` configuration.
|
||||
- **Add**: `CustomizableButton` class that is a `CustomizableButtonView` subview and gives it a button functionality.
|
||||
|
||||
### 0.9.17
|
||||
- **Fix**: SpinnerView infinity animation.
|
||||
|
||||
### 0.9.16
|
||||
- **Add**: `LabelTableViewCell` moved from `LeadKitAdditions`.
|
||||
- **Add**: `SnapKit` dependency.
|
||||
|
||||
### 0.9.15
|
||||
- **Add**: `BaseSearchViewController` class that allows to enter text for search and then displays search results in table view.
|
||||
- **Add**: `BaseSearchViewModel` class that loads data from a given data source and performs search among the results.
|
||||
- **Add**: `SearchResultsController` protocol that represent a controller able to display search results.
|
||||
- **Add**: `SearchResultsControllerState` enum that represents `SearchResultsController` state.
|
||||
|
||||
### 0.9.14
|
||||
- **Update**: SwiftDate dependency (~> 6).
|
||||
|
||||
### 0.9.13
|
||||
- **Add**: `ApiUploadRequestParameters` struct that defines upload data request parameters.
|
||||
- **Add**: `rxUploadRequest` method to `NetworkService` class that performs reactive request to upload data.
|
||||
- **Add**: `uploadResponseModel` method to `SessionManager` extension that executes upload request and serializes response.
|
||||
- **Add**: `handleMappingError` method to `Error` extension that tries to serialize response from a mapping request error to a model.
|
||||
- **Add**: `handleMappingError` method to `ObservableType`, `Single`, `Completable` extensions that handles a mapping error and serialize response to a model.
|
||||
- **Add**: `validate` method to `DataRequest` observable extension that validates status codes and catch network errors.
|
||||
- **Add**: `dataApiResponse` method to `DataRequest` reactive extension that serializes response into data.
|
||||
- **Update**: `validStatusCodes` parameter in network methods renamed to `additionalValidStatusCodes`.
|
||||
|
||||
### 0.9.12
|
||||
- **Update**: Swift 5 support
|
||||
|
||||
### 0.9.11
|
||||
- **[Breaking change]**: Renamed `NumberFormat`'s `allOptions` to `allCases`
|
||||
- **Fix**: Closure syntax fix. New closure naming.
|
||||
- **Fix**: Added missing `BasePlaceholderView` protocol function.
|
||||
|
||||
### 0.9.10
|
||||
- **Remove**: Removed unused scheme & target
|
||||
- **Remove**: Cocoapods deintegrated
|
||||
- **Update**: New closure typealiases
|
||||
|
||||
### 0.9.9
|
||||
- **Add**: `validStatusCodes` parameter to request methods in `NetworkService` class, that expands valid status codes for request.
|
||||
- **Add**: `validStatusCodes` parameter to response methods in `SessionManager` extension, that expands valid status codes for request.
|
||||
|
||||
### 0.9.8
|
||||
- **Add**: `rxDataRequest` method to `NetworkService` class, that performs reactive request to get data and http response.
|
||||
- **Add**: `responseData` method to `SessionManager` extension, that executes request and returns data.
|
||||
|
||||
### 0.9.7
|
||||
- **Add**: Carthage support.
|
||||
|
||||
### 0.9.6
|
||||
- **Add**: Add new `configureSeparators` method to `SeparatorRowBox` array.
|
||||
|
||||
### 0.9.5
|
||||
- **Add**: `TitleType` enum, that defines `UIViewController`'s title type.
|
||||
- **Add**: `UINavigationItem.largeTitleDisplayMode` property, that defines `UINavigationItem`'s large title display mode.
|
||||
- **Add**: `UIViewController.updateNavigationItemTitle` method, that takes `TitleType` as a parameter and updates `UIViewController`'s title.
|
||||
|
||||
### 0.9.4
|
||||
- **Add**: initialization of `ApiRequestParameters`, that takes an array as a request parameter.
|
||||
- **Add**: `NetworkServiceConfiguration.apiRequestParameters` method, that creates `ApiRequestParameters` with array request parameter.
|
||||
- **Add**: `SessionManager.request` method, that takes an array as a request parameter.
|
||||
- **Add**: `RequestUsageError` error, that represents wrong usage of requset parameters.
|
||||
|
||||
### 0.9.3
|
||||
- **Add**: `Insert`/`Remove` section with animation functions to `TableKit`. Also make new function `Replace` that uses new `Insert`/`Remove` to animate section replace.
|
||||
|
||||
### 0.9.2
|
||||
- **Update**: Add response to `RequestError`.
|
||||
- **Fix**: Update `SessionManager+Extensions` to catch network connection error.
|
||||
|
||||
### 0.9.1
|
||||
- **Update**: `DataRequest+Extensions` time out as network error
|
||||
|
||||
### 0.9.0
|
||||
- **Update**: version update.
|
||||
|
||||
### 0.8.13
|
||||
- **Add**: `configureLayout` method to `InitializeableView` protocol and all implementations.
|
||||
- **Update**: `GeneralDataLoadingViewModel` now can handle state changes and result of data source. Previously it was possible only in view controller.
|
||||
- **Add**: `GeneralDataLoadingHandler` protocol, that defines methods for common data loading states handling.
|
||||
- **Add**: `resultObservable` and `resultDriver` properties to `GeneralDataLoadingViewModel`.
|
||||
- **Add**: `hidesWhenStopped` option to `SpinnerView`, so you can stop animation without hiding image inside it.
|
||||
- **Update**: Migrate to Swift 4.2 & Xcode 10. Update dependencies.
|
||||
|
||||
### 0.8.12
|
||||
- **Add**: `UserDefaults+Codable` is back. Now with generic subscript support.
|
||||
|
||||
### 0.8.11
|
||||
- **Change**: `NumberFormattingService.computedFormatters` computed var reverted to static.
|
||||
|
||||
### 0.8.10
|
||||
- **[Breaking change]**: `NumberFormattingService` methods is not static anymore.
|
||||
- **Add**: `NSNumberConvertible` protocol for `NumberFormattingService` use cases.
|
||||
- **Add**: `TableDirector` methods for rows insertion and removal without reload a whole table.
|
||||
- **Add**: `UIImageView` binder for disclosure indicator rotation.
|
||||
- **Add**: `UIView.addSubviews(:)` methods with variable number of arguments and array of views.
|
||||
- **Add**: `PlaceholderConfigurable` that defines attributes and methods for view with placeholder and regular state.
|
||||
- **Add**: `ContentLoadingViewModel` enum that describes possible `PlaceholderConfigurable` view states.
|
||||
|
||||
### 0.8.9
|
||||
- **Add**: Methods `replace(with:)`, `asVoid()`, `asOptional()` to `ObservableType`, `SharedSequence` (aka `Driver`) and `Single`.
|
||||
- **Add**: `Completable.deferredJust(:)` static method.
|
||||
- **Add**: `ViewTextConfigurable` protocol. Conform `UILabel`, `UITextField` and `UIButton` to this protocol.
|
||||
- **Add**: `BaseTextAttributes` with base text appearance attributes.
|
||||
- **Update**: `ViewText.string` now uses `BaseTextAttributes` instead of separate properties.
|
||||
- **Add**: `BasePlaceholderView` and `BasePlaceholderViewModel` classes used to create your own placeholder.
|
||||
- **Add**: `TableKitViewModel` protocol that adds convenient extensions to cell view models that implements it.
|
||||
|
||||
### 0.8.8
|
||||
- **Update**: Update `DateFormat` protocol. Add `dateToStringFormat` and `stringToDateFormat` according to SwiftDate 5.0.
|
||||
- **Update**: Replace `String` with `DateFormat` in `DataFormattingService` date parsing methods.
|
||||
- **Update**: Replace `DateInRegion` with `DateRepresentable` in `DataFormattingService` string formatting methods.
|
||||
- **Add**: `parsedIn` optional parameter to date parsing method in `DataFormattingService`.
|
||||
|
||||
### 0.8.7
|
||||
- **Add**: Base configurable controllers hierarchy with generic custom view argument (`BaseConfigurableController`, `BaseCustomViewController`, `BaseScrollContentController`, `BaseTableContentController` and `BaseCollectionContentController`).
|
||||
- **Add**: `ScrollViewHolder`, `TableViewHolder` and `CollectionViewHolder` protocols.
|
||||
- **Update**: Update dependencies.
|
||||
- **[Breaking change]**: Update `SwiftDate` to 5.0.x.
|
||||
- **[Breaking change]**: Update `DateFormattingService`. Change `format` argument from `DateFormatType` to `String`.
|
||||
- **Update**: Add compile time debug messages. Improve compile time for some pieces of code.
|
||||
|
||||
### 0.8.6
|
||||
|
||||
- **Fix**: Add `trustPolicies` param to `NetworkServiceConfiguration` initialization.
|
||||
- **Fix**: Update `serverTrustPolicies` to save host instead of the whole URL as a key.
|
||||
- **Add**: String extension that extracts host.
|
||||
|
||||
### 0.8.5
|
||||
|
||||
- **Add**: `replaceDataSource` method to `RxNetworkOperationModel`.
|
||||
- **Add**: `customErrorHandler` constructor parameter to `RxNetworkOperationModel` and it heirs.
|
||||
|
||||
### 0.8.4
|
||||
|
||||
- **Fix**: Add `SeparatorCell` to `Core-iOS-Extension`.
|
||||
- **Fix**: `UIApplication` extensions path for `Core-iOS-Extension` exclusions.
|
||||
|
||||
### 0.8.3
|
||||
|
||||
- **Fix**: `SpinnerView` animation freezing
|
||||
|
||||
### 0.8.2
|
||||
|
||||
- **Add**: `acceptableStatusCodes` property to `NetworkServiceConfiguration`
|
||||
|
||||
### 0.8.1
|
||||
|
||||
- **Add**: Support for `localizedComponent` for `FixedWidthInteger`
|
||||
|
||||
|
||||
### 0.8.0
|
||||
- **Add**: tests for `NetworkService`
|
||||
- **Add**: `toJSON(with encoder: JSONEncoder)` method to `Encodable`
|
||||
- **Add**: `failedToDecode` error case to `LeadKitError`
|
||||
- **Add**: `SessionManager` class
|
||||
- **Remove**: occurrences `ObjectMapper` pod and its occurrences in code
|
||||
- **Update**: replace `ObjectMapper` mapping with `Decodable`
|
||||
|
||||
### 0.7.19
|
||||
- **Fix**: `PaginationWrapper` retry button showing.
|
||||
|
||||
### 0.7.18
|
||||
- **Update**: default implementation of `PaginationWrapperUIDelegate`.
|
||||
|
||||
### 0.7.17
|
||||
- **Add**: `RxNetworkOperationModel` base class, `NetworkOperationState` and `NetworkOperationStateType` protocols.
|
||||
|
||||
### 0.7.16
|
||||
- **[Breaking Change]**: Remove `ModuleConfigurator`, change type of `ConfigurableController.viewModel` property from `IUO` to plain `ViewModelT`.
|
||||
- **Add**: `InitializableView` protocol with default implementation.
|
||||
- **Update**: `ConfigurableController` protocol now inherit `InitializableView`.
|
||||
- **[Breaking Change]**: `setAppearance` of `ConfigurableController` replaced with `configureAppearance` of `InitializableView`.
|
||||
|
||||
### 0.7.15
|
||||
- **Fix**: `Double.roundValue(withPrecision:)` rounding issue
|
||||
- **Add**: `Double+Rounding` test case
|
||||
|
||||
### 0.7.14
|
||||
- **[Breaking Change]**: `PaginationWrapper` separating state views from data loading.
|
||||
|
||||
### 0.7.13
|
||||
- **Update**: Migrate from `Variable` to `BehaviorRelay`.
|
||||
- **Fix**: `PaginationWrapper` retry load more after fail.
|
||||
- **Fix**: `safeClear` method of `TableDirector` now creates section without header and footer.
|
||||
- **Add**: `TableSection` convenience initializer for section without footer and header.
|
||||
|
||||
### 0.7.12
|
||||
- **Add**: `UniversalMappable` protocol to have ability generate generic mapping models
|
||||
|
||||
### 0.7.11
|
||||
- **Fix**: `addHeaderBackground` cells overlapping.
|
||||
|
||||
### 0.7.10
|
||||
- **Fix**: `wtihInsets` renamed to `with insets`
|
||||
|
||||
### 0.7.9
|
||||
- **Fix**: timeoutInterval is set to another URLSessionConfiguration property in NetworkServiceConfiguration
|
||||
|
||||
### 0.7.8
|
||||
- **Remove**: `App`, `Log` and `LogFormatter`.
|
||||
- **Remove**: `CocoaLumberjack` dependency.
|
||||
- **Add**: Rotate operation for image drawing.
|
||||
- **Add**: `mapViewEvents` overload with closure that returns array of disposables.
|
||||
- **Update**: Update `ObjectMapper` to 3.1.
|
||||
- **Add**: `apiRequestParameters` method to `NetworkServiceConfiguration` extension.
|
||||
- **Update**: Rename setToCenter(withInsets:) to pintToSuperview(withInsets:excluding:)
|
||||
- **Update**: Added parameter "edges" with label "excluding" to aforementioned method
|
||||
|
||||
### 0.7.7
|
||||
- **Fix**: Fix doubling separator line issue
|
||||
|
||||
### 0.7.6
|
||||
|
||||
- **Add**: `NetworkServiceConfiguration` to configure NetworkService instance
|
||||
- **Remove**: `ConfigurableNetworkSevice` protocol
|
||||
- **Update**: Acceptable status codes in SessionManager become `Set<Int>`
|
||||
|
||||
### 0.7.5
|
||||
- **Add**: `topConfiguration` and `bottomConfiguration` properties, methods to configure top and bottom separators in `CellSeparatorType` extension.
|
||||
- **Add**: `totalHeight` property in `SeparatorConfiguration` extension.
|
||||
|
||||
### 0.7.4
|
||||
- **Update**: Exclude UIApplication extensions from iOS-Extension subspec.
|
||||
|
||||
### 0.7.3
|
||||
- **Update**: Xcode 9.3 migration.
|
||||
- **Remove**: Default initializer for Network service that conforms to `ConfigurableNetworkService` protocol.
|
||||
- **[Breaking Change]**: `DateFormattingService` class replaced with protocol.
|
||||
- **Add**: `SwiftDate` dependency for `DateFormattingService`.
|
||||
- **Add**: `ViewBackground` enum that describes possible view backgrounds.
|
||||
- **Add**: `ViewText` enum that describes text with appearance options.
|
||||
- **Removed**: `String+SizeCalculation` extension.
|
||||
|
||||
### 0.7.2
|
||||
|
||||
- **Fixed**: Change root controller for window
|
||||
|
||||
### 0.7.1
|
||||
- **Add**: Extension for comparing optional arrays (`[T]?`) with `Equatable` elements.
|
||||
- **Add**: `additionalHttpHeaders` static field in `ConfigurableNetworkService` protocol.
|
||||
- **Add**: Default initializer for Network service that conforms to `ConfigurableNetworkService` protocol.
|
||||
|
||||
## 0.7.0
|
||||
- **Add**: `TotalCountCursor` for total count based pagination and related stuff.
|
||||
- **[Breaking Change]**: `PaginationTableViewWrapper` and `PaginationTableViewWrapperDelegate` was renamed to `PaginationWrapper` and `PaginationWrapperDelegate `. Also there is significant changes in api
|
||||
- **Add**: `GeneralDataLoadingModel` and `PaginationDataLoadingModel` for regular and paginated data loading with state handling.
|
||||
- **Add**: `GeneralDataLoadingViewModel` and `GeneralDataLoadingController` for regular data loading and state handling in UI.
|
||||
- **Add**: `ConfigurableNetworkService` - replacement of `DefaultNetworkService` from LeadKitAdditions.
|
||||
- **Add**: `NumberFormattingService` and `NumberFormat` protocols with default implementation for creating per-project number formatters.
|
||||
- **Add**: Very flexible in configuration `TextFieldViewModel` with build-in two-side data model binding.
|
||||
- **Add**: `SingleLoadCursorConfiguration` as a replacement of `SingleLoadCursor`.
|
||||
- **Add**: `UIApplication` extensions for making phone calls.
|
||||
- **Add**: `NSAttributedString` extensions for appending attributed strings using `+` operator.
|
||||
- **Change**: Lots of fixes and enhancements.
|
||||
- **Update**: Update dependecies versions.
|
||||
|
||||
### 0.6.7
|
||||
|
||||
- **Add**: UITableView extension to add colored background for tableview bounce area.
|
||||
|
||||
### 0.6.6
|
||||
|
||||
- **Add**: Ability to map primitive response types (`String`, `Int`, `[String]`, etc.).
|
||||
|
||||
### 0.6.5
|
||||
|
||||
- **Add**: Ability to handle responses with non-default status codes.
|
||||
|
||||
### 0.6.4
|
||||
|
||||
- **Fix**: SpinnerView bug(no animation) in Swift 4.
|
||||
|
||||
### 0.6.3
|
||||
- **Fix**: SeparatorCell updates constraints after setting separator insets
|
||||
|
||||
### 0.6.2
|
||||
- **Fix**: AlamofireManager extension no longer performs requests with default manager
|
||||
|
||||
### 0.6.1
|
||||
- **New**: `RequestError`. Represents general api request errors
|
||||
- **Change**: All api methods now throws `RequestError` when fails.
|
||||
|
||||
## 0.6.0
|
||||
- **New**: Swift 4 support & dependencies update.
|
||||
- **Remove**: `Mutex`
|
||||
- **Remove**: `IndexPath+ImmutableIndexPath`
|
||||
- **Remove**: `StoryboardProtocol`, `StoryboardProtocol+Extensions`, `StoryboardProtocol+DefaultBundle`
|
||||
- **Remove**: `String+Extensions` image property
|
||||
- **Remove**: `UICollectionView+CellRegistration`
|
||||
- **Remove**: `UIStoryboard+InstantiateViewController`
|
||||
- **Remove**: `NetworkService` extension for loading images
|
||||
- **Remove**: `Observable` creation for `ImmutableMappable`
|
||||
- **Remove**: `UIView` and `UsedDefaults` extensions, `EstimatedViewHeightProtocol`, `StaticEstimatedViewHeightProtocol`, `StoryboardIdentifierProtocol`
|
||||
|
||||
### 0.5.18
|
||||
- **Fix**: EmptyCell first appearance setup fix
|
||||
|
||||
### 0.5.17
|
||||
- **Fix**: EmptyCell reusing appearance fix
|
||||
- **Fix**: SeparatorCell reusing separators fix
|
||||
|
||||
### 0.5.16
|
||||
|
||||
- **Change**: Rename `AppearanceProtocol` to `AppearanceConfigurable`
|
||||
- **Add**: `subscript(safe:)` subscript to `Array` extension for safe access to element by index
|
||||
|
||||
### 0.5.15
|
||||
|
||||
- **Add**: `AppearanceProtocol` which ensures that specific type can apply appearance to itself
|
||||
- **Add**: `with(appearance:)`, `set(appearance:)` methods to TableRow extension
|
||||
- **Add**: `Appearance` to `EmptyCell`
|
||||
- **Remove**: `SeparatorCellViewModel`.
|
||||
|
||||
### 0.5.13
|
||||
|
||||
- **Change**: Remove type erasure behavior from `AnyBaseTableRow`
|
||||
- **Change**: Rename `AnyBaseTableRow` class to `SeparatorRowBox`
|
||||
- **Change**: Move `anyRow` property from `EmptyCellRow` to `TableRow` extension and rename it to `separatorRowBox`.
|
||||
- **Change**: Move `configure(extreme: middle:)` method from `TableDirector` extension to `Array` extension and rename it to `configureSeparators(extreme: middle:)`
|
||||
|
||||
### 0.5.12
|
||||
|
||||
- **Fix**: Update type of `viewModel` in `ConfigurableController` to `ImplicitlyUwrappedOptional<ViewModelT>` instead of `ViewModelT`
|
||||
|
||||
### 0.5.11
|
||||
|
||||
- **[Breaking Change]**: rename initializer from `init(initialFrom:)` to `init(resetFrom:)` in `ResettableType`
|
||||
- **Add**: `SeparatorCell` with `SeparatorCellViewModel`
|
||||
- **Add**: `AnyBaseTableRow` for type-erasure
|
||||
- **Add**: `EmptyCellRow` for empty cell with static height
|
||||
|
||||
### 0.5.10
|
||||
|
||||
- **Fix**: `Public` modifier for `SpinnerView`
|
||||
|
||||
### 0.5.9
|
||||
|
||||
- **Fix**: One-two-many fixed for values more than 99
|
||||
|
||||
### 0.5.8
|
||||
|
||||
- **Fix**: Synchronization over `NSRecursiveLock` for request count tracker in NetworkService
|
||||
|
||||
### 0.5.7
|
||||
|
||||
- **Add**: String extension `localizedComponent(value:stringOne:stringTwo:stringMany:)`
|
||||
|
||||
### 0.5.6
|
||||
|
||||
- **Fix**: Clear tableview if placeholder is shown
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
github "malcommac/SwiftDate"
|
||||
github "Alamofire/Alamofire"
|
||||
github "RxSwiftCommunity/RxAlamofire" ~> 6.1
|
||||
github "TouchInstinct/TableKit"
|
||||
github "ReactiveX/RxSwift" ~> 6.2
|
||||
github "pronebird/UIScrollView-InfiniteScroll" "1.1.0"
|
||||
github "SnapKit/SnapKit" ~> 5.0
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
github "Alamofire/Alamofire" "5.4.3"
|
||||
github "ReactiveX/RxSwift" "6.2.0"
|
||||
github "RxSwiftCommunity/RxAlamofire" "v6.1.2"
|
||||
github "SnapKit/SnapKit" "5.0.1"
|
||||
github "TouchInstinct/TableKit" "2.10008.1"
|
||||
github "malcommac/SwiftDate" "6.3.1"
|
||||
github "pronebird/UIScrollView-InfiniteScroll" "1.1.0"
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
source "https://rubygems.org"
|
||||
|
||||
gem "cocoapods", "~> 1.11"
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
CFPropertyList (3.0.5)
|
||||
rexml
|
||||
activesupport (6.1.5)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
i18n (>= 1.6, < 2)
|
||||
minitest (>= 5.1)
|
||||
tzinfo (~> 2.0)
|
||||
zeitwerk (~> 2.3)
|
||||
addressable (2.8.0)
|
||||
public_suffix (>= 2.0.2, < 5.0)
|
||||
algoliasearch (1.27.5)
|
||||
httpclient (~> 2.8, >= 2.8.3)
|
||||
json (>= 1.5.1)
|
||||
atomos (0.1.3)
|
||||
claide (1.1.0)
|
||||
cocoapods (1.11.3)
|
||||
addressable (~> 2.8)
|
||||
claide (>= 1.0.2, < 2.0)
|
||||
cocoapods-core (= 1.11.3)
|
||||
cocoapods-deintegrate (>= 1.0.3, < 2.0)
|
||||
cocoapods-downloader (>= 1.4.0, < 2.0)
|
||||
cocoapods-plugins (>= 1.0.0, < 2.0)
|
||||
cocoapods-search (>= 1.0.0, < 2.0)
|
||||
cocoapods-trunk (>= 1.4.0, < 2.0)
|
||||
cocoapods-try (>= 1.1.0, < 2.0)
|
||||
colored2 (~> 3.1)
|
||||
escape (~> 0.0.4)
|
||||
fourflusher (>= 2.3.0, < 3.0)
|
||||
gh_inspector (~> 1.0)
|
||||
molinillo (~> 0.8.0)
|
||||
nap (~> 1.0)
|
||||
ruby-macho (>= 1.0, < 3.0)
|
||||
xcodeproj (>= 1.21.0, < 2.0)
|
||||
cocoapods-core (1.11.3)
|
||||
activesupport (>= 5.0, < 7)
|
||||
addressable (~> 2.8)
|
||||
algoliasearch (~> 1.0)
|
||||
concurrent-ruby (~> 1.1)
|
||||
fuzzy_match (~> 2.0.4)
|
||||
nap (~> 1.0)
|
||||
netrc (~> 0.11)
|
||||
public_suffix (~> 4.0)
|
||||
typhoeus (~> 1.0)
|
||||
cocoapods-deintegrate (1.0.5)
|
||||
cocoapods-downloader (1.6.2)
|
||||
cocoapods-plugins (1.0.0)
|
||||
nap
|
||||
cocoapods-search (1.0.1)
|
||||
cocoapods-trunk (1.6.0)
|
||||
nap (>= 0.8, < 2.0)
|
||||
netrc (~> 0.11)
|
||||
cocoapods-try (1.2.0)
|
||||
colored2 (3.1.2)
|
||||
concurrent-ruby (1.1.10)
|
||||
escape (0.0.4)
|
||||
ethon (0.15.0)
|
||||
ffi (>= 1.15.0)
|
||||
ffi (1.15.5)
|
||||
fourflusher (2.3.1)
|
||||
fuzzy_match (2.0.4)
|
||||
gh_inspector (1.1.3)
|
||||
httpclient (2.8.3)
|
||||
i18n (1.10.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
json (2.6.1)
|
||||
minitest (5.15.0)
|
||||
molinillo (0.8.0)
|
||||
nanaimo (0.3.0)
|
||||
nap (1.1.0)
|
||||
netrc (0.11.0)
|
||||
public_suffix (4.0.6)
|
||||
rexml (3.2.5)
|
||||
ruby-macho (2.5.1)
|
||||
typhoeus (1.4.0)
|
||||
ethon (>= 0.9.0)
|
||||
tzinfo (2.0.4)
|
||||
concurrent-ruby (~> 1.0)
|
||||
xcodeproj (1.21.0)
|
||||
CFPropertyList (>= 2.3.3, < 4.0)
|
||||
atomos (~> 0.1.3)
|
||||
claide (>= 1.0.2, < 2.0)
|
||||
colored2 (~> 3.1)
|
||||
nanaimo (~> 0.3.0)
|
||||
rexml (~> 3.2.4)
|
||||
zeitwerk (2.5.4)
|
||||
|
||||
PLATFORMS
|
||||
x86_64-darwin-20
|
||||
x86_64-darwin-21
|
||||
|
||||
DEPENDENCIES
|
||||
cocoapods (~> 1.11)
|
||||
|
||||
BUNDLED WITH
|
||||
2.3.26
|
||||
126
LeadKit.podspec
126
LeadKit.podspec
|
|
@ -1,18 +1,122 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = "LeadKit"
|
||||
s.version = "0.4.4"
|
||||
s.version = "1.35.0"
|
||||
s.summary = "iOS framework with a bunch of tools for rapid development"
|
||||
s.homepage = "https://github.com/TouchInstinct/LeadKit"
|
||||
s.homepage = "https://git.svc.touchin.ru/TouchInstinct/LeadKit"
|
||||
s.license = "Apache License, Version 2.0"
|
||||
s.author = "Touch Instinct"
|
||||
s.platform = :ios, "9.0"
|
||||
s.source = { :git => "https://github.com/TouchInstinct/LeadKit.git", :tag => s.version }
|
||||
s.source_files = "LeadKit/LeadKit/**/*.swift"
|
||||
s.source = { :git => "https://git.svc.touchin.ru/TouchInstinct/LeadKit.git", :tag => s.version }
|
||||
s.platform = :ios, '10.0'
|
||||
s.swift_versions = ['5.1']
|
||||
|
||||
s.subspec 'UIColorHex' do |ss|
|
||||
ss.ios.deployment_target = '10.0'
|
||||
ss.tvos.deployment_target = '10.0'
|
||||
ss.watchos.deployment_target = '3.0'
|
||||
|
||||
ss.source_files = "Sources/Extensions/UIColor/UIColor+Hex.swift"
|
||||
end
|
||||
|
||||
s.subspec 'Core' do |ss|
|
||||
ss.ios.deployment_target = '10.0'
|
||||
ss.tvos.deployment_target = '10.0'
|
||||
ss.watchos.deployment_target = '3.0'
|
||||
|
||||
ss.source_files = "Sources/**/*.swift"
|
||||
ss.watchos.exclude_files = [
|
||||
"Sources/Classes/Controllers/**/*",
|
||||
"Sources/Classes/Views/SeparatorRowBox/*",
|
||||
"Sources/Classes/Views/BaseRxTableViewCell/*",
|
||||
"Sources/Classes/Views/ContainerTableCell/*",
|
||||
"Sources/Classes/Views/SeparatorCell/*",
|
||||
"Sources/Classes/Views/EmptyCell/*",
|
||||
"Sources/Classes/Views/LabelTableViewCell/*",
|
||||
"Sources/Classes/DataLoading/PaginationDataLoading/PaginationWrapper.swift",
|
||||
"Sources/Classes/Views/XibView/*",
|
||||
"Sources/Classes/Views/SpinnerView/*",
|
||||
"Sources/Classes/Views/DefaultPlaceholders/*",
|
||||
"Sources/Classes/Views/CollectionViewWrapperView/*",
|
||||
"Sources/Classes/Views/TableViewWrapperView/*",
|
||||
"Sources/Classes/Views/BasePlaceholderView/*",
|
||||
"Sources/Classes/Views/CustomizableButton/*",
|
||||
"Sources/Classes/Search/*",
|
||||
"Sources/Enums/Search/*",
|
||||
"Sources/Extensions/CABasicAnimation/*",
|
||||
"Sources/Extensions/CGFloat/CGFloat+Pixels.swift",
|
||||
"Sources/Extensions/NetworkService/NetworkService+RxLoadImage.swift",
|
||||
"Sources/Extensions/DataLoading/GeneralDataLoading/GeneralDataLoadingController+DefaultImplementation.swift",
|
||||
"Sources/Extensions/DataLoading/PaginationDataLoading/*",
|
||||
"Sources/Extensions/Support/UINavigationItem+Support.swift",
|
||||
"Sources/Extensions/TableKit/**/*.swift",
|
||||
"Sources/Extensions/Array/Array+SeparatorRowBoxExtensions.swift",
|
||||
"Sources/Extensions/Array/Array+RowExtensions.swift",
|
||||
"Sources/Extensions/Drawing/UIImage/*",
|
||||
"Sources/Extensions/UIKit/**/*.swift",
|
||||
"Sources/Extensions/Views/ViewBackground/*",
|
||||
"Sources/Extensions/Views/SeparatorCell/*",
|
||||
"Sources/Extensions/Views/ConfigurableView/*",
|
||||
"Sources/Extensions/Views/PlaceholderConfigurable/*",
|
||||
"Sources/Protocols/UIKit/**/*.swift",
|
||||
"Sources/Protocols/LoadingIndicator.swift",
|
||||
"Sources/Protocols/DataLoading/PaginationDataLoading/PaginationWrappable.swift",
|
||||
"Sources/Protocols/DataLoading/GeneralDataLoading/GeneralDataLoadingController.swift",
|
||||
"Sources/Protocols/Views/SeparatorCell/*",
|
||||
"Sources/Protocols/Views/PlaceholderConfigurable/*",
|
||||
"Sources/Protocols/TableKit/**/*",
|
||||
"Sources/Protocols/Controllers/SearchResultsViewController.swift",
|
||||
"Sources/Structures/Views/AnyLoadingIndicator.swift",
|
||||
"Sources/Structures/DrawingOperations/CALayerDrawingOperation.swift",
|
||||
"Sources/Structures/DrawingOperations/RoundDrawingOperation.swift",
|
||||
"Sources/Structures/DrawingOperations/BorderDrawingOperation.swift",
|
||||
"Sources/Structures/DataLoading/PaginationDataLoading/*",
|
||||
"Sources/Extensions/UIInterfaceOrientation/*"
|
||||
]
|
||||
ss.tvos.exclude_files = [
|
||||
"Sources/Classes/Controllers/BaseConfigurableController.swift",
|
||||
"Sources/Classes/Controllers/BaseCollectionContentController.swift",
|
||||
"Sources/Classes/Views/TableViewWrapperView/TableViewWrapperView.swift",
|
||||
"Sources/Classes/Views/CollectionViewWrapperView/CollectionViewWrapperView.swift",
|
||||
"Sources/Classes/Controllers/BaseScrollContentController.swift",
|
||||
"Sources/Classes/Controllers/BaseCustomViewController.swift",
|
||||
"Sources/Classes/Controllers/BaseOrientationNavigationController.swift",
|
||||
"Sources/Extensions/UIKit/UIDevice/UIDevice+ScreenOrientation.swift",
|
||||
"Sources/Classes/Controllers/BaseTableContentController.swift",
|
||||
"Sources/Classes/Views/BaseRxTableViewCell/*",
|
||||
"Sources/Classes/Views/ContainerTableCell/*",
|
||||
"Sources/Classes/Views/SeparatorRowBox/*",
|
||||
"Sources/Classes/Views/SeparatorCell/*",
|
||||
"Sources/Classes/Views/EmptyCell/*",
|
||||
"Sources/Classes/Views/LabelTableViewCell/*",
|
||||
"Sources/Classes/Views/CustomizableButton/*",
|
||||
"Sources/Classes/DataLoading/PaginationDataLoading/PaginationWrapper.swift",
|
||||
"Sources/Classes/Search/*",
|
||||
"Sources/Structures/Drawing/CALayerDrawingOperation.swift",
|
||||
"Sources/Enums/Search/*",
|
||||
"Sources/Extensions/DataLoading/PaginationDataLoading/*",
|
||||
"Sources/Extensions/Support/UINavigationItem+Support.swift",
|
||||
"Sources/Extensions/TableKit/**/*.swift",
|
||||
"Sources/Extensions/Array/Array+SeparatorRowBoxExtensions.swift",
|
||||
"Sources/Extensions/Array/Array+RowExtensions.swift",
|
||||
"Sources/Extensions/Views/SeparatorCell/*",
|
||||
"Sources/Protocols/DataLoading/PaginationDataLoading/PaginationWrappable.swift",
|
||||
"Sources/Protocols/Views/SeparatorCell/*",
|
||||
"Sources/Protocols/TableKit/**/*",
|
||||
"Sources/Protocols/Controllers/SearchResultsViewController.swift",
|
||||
"Sources/Structures/DataLoading/PaginationDataLoading/*",
|
||||
"Sources/Extensions/UIInterfaceOrientation/*",
|
||||
"Sources/Classes/Controllers/BaseOrientationController.swift"
|
||||
]
|
||||
|
||||
ss.dependency "RxSwift", '~> 6.2'
|
||||
ss.dependency "RxCocoa", '~> 6.2'
|
||||
ss.dependency "RxAlamofire", '~> 6.1'
|
||||
ss.dependency "SwiftDate", '~> 6'
|
||||
|
||||
ss.ios.dependency "TableKit", '~> 2.11'
|
||||
ss.ios.dependency "SnapKit", '~> 5.0.1'
|
||||
ss.ios.dependency "UIScrollView-InfiniteScroll", '~> 1.1.0'
|
||||
end
|
||||
|
||||
s.default_subspec = 'Core'
|
||||
|
||||
s.dependency "CocoaLumberjack/Swift", '~> 3.0.0'
|
||||
s.dependency "RxSwift", '3.2.0'
|
||||
s.dependency "RxCocoa", '3.2.0'
|
||||
s.dependency "RxAlamofire", '3.0.0'
|
||||
s.dependency "ObjectMapper", '~> 2.1'
|
||||
s.dependency "Toast-Swift", '~> 2.0.0'
|
||||
end
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0800"
|
||||
LastUpgradeVersion = "1230"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
|
@ -14,9 +14,9 @@
|
|||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "78CFEE291C5C456B00F50370"
|
||||
BlueprintIdentifier = "67186B271EB248F100CFAFFB"
|
||||
BuildableName = "LeadKit.framework"
|
||||
BlueprintName = "LeadKit"
|
||||
BlueprintName = "LeadKit iOS"
|
||||
ReferencedContainer = "container:LeadKit.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
|
|
@ -27,29 +27,27 @@
|
|||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "67186B271EB248F100CFAFFB"
|
||||
BuildableName = "LeadKit.framework"
|
||||
BlueprintName = "LeadKit iOS"
|
||||
ReferencedContainer = "container:LeadKit.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "78CFEE331C5C456B00F50370"
|
||||
BuildableName = "LeadKitTests.xctest"
|
||||
BlueprintName = "LeadKitTests"
|
||||
BlueprintIdentifier = "67186B2F1EB248F100CFAFFB"
|
||||
BuildableName = "LeadKit iOSTests.xctest"
|
||||
BlueprintName = "LeadKit iOSTests"
|
||||
ReferencedContainer = "container:LeadKit.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "78CFEE291C5C456B00F50370"
|
||||
BuildableName = "LeadKit.framework"
|
||||
BlueprintName = "LeadKit"
|
||||
ReferencedContainer = "container:LeadKit.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
|
|
@ -64,14 +62,12 @@
|
|||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "78CFEE291C5C456B00F50370"
|
||||
BlueprintIdentifier = "67186B271EB248F100CFAFFB"
|
||||
BuildableName = "LeadKit.framework"
|
||||
BlueprintName = "LeadKit"
|
||||
BlueprintName = "LeadKit iOS"
|
||||
ReferencedContainer = "container:LeadKit.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
|
|
@ -82,9 +78,9 @@
|
|||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "78CFEE291C5C456B00F50370"
|
||||
BlueprintIdentifier = "67186B271EB248F100CFAFFB"
|
||||
BuildableName = "LeadKit.framework"
|
||||
BlueprintName = "LeadKit"
|
||||
BlueprintName = "LeadKit iOS"
|
||||
ReferencedContainer = "container:LeadKit.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1230"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "6782BB9F1EB31D590086E0B8"
|
||||
BuildableName = "LeadKit.framework"
|
||||
BlueprintName = "LeadKit tvOS"
|
||||
ReferencedContainer = "container:LeadKit.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "6782BB9F1EB31D590086E0B8"
|
||||
BuildableName = "LeadKit.framework"
|
||||
BlueprintName = "LeadKit tvOS"
|
||||
ReferencedContainer = "container:LeadKit.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "6782BBA71EB31D5A0086E0B8"
|
||||
BuildableName = "LeadKit tvOSTests.xctest"
|
||||
BlueprintName = "LeadKit tvOSTests"
|
||||
ReferencedContainer = "container:LeadKit.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "6782BB9F1EB31D590086E0B8"
|
||||
BuildableName = "LeadKit.framework"
|
||||
BlueprintName = "LeadKit tvOS"
|
||||
ReferencedContainer = "container:LeadKit.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "6782BB9F1EB31D590086E0B8"
|
||||
BuildableName = "LeadKit.framework"
|
||||
BlueprintName = "LeadKit tvOS"
|
||||
ReferencedContainer = "container:LeadKit.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
disabled_rules:
|
||||
- variable_name
|
||||
excluded:
|
||||
- Carthage
|
||||
- Pods
|
||||
line_length: 128
|
||||
type_body_length:
|
||||
- 500 # warning
|
||||
- 700 # error
|
||||
file_length:
|
||||
warning: 500
|
||||
error: 1200
|
||||
warning_threshold: 1
|
||||
|
||||
custom_rules:
|
||||
uiwebview_disabled:
|
||||
included: ".*.swift"
|
||||
name: "UIWebView Usage Disabled"
|
||||
regex: "(UIWebView)"
|
||||
message: "Do not use UIWebView. Use WKWebView Instead. https://developer.apple.com/reference/uikit/uiwebview"
|
||||
severity: error
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
exclude:
|
||||
- 'Pods'
|
||||
- 'Carthage'
|
||||
- 'RxAlamofire'
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,59 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2017 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
open class App {
|
||||
|
||||
fileprivate static let stringVendorIdentifierKey = "stringIdentifierForVendor"
|
||||
/// The value of CFBundleName
|
||||
open static let bundleName = Bundle.main.infoDictionary?["CFBundleName"] as? String ?? ""
|
||||
/// The value of CFBundleShortVersionString
|
||||
open static let shortVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? ""
|
||||
/// The value of CFBundleVersion
|
||||
open static let bundleVersion = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? ""
|
||||
|
||||
/**
|
||||
Return app's version
|
||||
- returns: shortBundleVersion.bundleVersion
|
||||
*/
|
||||
open static var version: String {
|
||||
return App.shortVersion + "." + App.bundleVersion
|
||||
}
|
||||
|
||||
/**
|
||||
Return device identifier
|
||||
- returns: UUIDString
|
||||
*/
|
||||
open static var deviceUniqueIdentifier: String {
|
||||
if let vendorIdentifier = UserDefaults.standard.string(forKey: App.stringVendorIdentifierKey) {
|
||||
return vendorIdentifier
|
||||
}
|
||||
|
||||
let vendorIdentifier = UUID().uuidString
|
||||
UserDefaults.standard.set(vendorIdentifier, forKey: App.stringVendorIdentifierKey)
|
||||
UserDefaults.standard.synchronize()
|
||||
|
||||
return vendorIdentifier
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2017 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CocoaLumberjack
|
||||
|
||||
open class Log {
|
||||
|
||||
public init() {
|
||||
DDLog.add(DDFileLogger())
|
||||
|
||||
DDLog.add(DDASLLogger.sharedInstance)
|
||||
DDLog.add(DDTTYLogger.sharedInstance)
|
||||
|
||||
let logFormatter = LogFormatter()
|
||||
|
||||
DDASLLogger.sharedInstance.logFormatter = logFormatter
|
||||
DDTTYLogger.sharedInstance.logFormatter = logFormatter
|
||||
|
||||
let assertionHandler = NSAssertionHandler()
|
||||
|
||||
Thread.current.threadDictionary.setValue(assertionHandler, forKey: NSAssertionHandlerKey)
|
||||
}
|
||||
|
||||
/**
|
||||
Add start message for your application
|
||||
|
||||
- returns: Return value looks like "AppName 1.0.1 session started on version 9.2 (build 13c75)"
|
||||
*/
|
||||
open static var startMessage: String {
|
||||
let startMessage = App.bundleName + " " + App.shortVersion + "."
|
||||
+ App.bundleVersion + " session started on "
|
||||
+ ProcessInfo.processInfo.operatingSystemVersionString.lowercased()
|
||||
return startMessage
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2017 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CocoaLumberjack
|
||||
|
||||
class LogFormatter: NSObject, DDLogFormatter {
|
||||
fileprivate let dateFormatter: DateFormatter
|
||||
|
||||
override init() {
|
||||
dateFormatter = DateFormatter()
|
||||
dateFormatter.formatterBehavior = .behavior10_4
|
||||
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss:SSS"
|
||||
|
||||
super.init()
|
||||
}
|
||||
|
||||
func format(message logMessage: DDLogMessage) -> String? {
|
||||
let level: String
|
||||
|
||||
switch logMessage.flag {
|
||||
case DDLogFlag.error:
|
||||
level = "ERR"
|
||||
case DDLogFlag.warning:
|
||||
level = "WRN"
|
||||
case DDLogFlag.info:
|
||||
level = "INF"
|
||||
case DDLogFlag.debug:
|
||||
level = "DBG"
|
||||
default:
|
||||
level = "VRB"
|
||||
}
|
||||
|
||||
let dateAndTime = dateFormatter.string(from: logMessage.timestamp)
|
||||
return "\(level) \(dateAndTime) [\(logMessage.fileName):\(logMessage.line)]: \(logMessage.message)"
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,119 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2017 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import RxSwift
|
||||
import RxCocoa
|
||||
import Alamofire
|
||||
import ObjectMapper
|
||||
import RxAlamofire
|
||||
|
||||
/// Base network service implementation build on top of LeadKit extensions for Alamofire.
|
||||
/// Has an ability to automatically show / hide network activity indicator
|
||||
/// and shows errors in DEBUG mode
|
||||
open class NetworkService {
|
||||
|
||||
private let disposeBag = DisposeBag()
|
||||
private let requestCount = Variable<Int>(0)
|
||||
|
||||
public let sessionManager: Alamofire.SessionManager
|
||||
|
||||
/// Let netwrok service automatically show / hide activity indicator
|
||||
public func bindActivityIndicator() {
|
||||
return requestCount.asDriver()
|
||||
.map { $0 != 0 }
|
||||
.drive(UIApplication.shared.rx.isNetworkActivityIndicatorVisible)
|
||||
.addDisposableTo(disposeBag)
|
||||
}
|
||||
|
||||
/// Creates new instance of NetworkService with given Alamofire session manager
|
||||
///
|
||||
/// - Parameter sessionManager: Alamofire.SessionManager to use for requests
|
||||
public init(sessionManager: Alamofire.SessionManager) {
|
||||
self.sessionManager = sessionManager
|
||||
}
|
||||
|
||||
/// Perform reactive request to get mapped ObservableMappable model and http response
|
||||
///
|
||||
/// - Parameter parameters: api parameters to pass Alamofire
|
||||
/// - Returns: Observable of tuple containing (HTTPURLResponse, ObservableMappable)
|
||||
public func rxRequest<T: ObservableMappable>(with parameters: ApiRequestParameters)
|
||||
-> Observable<(response: HTTPURLResponse, model: T)> where T.ModelType == T {
|
||||
|
||||
return sessionManager.rx.responseObservableModel(requestParameters: parameters)
|
||||
.counterTracking(for: self)
|
||||
.showErrorsInToastInDebugMode()
|
||||
}
|
||||
|
||||
/// Perform reactive request to get mapped ImmutableMappable model and http response
|
||||
///
|
||||
/// - Parameter parameters: api parameters to pass Alamofire
|
||||
/// - Returns: Observable of tuple containing (HTTPURLResponse, ImmutableMappable)
|
||||
public func rxRequest<T: ImmutableMappable>(with parameters: ApiRequestParameters)
|
||||
-> Observable<(response: HTTPURLResponse, model: T)> {
|
||||
|
||||
return sessionManager.rx.responseModel(requestParameters: parameters)
|
||||
.counterTracking(for: self)
|
||||
.showErrorsInToastInDebugMode()
|
||||
}
|
||||
|
||||
/// Perform reactive request to get UIImage and http response
|
||||
///
|
||||
/// - Parameter url: An object adopting `URLConvertible`
|
||||
/// - Returns: Observable of tuple containing (HTTPURLResponse, UIImage?)
|
||||
public func rxLoadImage(url: String) -> Observable<(HTTPURLResponse, UIImage?)> {
|
||||
let request = RxAlamofire.requestData(.get, url, headers: [:])
|
||||
|
||||
return request
|
||||
.observeOn(ConcurrentDispatchQueueScheduler(qos: .background))
|
||||
.map { (response, data) -> (HTTPURLResponse, UIImage?) in
|
||||
(response, UIImage(data: data))
|
||||
}
|
||||
.counterTracking(for: self)
|
||||
.showErrorsInToastInDebugMode()
|
||||
}
|
||||
|
||||
fileprivate func increaseRequestCounter() {
|
||||
requestCount.value += 1
|
||||
}
|
||||
|
||||
fileprivate func decreaseRequestCounter() {
|
||||
requestCount.value -= 1
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public extension Observable {
|
||||
|
||||
/// Increase and descrease NetworkService request counter on subscribe and dispose
|
||||
/// (used to show / hide activity indicator)
|
||||
///
|
||||
/// - Parameter networkService: NetworkService to operate on it
|
||||
/// - Returns: The source sequence with the side-effecting behavior applied.
|
||||
func counterTracking(for networkService: NetworkService) -> Observable<Observable.E> {
|
||||
return `do`(onSubscribe: {
|
||||
networkService.increaseRequestCounter()
|
||||
}, onDispose: {
|
||||
networkService.decreaseRequestCounter()
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,94 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2017 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import Alamofire
|
||||
import RxSwift
|
||||
import RxAlamofire
|
||||
import ObjectMapper
|
||||
|
||||
public extension Reactive where Base: Alamofire.SessionManager {
|
||||
|
||||
/// Method which executes request with given api parameters
|
||||
///
|
||||
/// - Parameter requestParameters: api parameters to pass Alamofire
|
||||
/// - Returns: Observable with request
|
||||
func apiRequest(requestParameters: ApiRequestParameters) -> Observable<DataRequest> {
|
||||
return RxAlamofire.request(requestParameters.method,
|
||||
requestParameters.url,
|
||||
parameters: requestParameters.parameters,
|
||||
encoding: requestParameters.encoding,
|
||||
headers: requestParameters.headers)
|
||||
}
|
||||
|
||||
/// Method which executes request and serializes response into target object
|
||||
///
|
||||
/// - Parameter requestParameters: api parameters to pass Alamofire
|
||||
/// - Parameter mappingQueue: The dispatch queue to use for mapping
|
||||
/// - Returns: Observable with HTTP URL Response and target object
|
||||
func responseModel<T: ImmutableMappable>(requestParameters: ApiRequestParameters,
|
||||
mappingQueue: DispatchQueue = DispatchQueue.global())
|
||||
-> Observable<(response: HTTPURLResponse, model: T)> {
|
||||
|
||||
return apiRequest(requestParameters: requestParameters)
|
||||
.flatMap { $0.validate().rx.apiResponse(mappingQueue: mappingQueue) }
|
||||
}
|
||||
|
||||
/// Method which executes request and serializes response into array of target objects
|
||||
///
|
||||
/// - Parameter requestParameters: api parameters to pass Alamofire
|
||||
/// - Parameter mappingQueue: The dispatch queue to use for mapping
|
||||
/// - Returns: Observable with HTTP URL Response and array of target objects
|
||||
func responseModel<T: ImmutableMappable>(requestParameters: ApiRequestParameters,
|
||||
mappingQueue: DispatchQueue = DispatchQueue.global())
|
||||
-> Observable<(response: HTTPURLResponse, models: [T])> {
|
||||
|
||||
return apiRequest(requestParameters: requestParameters)
|
||||
.flatMap { $0.validate().rx.apiResponse(mappingQueue: mappingQueue) }
|
||||
}
|
||||
|
||||
/// Method which executes request and serializes response into target object
|
||||
///
|
||||
/// - Parameter requestParameters: api parameters to pass Alamofire
|
||||
/// - Parameter mappingQueue: The dispatch queue to use for mapping
|
||||
/// - Returns: Observable with HTTP URL Response and target object
|
||||
func responseObservableModel<T: ObservableMappable>(requestParameters: ApiRequestParameters,
|
||||
mappingQueue: DispatchQueue = DispatchQueue.global())
|
||||
-> Observable<(response: HTTPURLResponse, model: T)> where T.ModelType == T {
|
||||
|
||||
return apiRequest(requestParameters: requestParameters)
|
||||
.flatMap { $0.validate().rx.apiResponse(mappingQueue: mappingQueue) }
|
||||
}
|
||||
|
||||
/// Method which executes request and serializes response into array of target objects
|
||||
///
|
||||
/// - Parameter requestParameters: api parameters to pass Alamofire
|
||||
/// - Parameter mappingQueue: The dispatch queue to use for mapping
|
||||
/// - Returns: Observable with HTTP URL Response and array of target objects
|
||||
func responseObservableModel<T: ObservableMappable>(requestParameters: ApiRequestParameters,
|
||||
mappingQueue: DispatchQueue = DispatchQueue.global())
|
||||
-> Observable<(response: HTTPURLResponse, models: [T])> where T.ModelType == T {
|
||||
|
||||
return apiRequest(requestParameters: requestParameters)
|
||||
.flatMap { $0.validate().rx.apiResponse(mappingQueue: mappingQueue) }
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,100 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2017 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import Alamofire
|
||||
import RxSwift
|
||||
import ObjectMapper
|
||||
import RxAlamofire
|
||||
|
||||
public extension Reactive where Base: DataRequest {
|
||||
|
||||
/// Method which serializes response into target object
|
||||
///
|
||||
/// - Parameter mappingQueue: The dispatch queue to use for mapping
|
||||
/// - Returns: Observable with HTTP URL Response and target object
|
||||
func apiResponse<T: ImmutableMappable>(mappingQueue: DispatchQueue = DispatchQueue.global())
|
||||
-> Observable<(response: HTTPURLResponse, model: T)> {
|
||||
|
||||
return responseJSONOnQueue(mappingQueue)
|
||||
.map { resp, value in
|
||||
let json = try cast(value) as [String: Any]
|
||||
|
||||
return (resp, try T(JSON: json))
|
||||
}
|
||||
}
|
||||
|
||||
/// Method which serializes response into array of target objects
|
||||
///
|
||||
/// - Parameter mappingQueue: The dispatch queue to use for mapping
|
||||
/// - Returns: Observable with HTTP URL Response and array of target objects
|
||||
func apiResponse<T: ImmutableMappable>(mappingQueue: DispatchQueue = DispatchQueue.global())
|
||||
-> Observable<(response: HTTPURLResponse, models: [T])> {
|
||||
|
||||
return responseJSONOnQueue(mappingQueue)
|
||||
.map { resp, value in
|
||||
let jsonArray = try cast(value) as [[String: Any]]
|
||||
|
||||
return (resp, try Mapper<T>().mapArray(JSONArray: jsonArray))
|
||||
}
|
||||
}
|
||||
|
||||
/// Method which serializes response into target object
|
||||
///
|
||||
/// - Parameter mappingQueue: The dispatch queue to use for mapping
|
||||
/// - Returns: Observable with HTTP URL Response and target object
|
||||
func apiResponse<T: ObservableMappable>(mappingQueue: DispatchQueue = DispatchQueue.global())
|
||||
-> Observable<(response: HTTPURLResponse, model: T)> where T.ModelType == T {
|
||||
|
||||
return responseJSONOnQueue(mappingQueue)
|
||||
.flatMap { resp, value -> Observable<(response: HTTPURLResponse, model: T)> in
|
||||
let json = try cast(value) as [String: Any]
|
||||
|
||||
return T.createFrom(map: Map(mappingType: .fromJSON, JSON: json))
|
||||
.map { (resp, $0) }
|
||||
}
|
||||
}
|
||||
|
||||
/// Method which serializes response into array of target objects
|
||||
///
|
||||
/// - Parameter mappingQueue: The dispatch queue to use for mapping
|
||||
/// - Returns: Observable with HTTP URL Response and array of target objects
|
||||
func apiResponse<T: ObservableMappable>(mappingQueue: DispatchQueue = DispatchQueue.global())
|
||||
-> Observable<(response: HTTPURLResponse, models: [T])> where T.ModelType == T {
|
||||
|
||||
return responseJSONOnQueue(mappingQueue)
|
||||
.flatMap { resp, value -> Observable<(response: HTTPURLResponse, models: [T])> in
|
||||
let jsonArray = try cast(value) as [[String: Any]]
|
||||
|
||||
let createFromList = jsonArray.map {
|
||||
T.createFrom(map: Map(mappingType: .fromJSON, JSON: $0))
|
||||
}
|
||||
|
||||
return Observable.zip(createFromList) { $0 }
|
||||
.map { (resp, $0) }
|
||||
}
|
||||
}
|
||||
|
||||
internal func responseJSONOnQueue(_ queue: DispatchQueue) -> Observable<(HTTPURLResponse, Any)> {
|
||||
return responseResult(queue: queue, responseSerializer: DataRequest.jsonResponseSerializer(options: .allowFragments))
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2017 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import CoreGraphics
|
||||
|
||||
public extension CGContext {
|
||||
|
||||
/**
|
||||
method which creates an instance of CGContext with parameters taken from a given image
|
||||
|
||||
- parameter forCGImage: CGImage instance from which the parameters will be taken
|
||||
- parameter fallbackColorSpace: fallback color space if image doesn't have it
|
||||
*/
|
||||
public static func create(forCGImage cgImage: CGImage,
|
||||
fallbackColorSpace: CGColorSpace = CGColorSpaceCreateDeviceRGB()) -> CGContext? {
|
||||
|
||||
return create(width: cgImage.width,
|
||||
height: cgImage.height,
|
||||
bitmapInfo: cgImage.bitmapInfo,
|
||||
colorSpace: cgImage.colorSpace ?? fallbackColorSpace,
|
||||
bitsPerComponent: cgImage.bitsPerComponent)
|
||||
}
|
||||
|
||||
/**
|
||||
method which creates an instance of CGContext
|
||||
|
||||
- parameter width: The width, in pixels, of the required bitmap.
|
||||
- parameter height: The height, in pixels, of the required bitmap.
|
||||
- parameter bitmapInfo: Constants that specify whether the bitmap should contain an alpha channel,
|
||||
the alpha channel’s relative location in a pixel,
|
||||
and information about whether the pixel components are floating-point or integer values.
|
||||
- parameter colorSpace: The color space to use for the bitmap context.
|
||||
- parameter bitsPerComponent: The number of bits to use for each component of a pixel in memory.
|
||||
*/
|
||||
public static func create(width: Int,
|
||||
height: Int,
|
||||
bitmapInfo: CGBitmapInfo = alphaBitmapInfo,
|
||||
colorSpace: CGColorSpace = CGColorSpaceCreateDeviceRGB(),
|
||||
bitsPerComponent: Int = 8) -> CGContext? {
|
||||
|
||||
return CGContext(data: nil,
|
||||
width: width,
|
||||
height: height,
|
||||
bitsPerComponent: bitsPerComponent,
|
||||
bytesPerRow: 0,
|
||||
space: colorSpace,
|
||||
bitmapInfo: bitmapInfo.rawValue)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,78 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2017 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import CoreGraphics
|
||||
|
||||
public extension CGImage {
|
||||
|
||||
/**
|
||||
method which creates new CGImage instance filled by given color
|
||||
|
||||
- parameter color: color to fill
|
||||
- parameter width: width of new image
|
||||
- parameter height: height of new image
|
||||
- parameter opaque: a flag indicating whether the bitmap is opaque (default: False)
|
||||
|
||||
- returns: new instanse of UIImage with given size and color
|
||||
*/
|
||||
public static func create(color: CGColor,
|
||||
width: Int,
|
||||
height: Int,
|
||||
opaque: Bool = false) -> CGImage? {
|
||||
|
||||
let context = CGContext.create(width: width,
|
||||
height: height,
|
||||
bitmapInfo: opaque ? opaqueBitmapInfo : alphaBitmapInfo)
|
||||
|
||||
guard let ctx = context else {
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx.setFillColor(color)
|
||||
ctx.fill(CGRect(origin: CGPoint.zero, size: CGSize(width: width, height: height)))
|
||||
|
||||
return ctx.makeImage()
|
||||
}
|
||||
|
||||
/**
|
||||
creates an image from a UIView.
|
||||
|
||||
- parameter fromView: The source view.
|
||||
|
||||
- returns A new image
|
||||
*/
|
||||
public static func create(fromView view: UIView) -> CGImage? {
|
||||
let size = view.bounds.size
|
||||
|
||||
let ctxWidth = Int(ceil(size.width))
|
||||
let ctxHeight = Int(ceil(size.height))
|
||||
|
||||
guard let ctx = CGContext.create(width: ctxWidth, height: ctxHeight) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
view.layer.render(in: ctx)
|
||||
|
||||
return ctx.makeImage()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2017 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import CoreGraphics
|
||||
|
||||
public extension CGImage {
|
||||
|
||||
/**
|
||||
method which render current template CGImage into new image using given color
|
||||
|
||||
- parameter withColor: color which used to fill template image
|
||||
|
||||
- returns: new CGImage rendered with given color or nil if something goes wrong
|
||||
*/
|
||||
public func renderTemplate(withColor color: CGColor) -> CGImage? {
|
||||
guard let ctx = CGContext.create(forCGImage: self) ?? CGContext.create(width: width, height: height) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let imageRect = bounds
|
||||
|
||||
ctx.setFillColor(color)
|
||||
|
||||
ctx.translateBy(x: 0, y: CGFloat(height))
|
||||
ctx.scaleBy(x: 1.0, y: -1.0)
|
||||
ctx.clip(to: imageRect, mask: self)
|
||||
ctx.fill(imageRect)
|
||||
|
||||
ctx.setBlendMode(.multiply)
|
||||
|
||||
return ctx.makeImage()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,203 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2017 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import CoreGraphics
|
||||
|
||||
public extension CGImage {
|
||||
|
||||
/**
|
||||
creates a new image with rounded corners.
|
||||
|
||||
- parameter withRadius: The corner radius.
|
||||
|
||||
- returns: A new image
|
||||
*/
|
||||
public func round(withRadius radius: CGFloat) -> CGImage? {
|
||||
guard let ctx = CGContext.create(forCGImage: self) ?? CGContext.create(width: width, height: height) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx.addPath(UIBezierPath(roundedRect: bounds, cornerRadius: radius).cgPath)
|
||||
ctx.clip()
|
||||
ctx.draw(self, in: bounds)
|
||||
|
||||
return ctx.makeImage()
|
||||
}
|
||||
|
||||
/**
|
||||
creates a new image with a border.
|
||||
|
||||
- parameter width: The size of the border.
|
||||
- parameter color: The color of the border.
|
||||
- parameter radius: The corner radius.
|
||||
- parameter extendSize: Extend result image size and don't overlap source image by border.
|
||||
|
||||
- returns: A new image
|
||||
*/
|
||||
public func applyBorder(width border: CGFloat,
|
||||
color: CGColor,
|
||||
radius: CGFloat = 0,
|
||||
extendSize: Bool = false) -> CGImage? {
|
||||
|
||||
let offset = extendSize ? border : 0
|
||||
|
||||
let newWidth = CGFloat(width) + offset * 2
|
||||
let newHeight = CGFloat(height) + offset * 2
|
||||
|
||||
let ctxWidth = Int(ceil(newWidth))
|
||||
let ctxHeight = Int(ceil(newHeight))
|
||||
|
||||
let ctxRect: CGRect = CGRect(origin: CGPoint.zero, size: CGSize(width: newWidth, height: newHeight))
|
||||
|
||||
let context = CGContext.create(width: ctxWidth,
|
||||
height: ctxHeight,
|
||||
bitmapInfo: bitmapInfo,
|
||||
colorSpace: colorSpace ?? CGColorSpaceCreateDeviceRGB(),
|
||||
bitsPerComponent: bitsPerComponent)
|
||||
|
||||
guard let ctx = context ?? CGContext.create(width: width, height: height) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx.draw(self, in: CGRect(x: offset, y: offset, width: CGFloat(width), height: CGFloat(height)))
|
||||
|
||||
ctx.setStrokeColor(color)
|
||||
|
||||
let widthDiff = CGFloat(ctxWidth) - newWidth // difference between context width and real width
|
||||
let heightDiff = CGFloat(ctxWidth) - newWidth // difference between context height and real height
|
||||
|
||||
let inset = ctxRect.insetBy(dx: border / 2 + widthDiff, dy: border / 2 + heightDiff)
|
||||
|
||||
if radius != 0 {
|
||||
ctx.setLineWidth(border)
|
||||
ctx.addPath(UIBezierPath(roundedRect: inset, cornerRadius: radius).cgPath)
|
||||
ctx.strokePath()
|
||||
} else {
|
||||
ctx.stroke(inset, width: border)
|
||||
}
|
||||
|
||||
return ctx.makeImage()
|
||||
}
|
||||
|
||||
/**
|
||||
creates a resized copy of an image.
|
||||
|
||||
- parameter newSize: the new size of the image.
|
||||
- parameter contentMode: the way to handle the content in the new size.
|
||||
|
||||
- returns: a new image
|
||||
*/
|
||||
public func resize(newSize: CGSize, contentMode: ImageContentMode = .scaleToFill) -> CGImage? {
|
||||
let ctxWidth = Int(ceil(newSize.width))
|
||||
let ctxHeight = Int(ceil(newSize.height))
|
||||
|
||||
let context = CGContext.create(width: ctxWidth,
|
||||
height: ctxHeight,
|
||||
bitmapInfo: bitmapInfo,
|
||||
colorSpace: colorSpace ?? CGColorSpaceCreateDeviceRGB(),
|
||||
bitsPerComponent: bitsPerComponent)
|
||||
|
||||
guard let ctx = context ?? CGContext.create(width: ctxWidth, height: ctxHeight) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let horizontalRatio = newSize.width / CGFloat(width)
|
||||
let verticalRatio = newSize.height / CGFloat(height)
|
||||
|
||||
let ratio: CGFloat
|
||||
|
||||
switch contentMode {
|
||||
case .scaleToFill:
|
||||
ratio = 1
|
||||
case .scaleAspectFill:
|
||||
ratio = max(horizontalRatio, verticalRatio)
|
||||
case .scaleAspectFit:
|
||||
ratio = min(horizontalRatio, verticalRatio)
|
||||
}
|
||||
|
||||
let newImageWidth = contentMode == .scaleToFill ? newSize.width : CGFloat(width) * ratio
|
||||
let newImageHeight = contentMode == .scaleToFill ? newSize.height : CGFloat(height) * ratio
|
||||
|
||||
let originX: CGFloat
|
||||
let originY: CGFloat
|
||||
|
||||
if newImageWidth > newSize.width {
|
||||
originX = (newSize.width - newImageWidth) / 2
|
||||
} else if newImageWidth < newSize.width {
|
||||
originX = newSize.width / 2 - newImageWidth / 2
|
||||
} else {
|
||||
originX = 0
|
||||
}
|
||||
|
||||
if newImageHeight > newSize.height {
|
||||
originY = (newSize.height - newImageHeight) / 2
|
||||
} else if newImageHeight < newSize.height {
|
||||
originY = newSize.height / 2 - newImageHeight / 2
|
||||
} else {
|
||||
originY = 0
|
||||
}
|
||||
|
||||
let rect = CGRect(origin: CGPoint(x: originX, y: originY),
|
||||
size: CGSize(width: newImageWidth, height: newImageHeight))
|
||||
|
||||
ctx.interpolationQuality = .high
|
||||
ctx.draw(self, in: rect)
|
||||
|
||||
return ctx.makeImage()
|
||||
}
|
||||
|
||||
/**
|
||||
returns a copy of the image with border of the given size added around its edges.
|
||||
|
||||
- parameter padding: The padding amount.
|
||||
|
||||
- returns: A new image.
|
||||
*/
|
||||
public func applyPadding(_ padding: CGFloat) -> CGImage? {
|
||||
let ctxWidth = Int(ceil(CGFloat(width) + padding * 2))
|
||||
let ctxHeight = Int(ceil(CGFloat(height) + padding * 2))
|
||||
|
||||
let context = CGContext.create(width: ctxWidth,
|
||||
height: ctxHeight,
|
||||
bitmapInfo: bitmapInfo,
|
||||
colorSpace: colorSpace ?? CGColorSpaceCreateDeviceRGB(),
|
||||
bitsPerComponent: bitsPerComponent)
|
||||
|
||||
guard let ctx = context ?? CGContext.create(width: ctxWidth, height: ctxHeight) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Draw the image in the center of the context, leaving a gap around the edges
|
||||
let imageLocation = CGRect(x: padding,
|
||||
y: padding,
|
||||
width: CGFloat(width),
|
||||
height: CGFloat(height))
|
||||
|
||||
ctx.addRect(imageLocation)
|
||||
ctx.clip()
|
||||
|
||||
ctx.draw(self, in: imageLocation)
|
||||
|
||||
return ctx.makeImage()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2017 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
/**
|
||||
* Struct for holding result of string size calculation
|
||||
*/
|
||||
public struct StringSizeCalculationResult {
|
||||
|
||||
public let size: CGSize
|
||||
public let fontLineHeight: CGFloat?
|
||||
|
||||
}
|
||||
|
||||
public extension StringSizeCalculationResult {
|
||||
|
||||
public var height: CGFloat { return size.height }
|
||||
|
||||
public var width: CGFloat { return size.width }
|
||||
|
||||
public var numberOfLines: UInt? {
|
||||
if let lineHeight = fontLineHeight {
|
||||
let lineHeightRounded = Double(lineHeight).roundValue(withPersicion: 2)
|
||||
|
||||
let heightRounded = Double(height).roundValue(withPersicion: 2)
|
||||
|
||||
let numberOfLines = ceil(heightRounded / lineHeightRounded)
|
||||
|
||||
return UInt(numberOfLines)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public extension String {
|
||||
|
||||
/**
|
||||
method which calculates string size based on given character attributes and (optional) max width and height
|
||||
|
||||
- parameter attributes: dictionary with string character attributes
|
||||
- parameter maxWidth: maximum width of text
|
||||
- parameter maxHeight: maximum height of text
|
||||
|
||||
- returns: string size calculation result
|
||||
*/
|
||||
public func size(withAttributes attributes: [String: AnyObject]?,
|
||||
maxWidth: CGFloat = CGFloat.greatestFiniteMagnitude,
|
||||
maxHeight: CGFloat = CGFloat.greatestFiniteMagnitude) -> StringSizeCalculationResult {
|
||||
|
||||
let size = self.boundingRect(with: CGSize(width: maxWidth, height: maxHeight),
|
||||
options: [.usesLineFragmentOrigin, .usesFontLeading],
|
||||
attributes: attributes,
|
||||
context: nil).size
|
||||
|
||||
let fontLineHeight = (attributes?[NSFontAttributeName] as? UIFont)?.lineHeight
|
||||
|
||||
return StringSizeCalculationResult(size: size, fontLineHeight: fontLineHeight)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2017 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
public extension UICollectionView {
|
||||
/**
|
||||
method which register UICollectionViewCell subclass for reusing in UICollectionView with reuse identifier
|
||||
provided by ReuseIdentifierProtocol protocol implementation and nib name
|
||||
provided by StaticNibNameProtocol protocol implementation
|
||||
|
||||
- parameter cellClass: UICollectionViewCell subclass which implements ReuseIdentifierProtocol and StaticNibNameProtocol
|
||||
|
||||
- see: ReuseIdentifierProtocol, StaticNibNameProtocol
|
||||
*/
|
||||
|
||||
public func registerNib<T>(forCellClass cellClass: T.Type)
|
||||
where T: ReuseIdentifierProtocol, T: UICollectionViewCell, T: StaticNibNameProtocol {
|
||||
|
||||
register(UINib(nibName: T.nibName), forCellWithReuseIdentifier: T.reuseIdentifier)
|
||||
}
|
||||
|
||||
/**
|
||||
method which register UICollectionViewCell subclass for reusing in UICollectionView with reuse identifier
|
||||
provided by ReuseIdentifierProtocol protocol implementation and nib name
|
||||
provided by NibNameProtocol protocol implementation
|
||||
|
||||
- parameter cellClass: UICollectionViewCell subclass which implements ReuseIdentifierProtocol and NibNameProtocol
|
||||
- parameter interfaceIdiom: UIUserInterfaceIdiom value for NibNameProtocol
|
||||
|
||||
- see: ReuseIdentifierProtocol, NibNameProtocol
|
||||
*/
|
||||
|
||||
public func registerNib<T>(forCellClass cellClass: T.Type,
|
||||
forUserInterfaceIdiom interfaceIdiom: UIUserInterfaceIdiom)
|
||||
where T: ReuseIdentifierProtocol, T: UICollectionViewCell, T: NibNameProtocol {
|
||||
|
||||
let nib = UINib(nibName: T.nibName(forConfiguration: interfaceIdiom))
|
||||
register(nib, forCellWithReuseIdentifier: T.reuseIdentifier)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,169 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2017 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
public extension UIImage {
|
||||
|
||||
/**
|
||||
method which creates new UIImage instance filled by given color
|
||||
|
||||
- parameter color: color to fill
|
||||
- parameter size: size of new image
|
||||
|
||||
- returns: new instanse of UIImage with given size and color
|
||||
*/
|
||||
|
||||
public convenience init?(color: UIColor, size: CGSize) {
|
||||
let cgImage = CGImage.create(color: color.cgColor,
|
||||
width: Int(ceil(size.width)),
|
||||
height: Int(ceil(size.height)))
|
||||
|
||||
guard let image = cgImage else {
|
||||
return nil
|
||||
}
|
||||
|
||||
self.init(cgImage: image)
|
||||
}
|
||||
|
||||
/**
|
||||
creates an image from a UIView.
|
||||
|
||||
- parameter fromView: The source view.
|
||||
|
||||
- returns A new image or nil if something goes wrong.
|
||||
*/
|
||||
|
||||
public convenience init?(fromView view: UIView) {
|
||||
guard let cgImage = CGImage.create(fromView: view) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
self.init(cgImage: cgImage)
|
||||
}
|
||||
|
||||
/**
|
||||
method which render current template CGImage into new image using given color
|
||||
|
||||
- parameter withColor: color which used to fill template image
|
||||
|
||||
- returns: new CGImage rendered with given color or nil if something goes wrong
|
||||
*/
|
||||
public func renderTemplate(withColor color: UIColor) -> UIImage? {
|
||||
return cgImage?.renderTemplate(withColor: color.cgColor)?.uiImage
|
||||
}
|
||||
|
||||
/**
|
||||
creates a new image with rounded corners and border.
|
||||
|
||||
- parameter cornerRadius: The corner radius.
|
||||
- parameter border: The size of the border.
|
||||
- parameter color: The color of the border.
|
||||
- parameter extendSize: Extend result image size and don't overlap source image by border.
|
||||
|
||||
- returns: A new image
|
||||
*/
|
||||
public func roundCorners(cornerRadius: CGFloat,
|
||||
borderWidth: CGFloat,
|
||||
color: UIColor,
|
||||
extendSize: Bool = false) -> UIImage? {
|
||||
|
||||
let rounded = cgImage?.round(withRadius: cornerRadius)
|
||||
|
||||
return rounded?.applyBorder(width: borderWidth,
|
||||
color: color.cgColor,
|
||||
radius: cornerRadius,
|
||||
extendSize: extendSize)?.uiImage
|
||||
}
|
||||
|
||||
/**
|
||||
creates a new circle image.
|
||||
|
||||
- returns: A new image
|
||||
*/
|
||||
public func roundCornersToCircle() -> UIImage? {
|
||||
return cgImage?.round(withRadius: CGFloat(min(size.width, size.height) / 2))?.uiImage
|
||||
}
|
||||
|
||||
/**
|
||||
creates a new circle image with a border.
|
||||
|
||||
- parameter border: CGFloat The size of the border.
|
||||
- parameter color: UIColor The color of the border.
|
||||
- parameter extendSize: Extend result image size and don't overlap source image by border.
|
||||
|
||||
- returns: UIImage?
|
||||
*/
|
||||
public func roundCornersToCircle(borderWidth: CGFloat,
|
||||
borderColor: UIColor,
|
||||
extendSize: Bool = false) -> UIImage? {
|
||||
|
||||
let radius = CGFloat(min(size.width, size.height) / 2)
|
||||
let rounded = cgImage?.round(withRadius: radius)
|
||||
|
||||
return rounded?.applyBorder(width: borderWidth,
|
||||
color: borderColor.cgColor,
|
||||
radius: radius,
|
||||
extendSize: extendSize)?.uiImage
|
||||
}
|
||||
|
||||
/**
|
||||
creates a resized copy of an image.
|
||||
|
||||
- parameter newSize: the new size of the image.
|
||||
- parameter contentMode: the way to handle the content in the new size.
|
||||
|
||||
- returns: a new image
|
||||
*/
|
||||
public func resize(newSize: CGSize, contentMode: ImageContentMode = .scaleToFill) -> UIImage? {
|
||||
return cgImage?.resize(newSize: newSize, contentMode: contentMode)?.uiImage
|
||||
}
|
||||
|
||||
/**
|
||||
creates a cropped copy of an image.
|
||||
|
||||
- parameter to: The bounds of the rectangle inside the image.
|
||||
|
||||
- returns: A new image
|
||||
*/
|
||||
public func crop(to bounds: CGRect) -> UIImage? {
|
||||
return cgImage?.cropping(to: bounds)?.uiImage
|
||||
}
|
||||
|
||||
/**
|
||||
crop image to square from center
|
||||
|
||||
- returns: cropped image
|
||||
*/
|
||||
public func cropFromCenterToSquare() -> UIImage? {
|
||||
return cgImage?.cropFromCenterToSquare()?.uiImage
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public extension CGImage {
|
||||
|
||||
public var uiImage: UIImage {
|
||||
return UIImage(cgImage: self)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2017 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
public extension UINib {
|
||||
convenience public init(nibName name: String) {
|
||||
self.init(nibName: name, bundle: nil)
|
||||
}
|
||||
}
|
||||
|
||||
public extension UIView {
|
||||
/**
|
||||
method which return UIView subclass instance loaded from nib using nib name provided by NibNameProtocol implementation
|
||||
|
||||
- parameter interfaceIdiom: UIUserInterfaceIdiom value for passing into NibNameProtocol
|
||||
|
||||
- returns: UIView subclass instance
|
||||
*/
|
||||
|
||||
public static func loadFromNib<T>
|
||||
(forUserInterfaceIdiom interfaceIdiom: UIUserInterfaceIdiom) -> T where T: NibNameProtocol, T: UIView {
|
||||
return loadFromNib(named: T.nibName(forConfiguration: interfaceIdiom))
|
||||
}
|
||||
|
||||
/**
|
||||
method which return UIView subclass instance loaded from nib using nib name
|
||||
provided by StaticNibNameProtocol implementation
|
||||
|
||||
- returns: UIView subclass instance
|
||||
*/
|
||||
public static func loadFromNib<T>() -> T where T: StaticNibNameProtocol, T: UIView {
|
||||
return loadFromNib(named: T.nibName)
|
||||
}
|
||||
|
||||
/**
|
||||
method which loads UIView (or subclass) instance from nib using given nib name parameter
|
||||
|
||||
- parameter nibName: nib name
|
||||
|
||||
- returns: UIView subclass instance
|
||||
*/
|
||||
public static func loadFromNib<T>(named nibName: String) -> T {
|
||||
guard let nibView = UINib(nibName: nibName).instantiate(withOwner: nil, options: nil).first as? T else {
|
||||
fatalError("Can't nstantiate nib view with type \(T.self)")
|
||||
}
|
||||
|
||||
return nibView
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,221 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2017 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import ObjectMapper
|
||||
import RxSwift
|
||||
|
||||
/// A type representing an possible errors that can be thrown during fetching
|
||||
/// model or array of specified type from UserDefaults.
|
||||
///
|
||||
/// - noSuchValue: there is no such value for given key
|
||||
/// - unableToMap: the value cannot be mapped to given type for some reason
|
||||
public enum UserDefaultsError: Error {
|
||||
|
||||
case noSuchValue(key: String)
|
||||
case unableToMap(mappingError: Error)
|
||||
|
||||
}
|
||||
|
||||
fileprivate typealias JSONObject = [String: Any]
|
||||
|
||||
public extension UserDefaults {
|
||||
|
||||
fileprivate func storedValue<ST>(forKey key: String) throws -> ST {
|
||||
guard let objectForKey = object(forKey: key) else {
|
||||
throw UserDefaultsError.noSuchValue(key: key)
|
||||
}
|
||||
|
||||
return try cast(objectForKey) as ST
|
||||
}
|
||||
|
||||
/// Returns the object with specified type associated with the first occurrence of the specified default.
|
||||
///
|
||||
/// - parameter key: A key in the current user's defaults database.
|
||||
///
|
||||
/// - throws: One of cases in UserDefaultsError
|
||||
///
|
||||
/// - returns: The object with specified type associated with the specified key,
|
||||
/// or throw exception if the key was not found.
|
||||
public func object<T>(forKey key: String) throws -> T where T: ImmutableMappable {
|
||||
let jsonObject = try storedValue(forKey: key) as JSONObject
|
||||
|
||||
do {
|
||||
return try T(JSON: jsonObject)
|
||||
} catch {
|
||||
throw UserDefaultsError.unableToMap(mappingError: error)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the array of objects with specified type associated with the first occurrence of the specified default.
|
||||
///
|
||||
/// - parameter key: A key in the current user's defaults database.
|
||||
///
|
||||
/// - throws: One of cases in UserDefaultsError
|
||||
///
|
||||
/// - returns: The array of objects with specified type associated with the specified key,
|
||||
/// or throw exception if the key was not found.
|
||||
public func objects<T>(forKey key: String) throws -> [T] where T: ImmutableMappable {
|
||||
let jsonArray = try storedValue(forKey: key) as [JSONObject]
|
||||
|
||||
do {
|
||||
return try jsonArray.map { try T(JSON: $0) }
|
||||
} catch {
|
||||
throw UserDefaultsError.unableToMap(mappingError: error)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the object with specified type associated with the first occurrence of the specified default.
|
||||
///
|
||||
/// - parameter key: A key in the current user's defaults database.
|
||||
/// - parameter defaultValue: A default value which will be used if there is no such value for specified key,
|
||||
/// or if error occurred during mapping
|
||||
///
|
||||
/// - returns: The object with specified type associated with the specified key, or passed default value
|
||||
/// if there is no such value for specified key or if error occurred during mapping.
|
||||
public func object<T>(forKey key: String, defaultValue: T) -> T where T: ImmutableMappable {
|
||||
return (try? object(forKey: key)) ?? defaultValue
|
||||
}
|
||||
|
||||
/// Returns the array of objects with specified type associated with the first occurrence of the specified default.
|
||||
///
|
||||
/// - parameter key: A key in the current user's defaults database.
|
||||
/// - parameter defaultValue: A default value which will be used if there is no such value for specified key,
|
||||
/// or if error occurred during mapping
|
||||
///
|
||||
/// - returns: The array of objects with specified type associated with the specified key, or passed default value
|
||||
/// if there is no such value for specified key or if error occurred during mapping.
|
||||
public func objects<T>(forKey key: String, defaultValue: [T]) -> [T] where T: ImmutableMappable {
|
||||
return (try? objects(forKey: key)) ?? defaultValue
|
||||
}
|
||||
|
||||
/// Sets or removes the value of the specified default key in the standard application domain.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - model: The object with specified type to store or nil to remove it from the defaults database.
|
||||
/// - key: The key with which to associate with the value.
|
||||
public func set<T>(model: T?, forKey key: String) where T: ImmutableMappable {
|
||||
if let model = model {
|
||||
set(model.toJSON(), forKey: key)
|
||||
} else {
|
||||
set(nil, forKey: key)
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets or removes the value of the specified default key in the standard application domain.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - models: The array of object with specified type to store or nil to remove it from the defaults database.
|
||||
/// - key: The key with which to associate with the value.
|
||||
public func set<T, S>(models: S?, forKey key: String) where T: ImmutableMappable, S: Sequence, S.Iterator.Element == T {
|
||||
if let models = models {
|
||||
set(models.map { $0.toJSON() }, forKey: key)
|
||||
} else {
|
||||
set(nil, forKey: key)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public extension Reactive where Base: UserDefaults {
|
||||
|
||||
/// Reactive version of object<T>(forKey:) -> T.
|
||||
///
|
||||
/// - parameter key: A key in the current user's defaults database.
|
||||
///
|
||||
/// - returns: Observable of specified model type.
|
||||
func object<T>(forKey key: String) -> Observable<T> where T: ImmutableMappable {
|
||||
return Observable.deferredJust { try self.base.object(forKey: key) }
|
||||
}
|
||||
|
||||
/// Reactive version of object<T>(forKey:defaultValue:) -> T.
|
||||
///
|
||||
/// Will never call onError(:) on observer.
|
||||
///
|
||||
/// - parameter key: A key in the current user's defaults database.
|
||||
/// - parameter defaultValue: A default value which will be used if there is no such value for specified key,
|
||||
/// or if error occurred during mapping
|
||||
///
|
||||
/// - returns: Observable of specified model type.
|
||||
func object<T>(forKey key: String, defaultValue: T) -> Observable<T> where T: ImmutableMappable {
|
||||
return Observable.deferredJust { self.base.object(forKey: key, defaultValue: defaultValue) }
|
||||
}
|
||||
|
||||
/// Reactive version of object<T>(forKey:) -> [T].
|
||||
///
|
||||
/// - parameter key: A key in the current user's defaults database.
|
||||
///
|
||||
/// - returns: Observable of specified array type.
|
||||
func object<T>(forKey key: String) -> Observable<[T]> where T: ImmutableMappable {
|
||||
return Observable.deferredJust { try self.base.objects(forKey: key) }
|
||||
}
|
||||
|
||||
/// Reactive version of object<T>(forKey:defaultValue:) -> [T].
|
||||
///
|
||||
/// Will never call onError(:) on observer.
|
||||
///
|
||||
/// - parameter key: A key in the current user's defaults database.
|
||||
/// - parameter defaultValue: A default value which will be used if there is no such value for specified key,
|
||||
/// or if error occurred during mapping
|
||||
///
|
||||
/// - returns: Observable of specified array type.
|
||||
func object<T>(forKey key: String, defaultValue: [T]) -> Observable<[T]> where T: ImmutableMappable {
|
||||
return Observable.deferredJust { self.base.objects(forKey: key, defaultValue: defaultValue) }
|
||||
}
|
||||
|
||||
/// Reactive version of set<T>(_:forKey:).
|
||||
///
|
||||
/// Will never call onError(:) on observer.
|
||||
///
|
||||
/// - parameter model: The object with specified type to store in the defaults database.
|
||||
/// - parameter key: The key with which to associate with the value.
|
||||
///
|
||||
/// - returns: Observable of Void type.
|
||||
func set<T>(model: T?, forKey key: String) -> Observable<Void> where T: ImmutableMappable {
|
||||
return Observable.create { observer in
|
||||
observer.onNext(self.base.set(model: model, forKey: key))
|
||||
observer.onCompleted()
|
||||
|
||||
return Disposables.create()
|
||||
}
|
||||
}
|
||||
|
||||
/// Reactive version of set<T, S>(_:forKey:).
|
||||
///
|
||||
/// Will never call onError(:) on observer.
|
||||
///
|
||||
/// - parameter models: The array of object with specified type to store in the defaults database.
|
||||
/// - parameter key: The key with which to associate with the value.
|
||||
///
|
||||
/// - returns: Observable of Void type.
|
||||
func set<T, S>(models: S?, forKey key: String) -> Observable<Void>
|
||||
where T: ImmutableMappable, S: Sequence, S.Iterator.Element == T {
|
||||
|
||||
return Observable.create { observer in
|
||||
observer.onNext(self.base.set(models: models, forKey: key))
|
||||
observer.onCompleted()
|
||||
|
||||
return Disposables.create()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2017 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
* protocol which ensures that specific type can return nib name of view for specific configuration
|
||||
*/
|
||||
public protocol AbstractNibNameProtocol {
|
||||
associatedtype ConfigurationType
|
||||
|
||||
/**
|
||||
static method which returns nib name for specific configuration
|
||||
|
||||
- parameter configuration: object which represents configuration
|
||||
|
||||
- returns: nib name string
|
||||
*/
|
||||
static func nibName(forConfiguration configuration: ConfigurationType) -> String
|
||||
}
|
||||
|
||||
/**
|
||||
* protocol which ensures that specific type can return nib name of view
|
||||
for specified UserInterfaceIdiom (iPhone, iPad, AppleTV)
|
||||
*/
|
||||
|
||||
public protocol NibNameProtocol: AbstractNibNameProtocol {
|
||||
/**
|
||||
static method which returns nib name for specific UIUserInterfaceIdiom value
|
||||
|
||||
- parameter configuration: object which represents configuration in terms of user interface idiom
|
||||
|
||||
- returns: nib name string
|
||||
*/
|
||||
static func nibName(forConfiguration configuration: UIUserInterfaceIdiom) -> String
|
||||
}
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2017 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
/**
|
||||
* protocol which helps us organize storyboards and view controllers creation
|
||||
*/
|
||||
public protocol StoryboardProtocol {
|
||||
|
||||
associatedtype StoryboardIdentifier
|
||||
associatedtype ViewControllerIdentifier
|
||||
|
||||
/**
|
||||
- returns: storyboard identifier with associatedtype type
|
||||
*/
|
||||
static var storyboardIdentifier: StoryboardIdentifier { get }
|
||||
|
||||
/**
|
||||
- returns: bundle for storyboard initialization
|
||||
*/
|
||||
static var bundle: Bundle? { get }
|
||||
|
||||
/**
|
||||
method which instantiate UIViewControlle instance for specific view controller identifier
|
||||
|
||||
- parameter _: object which represents view controller identifier
|
||||
|
||||
- returns: UIViewController instance
|
||||
*/
|
||||
static func instantiateViewController(_: ViewControllerIdentifier) -> UIViewController
|
||||
|
||||
}
|
||||
|
||||
public extension StoryboardProtocol {
|
||||
|
||||
public static func instantiate<T: UIViewController>(_ identificator: Self.ViewControllerIdentifier) -> T {
|
||||
guard let controller = instantiateViewController(identificator) as? T else {
|
||||
assertionFailure("\(T.self) not created")
|
||||
return T()
|
||||
}
|
||||
return controller
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,94 +0,0 @@
|
|||
//
|
||||
// MappableUserDefaultsTests.swift
|
||||
// LeadKit
|
||||
//
|
||||
// Created by Ivan Smolin on 28/02/2017.
|
||||
// Copyright © 2017 Touch Instinct. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
import LeadKit
|
||||
import RxSwift
|
||||
|
||||
class MappableUserDefaultsTests: XCTestCase {
|
||||
|
||||
lazy var post: Post = {
|
||||
return Post(userId: 1, id: 1, title: "First post", body: "")
|
||||
}()
|
||||
|
||||
lazy var posts: [Post] = {
|
||||
return [Post(userId: 1, id: 1, title: "First post", body: ""),
|
||||
Post(userId: 1, id: 2, title: "Second post", body: ""),
|
||||
Post(userId: 2, id: 3, title: "Third post", body: ""),
|
||||
Post(userId: 2, id: 4, title: "Forth post", body: "")]
|
||||
}()
|
||||
|
||||
let userDefaults = UserDefaults.standard
|
||||
|
||||
static let postKey = "post"
|
||||
static let postsKey = "posts"
|
||||
|
||||
let disposeBag = DisposeBag()
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
userDefaults.set(nil, forKey: MappableUserDefaultsTests.postKey)
|
||||
userDefaults.set(nil, forKey: MappableUserDefaultsTests.postsKey)
|
||||
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func testPostSave() {
|
||||
userDefaults.set(model: post, forKey: MappableUserDefaultsTests.postKey)
|
||||
|
||||
do {
|
||||
let savedPost: Post = try userDefaults.object(forKey: MappableUserDefaultsTests.postKey)
|
||||
|
||||
XCTAssertTrue(savedPost == post, "Saved post != test post")
|
||||
} catch {
|
||||
XCTFail(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
func testPostsSave() {
|
||||
userDefaults.set(models: posts, forKey: MappableUserDefaultsTests.postsKey)
|
||||
|
||||
do {
|
||||
let savedPosts: [Post] = try userDefaults.objects(forKey: MappableUserDefaultsTests.postsKey)
|
||||
|
||||
XCTAssertTrue(savedPosts == posts, "Saved posts != test posts")
|
||||
} catch {
|
||||
XCTFail(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
func testRxPostSave() {
|
||||
userDefaults.rx.set(model: post, forKey: MappableUserDefaultsTests.postKey)
|
||||
.flatMap {
|
||||
self.userDefaults.rx.object(forKey: MappableUserDefaultsTests.postKey) as Observable<Post>
|
||||
}
|
||||
.subscribe(onNext: { savedPost in
|
||||
XCTAssertTrue(savedPost == self.post, "Saved post != test post")
|
||||
}, onError: { error in
|
||||
XCTFail(error.localizedDescription)
|
||||
})
|
||||
.addDisposableTo(disposeBag)
|
||||
}
|
||||
|
||||
func testRxPostsSave() {
|
||||
userDefaults.rx.set(models: posts, forKey: MappableUserDefaultsTests.postsKey)
|
||||
.flatMap {
|
||||
self.userDefaults.rx.object(forKey: MappableUserDefaultsTests.postsKey) as Observable<[Post]>
|
||||
}
|
||||
.subscribe(onNext: { savedPosts in
|
||||
XCTAssertTrue(savedPosts == self.posts, "Saved posts != test posts")
|
||||
}, onError: { error in
|
||||
XCTFail(error.localizedDescription)
|
||||
})
|
||||
.addDisposableTo(disposeBag)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
# Uncomment the next line to define a global platform for your project
|
||||
platform :ios, '9.0'
|
||||
|
||||
target 'LeadKit' do
|
||||
use_frameworks!
|
||||
|
||||
pod "CocoaLumberjack/Swift", '~> 3.1.0'
|
||||
pod "RxSwift", '3.2.0'
|
||||
pod "RxCocoa", '3.2.0'
|
||||
pod "RxAlamofire", '3.0.0'
|
||||
pod "ObjectMapper", '~> 2.1'
|
||||
pod "Toast-Swift", '~> 2.0.0'
|
||||
|
||||
target 'LeadKitTests' do
|
||||
inherit! :search_paths
|
||||
# Pods for testing
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
# If you have slow HDD
|
||||
ENV['COCOAPODS_DISABLE_STATS'] = "true"
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
PODS:
|
||||
- Alamofire (4.4.0)
|
||||
- CocoaLumberjack/Default (3.1.0)
|
||||
- CocoaLumberjack/Swift (3.1.0):
|
||||
- CocoaLumberjack/Default
|
||||
- ObjectMapper (2.2.5)
|
||||
- RxAlamofire (3.0.0):
|
||||
- RxAlamofire/Core (= 3.0.0)
|
||||
- RxAlamofire/Core (3.0.0):
|
||||
- Alamofire (~> 4.0)
|
||||
- RxSwift (~> 3.0)
|
||||
- RxCocoa (3.2.0):
|
||||
- RxSwift (~> 3.1)
|
||||
- RxSwift (3.2.0)
|
||||
- Toast-Swift (2.0.0)
|
||||
|
||||
DEPENDENCIES:
|
||||
- CocoaLumberjack/Swift (~> 3.1.0)
|
||||
- ObjectMapper (~> 2.1)
|
||||
- RxAlamofire (= 3.0.0)
|
||||
- RxCocoa (= 3.2.0)
|
||||
- RxSwift (= 3.2.0)
|
||||
- Toast-Swift (~> 2.0.0)
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
Alamofire: dc44b1600b800eb63da6a19039a0083d62a6a62d
|
||||
CocoaLumberjack: 8311463ddf9ee86a06ef92a071dd656c89244500
|
||||
ObjectMapper: fb30f71e08470d1e5a20b199fafe1246281db898
|
||||
RxAlamofire: 0b1fa48f545fffe7f7a28af2086bcaa3b5946cc9
|
||||
RxCocoa: ccdf43101a70407097a29082f648ba1676075b30
|
||||
RxSwift: 46574f70d416b7923c237195939cc488a7fbf3a0
|
||||
Toast-Swift: 5b2f8f720f7e78e48511f693df1f9c9a6e38a25a
|
||||
|
||||
PODFILE CHECKSUM: ee07f67e4cce90b2d448e0c18941bc51ab6180cc
|
||||
|
||||
COCOAPODS: 1.2.0
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
export SRCROOT := $(shell pwd)
|
||||
|
||||
push_to_podspecs: TISwiftUtils.target TIFoundationUtils.target TICoreGraphicsUtils.target TIKeychainUtils.target TIUIKitCore.target TIUIElements.target TIWebView.target TIBottomSheet.target TISwiftUICore.target TITableKitUtils.target TIDeeplink.target TIDeveloperUtils.target TILogging.target TINetworking.target TIMoyaNetworking.target TINetworkingCache.target TIMapUtils.target TIAppleMapUtils.target TIGoogleMapUtils.target TIPagination.target TIAuth.target TIEcommerce.target TITextProcessing.target TIApplication.target
|
||||
$(call clean)
|
||||
|
||||
TISwiftUtils.target:
|
||||
MODULE_NAME="TISwiftUtils" ./project-scripts/push_to_podspecs.sh
|
||||
touch TISwiftUtils.target
|
||||
|
||||
TIFoundationUtils.target: TISwiftUtils.target TILogging.target
|
||||
MODULE_NAME="TIFoundationUtils" ./project-scripts/push_to_podspecs.sh
|
||||
touch TIFoundationUtils.target
|
||||
|
||||
TICoreGraphicsUtils.target:
|
||||
MODULE_NAME="TICoreGraphicsUtils" ./project-scripts/push_to_podspecs.sh
|
||||
touch TICoreGraphicsUtils.target
|
||||
|
||||
TIKeychainUtils.target: TIFoundationUtils.target
|
||||
MODULE_NAME="TIKeychainUtils" ./project-scripts/push_to_podspecs.sh
|
||||
touch TIKeychainUtils.target
|
||||
|
||||
TIUIKitCore.target: TISwiftUtils.target
|
||||
MODULE_NAME="TIUIKitCore" ./project-scripts/push_to_podspecs.sh
|
||||
touch TIUIKitCore.target
|
||||
|
||||
TIUIElements.target: TIUIKitCore.target TILogging.target
|
||||
MODULE_NAME="TIUIElements" ./project-scripts/push_to_podspecs.sh
|
||||
touch TIUIElements.target
|
||||
|
||||
TIWebView.target: TIUIKitCore.target
|
||||
MODULE_NAME="TIWebView" ./project-scripts/push_to_podspecs.sh
|
||||
touch TIWebView.target
|
||||
|
||||
TIBottomSheet.target: TIUIElements.target
|
||||
MODULE_NAME="TIBottomSheet" ./project-scripts/push_to_podspecs.sh
|
||||
touch TIBottomSheet.target
|
||||
|
||||
TISwiftUICore.target: TIUIKitCore.target
|
||||
MODULE_NAME="TISwiftUICore" ./project-scripts/push_to_podspecs.sh
|
||||
touch TISwiftUICore.target
|
||||
|
||||
TITableKitUtils.target: TIUIElements.target
|
||||
MODULE_NAME="TITableKitUtils" ./project-scripts/push_to_podspecs.sh
|
||||
touch TITableKitUtils.target
|
||||
|
||||
TIDeeplink.target: TIFoundationUtils.target
|
||||
MODULE_NAME="TIDeeplink" ./project-scripts/push_to_podspecs.sh
|
||||
touch TIDeeplink.target
|
||||
|
||||
TIDeveloperUtils.target: TIUIElements.target
|
||||
MODULE_NAME="TIDeveloperUtils" ./project-scripts/push_to_podspecs.sh
|
||||
touch TIDeveloperUtils.target
|
||||
|
||||
TINetworking.target: TIFoundationUtils.target
|
||||
MODULE_NAME="TINetworking" ./project-scripts/push_to_podspecs.sh
|
||||
touch TINetworking.target
|
||||
|
||||
TILogging.target:
|
||||
MODULE_NAME="TILogging" ./project-scripts/push_to_podspecs.sh
|
||||
touch TILogging.target
|
||||
|
||||
TIMoyaNetworking.target: TINetworking.target
|
||||
MODULE_NAME="TIMoyaNetworking" ./project-scripts/push_to_podspecs.sh
|
||||
touch TIMoyaNetworking.target
|
||||
|
||||
TINetworkingCache.target: TINetworking.target
|
||||
MODULE_NAME="TINetworkingCache" ./project-scripts/push_to_podspecs.sh
|
||||
touch TINetworkingCache.target
|
||||
|
||||
TIMapUtils.target: TILogging TICoreGraphicsUtils.target
|
||||
MODULE_NAME="TIMapUtils" ./project-scripts/push_to_podspecs.sh
|
||||
touch TIMapUtils.target
|
||||
|
||||
TIAppleMapUtils.target: TIMapUtils.target
|
||||
MODULE_NAME="TIAppleMapUtils" ./project-scripts/push_to_podspecs.sh
|
||||
touch TIAppleMapUtils.target
|
||||
|
||||
TIGoogleMapUtils.target: TIMapUtils.target
|
||||
MODULE_NAME="TIGoogleMapUtils" ./project-scripts/push_to_podspecs.sh
|
||||
touch TIGoogleMapUtils.target
|
||||
|
||||
TIYandexMapUtils.target: TIMapUtils.target
|
||||
MODULE_NAME="TIYandexMapUtils" ./project-scripts/push_to_podspecs.sh
|
||||
touch TIYandexMapUtils.target
|
||||
|
||||
TIPagination.target: TISwiftUtils.target
|
||||
MODULE_NAME="TIPagination" ./project-scripts/push_to_podspecs.sh
|
||||
touch TIPagination.target
|
||||
|
||||
TIAuth.target: TIUIKitCore.target TIKeychainUtils.target
|
||||
MODULE_NAME="TIAuth" ./project-scripts/push_to_podspecs.sh
|
||||
touch TIAuth.target
|
||||
|
||||
TIEcommerce.target: TINetworking.target TIUIElements.target
|
||||
MODULE_NAME="TIEcommerce" ./project-scripts/push_to_podspecs.sh
|
||||
touch TIEcommerce.target
|
||||
|
||||
TITextProcessing.target:
|
||||
MODULE_NAME="TITextProcessing" ./project-scripts/push_to_podspecs.sh
|
||||
touch TITextProcessing.target
|
||||
|
||||
TIApplication.target: TIFoundationUtils.target TILogging.target
|
||||
MODULE_NAME="TIApplication" ./project-scripts/push_to_podspecs.sh
|
||||
touch TIApplication.target
|
||||
|
||||
clean:
|
||||
rm *.target
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.5 MiB |
|
|
@ -0,0 +1,151 @@
|
|||
# OTPSwiftView
|
||||
|
||||

|
||||
|
||||
A fully customizable OTP view.
|
||||
|
||||
<p align="left">
|
||||
<img src="Assets/preview.gif" width=300 height=533>
|
||||
</p>
|
||||
|
||||
# Usage
|
||||
```swift
|
||||
class ViewController: UIViewController {
|
||||
let otpView = CustomOTPSwiftView() // Custom OTP view
|
||||
|
||||
let config = OTPCodeConfig(codeSymbolsCount: 6, // Base configuration of OTP view
|
||||
spacing: 6,
|
||||
customSpacing: [2: 20])
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
/*
|
||||
Add your codeView and set layout
|
||||
*/
|
||||
|
||||
/* Configure OTP view */
|
||||
|
||||
otpView.configure(with: config)
|
||||
|
||||
/* Bind events */
|
||||
|
||||
otpView.onTextEnter = { code in
|
||||
// Get code from codeView
|
||||
}
|
||||
|
||||
/* Update text */
|
||||
|
||||
otpView.code = "234435"
|
||||
|
||||
/* Update focus */
|
||||
|
||||
otpView.beginFirstResponder() // show keyboard
|
||||
otpView.resignFirstResponder() // hide keyboard
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# Customization
|
||||
## Single OTP View
|
||||
*OTPView* is a base class that describes a single OTP textfield.
|
||||
To customize the appearance and layout, you must inherit from the OTPView.
|
||||
*Don't forget to add UIGestureRecognizer to call closure `onTap?()`. Use UITapGestureRecognizer to avoid bugs.*
|
||||
|
||||
```swift
|
||||
import OTPSwiftView
|
||||
|
||||
class CustomOTPView: OTPView {
|
||||
override func addViews() {
|
||||
super.addViews()
|
||||
|
||||
// Adding additional views to current view. The OTP textfield has already been added.
|
||||
}
|
||||
|
||||
override func configureLayout() {
|
||||
super.configureLayout()
|
||||
|
||||
// Confgiure layout of subviews
|
||||
}
|
||||
|
||||
override func bindViews() {
|
||||
super.bindViews()
|
||||
|
||||
// Binding to data or user actions
|
||||
|
||||
let gesture = UITapGestureRecognizer(target: self, action: #selector(onTapAction))
|
||||
addGestureRecognizer(gesture)
|
||||
}
|
||||
|
||||
private func onTapAction() {
|
||||
onTap?()
|
||||
}
|
||||
|
||||
override func configureAppearance() {
|
||||
super.configureAppearance()
|
||||
|
||||
// Appearance configuration method
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
*If needed to set validation for input use `validationClosure: ValidationClosure<String>?`*. For example, only numbers validation:
|
||||
|
||||
```swift
|
||||
import OTPSwiftView
|
||||
|
||||
class CustomOTPView: OTPView {
|
||||
|
||||
override func bindViews() {
|
||||
super.bindViews()
|
||||
|
||||
codeTextField.validationClosure = { input in
|
||||
input.allSatisfy { $0.isNumber }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## OTPSwiftView
|
||||
*OTPSwiftView* is a base class that is responsible for the layout of single OTP views.
|
||||
As with OTPView, you should create an heir class to configure your full OTP view.
|
||||
|
||||
```swift
|
||||
import OTPSwiftView
|
||||
|
||||
final class CustomOTPSwiftView: OTPSwiftView<CustomOTPView> {
|
||||
override func addViews() {
|
||||
super.addViews()
|
||||
|
||||
// Adding additional views to current code view. The single OTP views has already been added.
|
||||
}
|
||||
|
||||
override func configureLayout() {
|
||||
super.configureLayout()
|
||||
|
||||
// Confgiure layout of subviews
|
||||
}
|
||||
|
||||
override func bindViews() {
|
||||
super.bindViews()
|
||||
|
||||
// Binding to data or user actions
|
||||
}
|
||||
|
||||
override func configureAppearance() {
|
||||
super.configureAppearance()
|
||||
|
||||
// Appearance configuration method
|
||||
}
|
||||
|
||||
override func configure(with config: OTPCodeConfig) {
|
||||
super.configure(with: config)
|
||||
|
||||
// Configure you code view with configuration
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# Installation via SPM
|
||||
|
||||
You can install this framework as a target of LeadKit.
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
//
|
||||
// Copyright (c) 2020 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
/// Base configuration for OTPSwiftView
|
||||
open class OTPCodeConfig {
|
||||
public typealias Spacing = [Int: CGFloat]
|
||||
|
||||
public let codeSymbolsCount: Int
|
||||
public let spacing: CGFloat
|
||||
public let customSpacing: Spacing?
|
||||
|
||||
public init(codeSymbolsCount: Int, spacing: CGFloat, customSpacing: Spacing?) {
|
||||
self.codeSymbolsCount = codeSymbolsCount
|
||||
self.spacing = spacing
|
||||
self.customSpacing = customSpacing
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
//
|
||||
// Copyright (c) 2020 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import TIUIElements
|
||||
import TISwiftUtils
|
||||
|
||||
/// Base full OTP View for entering the verification code
|
||||
open class OTPSwiftView<View: OTPView>: BaseInitializableControl {
|
||||
private var emptyOTPView: View? {
|
||||
textFieldsCollection.first { $0.codeTextField.text.orEmpty.isEmpty } ?? textFieldsCollection.last
|
||||
}
|
||||
|
||||
public private(set) var codeStackView = UIStackView()
|
||||
public private(set) var textFieldsCollection: [View] = []
|
||||
|
||||
public var onTextEnter: ParameterClosure<String>?
|
||||
|
||||
public var code: String {
|
||||
get {
|
||||
textFieldsCollection.compactMap { $0.codeTextField.text }.joined()
|
||||
}
|
||||
set {
|
||||
textFieldsCollection.first?.codeTextField.set(inputText: newValue)
|
||||
}
|
||||
}
|
||||
|
||||
public override var isFirstResponder: Bool {
|
||||
!textFieldsCollection.allSatisfy { !$0.codeTextField.isFirstResponder }
|
||||
}
|
||||
|
||||
open override func addViews() {
|
||||
super.addViews()
|
||||
|
||||
addSubview(codeStackView)
|
||||
}
|
||||
|
||||
open override func configureAppearance() {
|
||||
super.configureAppearance()
|
||||
|
||||
codeStackView.contentMode = .center
|
||||
codeStackView.distribution = .fillEqually
|
||||
}
|
||||
|
||||
open func configure(with config: OTPCodeConfig) {
|
||||
textFieldsCollection = createTextFields(numberOfFields: config.codeSymbolsCount)
|
||||
|
||||
codeStackView.addArrangedSubviews(textFieldsCollection)
|
||||
codeStackView.spacing = config.spacing
|
||||
|
||||
configure(customSpacing: config.customSpacing, for: codeStackView)
|
||||
|
||||
bindTextFields(with: config)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open override func becomeFirstResponder() -> Bool {
|
||||
guard let emptyOTPView = emptyOTPView, !emptyOTPView.isFirstResponder else {
|
||||
return false
|
||||
}
|
||||
|
||||
return emptyOTPView.codeTextField.becomeFirstResponder()
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open override func resignFirstResponder() -> Bool {
|
||||
guard let emptyOTPView = emptyOTPView, emptyOTPView.isFirstResponder else {
|
||||
return false
|
||||
}
|
||||
|
||||
return emptyOTPView.codeTextField.resignFirstResponder()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Configure textfields
|
||||
|
||||
private extension OTPSwiftView {
|
||||
func configure(customSpacing: OTPCodeConfig.Spacing?, for stackView: UIStackView) {
|
||||
guard let customSpacing = customSpacing else {
|
||||
return
|
||||
}
|
||||
|
||||
customSpacing.forEach { viewIndex, spacing in
|
||||
guard viewIndex < stackView.arrangedSubviews.count, viewIndex >= .zero else {
|
||||
return
|
||||
}
|
||||
|
||||
self.set(spacing: spacing,
|
||||
after: stackView.arrangedSubviews[viewIndex],
|
||||
for: stackView)
|
||||
}
|
||||
}
|
||||
|
||||
func set(spacing: CGFloat, after view: UIView, for stackView: UIStackView) {
|
||||
stackView.setCustomSpacing(spacing, after: view)
|
||||
}
|
||||
|
||||
func createTextFields(numberOfFields: Int) -> [View] {
|
||||
var textFieldsCollection: [View] = []
|
||||
|
||||
(.zero..<numberOfFields).forEach { _ in
|
||||
let textField = View()
|
||||
textField.codeTextField.previousTextField = textFieldsCollection.last?.codeTextField
|
||||
textFieldsCollection.last?.codeTextField.nextTextField = textField.codeTextField
|
||||
textFieldsCollection.append(textField)
|
||||
}
|
||||
|
||||
return textFieldsCollection
|
||||
}
|
||||
|
||||
func bindTextFields(with config: OTPCodeConfig) {
|
||||
let onTextChangedSignal: VoidClosure = { [weak self] in
|
||||
guard let code = self?.code else {
|
||||
return
|
||||
}
|
||||
|
||||
let correctedCode = code.prefix(config.codeSymbolsCount).string
|
||||
self?.onTextEnter?(correctedCode)
|
||||
}
|
||||
|
||||
let onTap: VoidClosure = { [weak self] in
|
||||
self?.becomeFirstResponder()
|
||||
}
|
||||
|
||||
textFieldsCollection.forEach {
|
||||
$0.codeTextField.onTextChangedSignal = onTextChangedSignal
|
||||
$0.onTap = onTap
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
//
|
||||
// Copyright (c) 2020 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import TISwiftUtils
|
||||
|
||||
/// Base one symbol textfield
|
||||
open class OTPTextField: UITextField {
|
||||
private let maxSymbolsCount = 1
|
||||
|
||||
public weak var previousTextField: OTPTextField?
|
||||
public weak var nextTextField: OTPTextField?
|
||||
|
||||
public var onTextChangedSignal: VoidClosure?
|
||||
public var validationClosure: Closure<String, Bool>?
|
||||
public var caretHeight: CGFloat?
|
||||
|
||||
public var lastNotEmpty: OTPTextField {
|
||||
let isLastNotEmpty = !text.orEmpty.isEmpty && nextTextField?.text.orEmpty.isEmpty ?? true
|
||||
return isLastNotEmpty ? self : nextTextField?.lastNotEmpty ?? self
|
||||
}
|
||||
|
||||
open override var font: UIFont? {
|
||||
didSet {
|
||||
if caretHeight == nil, let font = font {
|
||||
caretHeight = font.pointSize - font.descender
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
delegate = self
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required public init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
open override func deleteBackward() {
|
||||
guard text.orEmpty.isEmpty else {
|
||||
return
|
||||
}
|
||||
|
||||
onTextChangedSignal?()
|
||||
previousTextField?.text = ""
|
||||
previousTextField?.becomeFirstResponder()
|
||||
}
|
||||
|
||||
public func set(inputText: String) {
|
||||
text = inputText.prefix(maxSymbolsCount).string
|
||||
|
||||
let nextInputText = inputText.count >= maxSymbolsCount
|
||||
? inputText.suffix(inputText.count - maxSymbolsCount).string
|
||||
: ""
|
||||
|
||||
nextTextField?.set(inputText: nextInputText)
|
||||
}
|
||||
|
||||
open override func caretRect(for position: UITextPosition) -> CGRect {
|
||||
guard let caretHeight = caretHeight else {
|
||||
return super.caretRect(for: position)
|
||||
}
|
||||
|
||||
var superRect = super.caretRect(for: position)
|
||||
superRect.size.height = caretHeight
|
||||
|
||||
return superRect
|
||||
}
|
||||
|
||||
open override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
let view = super.hitTest(point, with: event)
|
||||
return view == self && isFirstResponder ? view : nil
|
||||
}
|
||||
}
|
||||
|
||||
extension OTPTextField: UITextFieldDelegate {
|
||||
public func textField(_ textField: UITextField,
|
||||
shouldChangeCharactersIn range: NSRange,
|
||||
replacementString string: String) -> Bool {
|
||||
guard let textField = textField as? OTPTextField else {
|
||||
return true
|
||||
}
|
||||
|
||||
let isInputEmpty = textField.text.orEmpty.isEmpty && string.isEmpty
|
||||
|
||||
guard isInputEmpty || validationClosure?(string) ?? true else {
|
||||
return false
|
||||
}
|
||||
|
||||
switch range.length {
|
||||
case 0: // set text to textfield
|
||||
textField.set(inputText: string)
|
||||
|
||||
let currentTextField = textField.lastNotEmpty.nextTextField ?? textField.lastNotEmpty
|
||||
currentTextField.becomeFirstResponder()
|
||||
textField.onTextChangedSignal?()
|
||||
|
||||
return false
|
||||
|
||||
case 1: // remove character from textfield
|
||||
textField.text = ""
|
||||
textField.onTextChangedSignal?()
|
||||
return false
|
||||
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
//
|
||||
// Copyright (c) 2020 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import TIUIElements
|
||||
import TISwiftUtils
|
||||
|
||||
/// Base OTP view with textfield for entering a one symbol
|
||||
open class OTPView: BaseInitializableView {
|
||||
public let codeTextField = OTPTextField()
|
||||
|
||||
public var onTap: VoidClosure?
|
||||
|
||||
open override func addViews() {
|
||||
super.addViews()
|
||||
|
||||
addSubview(codeTextField)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
{
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "alamofire",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/Alamofire/Alamofire.git",
|
||||
"state" : {
|
||||
"revision" : "bc268c28fb170f494de9e9927c371b8342979ece",
|
||||
"version" : "5.7.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "antlr4",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/antlr/antlr4",
|
||||
"state" : {
|
||||
"revision" : "44d87bc1d130c88aa452894aa5f7e2f710f68253",
|
||||
"version" : "4.10.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "cache",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/hyperoslo/Cache.git",
|
||||
"state" : {
|
||||
"revision" : "c7f4d633049c3bd649a353bad36f6c17e9df085f",
|
||||
"version" : "6.0.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "cursors",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/petropavel13/Cursors",
|
||||
"state" : {
|
||||
"revision" : "52f27b82cb1cbbc2b5fd09514c48b9c75e3b0300",
|
||||
"version" : "0.6.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "keychainaccess",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/kishikawakatsumi/KeychainAccess.git",
|
||||
"state" : {
|
||||
"revision" : "84e546727d66f1adc5439debad16270d0fdd04e7",
|
||||
"version" : "4.2.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "moya",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/Moya/Moya.git",
|
||||
"state" : {
|
||||
"revision" : "c263811c1f3dbf002be9bd83107f7cdc38992b26",
|
||||
"version" : "15.0.3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "panmodal",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://git.svc.touchin.ru/TouchInstinct/PanModal",
|
||||
"state" : {
|
||||
"revision" : "ced7c1703f90746df0224b6e0d33c146d9ae4284",
|
||||
"version" : "1.3.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "reactiveswift",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/ReactiveCocoa/ReactiveSwift.git",
|
||||
"state" : {
|
||||
"revision" : "c43bae3dac73fdd3cb906bd5a1914686ca71ed3c",
|
||||
"version" : "6.7.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "rxswift",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/ReactiveX/RxSwift.git",
|
||||
"state" : {
|
||||
"revision" : "9dcaa4b333db437b0fbfaf453fad29069044a8b4",
|
||||
"version" : "6.6.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "tablekit",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://git.svc.touchin.ru/TouchInstinct/TableKit.git",
|
||||
"state" : {
|
||||
"revision" : "fec9537745799fab55df7477cb3ec2b4ea5c254d",
|
||||
"version" : "2.12.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 2
|
||||
}
|
||||
|
|
@ -0,0 +1,198 @@
|
|||
// swift-tools-version:5.7
|
||||
|
||||
#if canImport(PackageDescription)
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "LeadKit",
|
||||
platforms: [
|
||||
.iOS(.v12)
|
||||
],
|
||||
products: [
|
||||
|
||||
// MARK: - Application
|
||||
|
||||
.library(name: "TIApplication", targets: ["TIApplication"]),
|
||||
|
||||
// MARK: - UIKit
|
||||
|
||||
.library(name: "TIUIKitCore", targets: ["TIUIKitCore"]),
|
||||
.library(name: "TIUIElements", targets: ["TIUIElements"]),
|
||||
.library(name: "TIWebView", targets: ["TIWebView"]),
|
||||
.library(name: "TIBottomSheet", targets: ["TIBottomSheet"]),
|
||||
|
||||
// MARK: - SwiftUI
|
||||
|
||||
.library(name: "TISwiftUICore", targets: ["TISwiftUICore"]),
|
||||
|
||||
// MARK: - Utils
|
||||
.library(name: "TISwiftUtils", targets: ["TISwiftUtils"]),
|
||||
.library(name: "TIFoundationUtils", targets: ["TIFoundationUtils"]),
|
||||
.library(name: "TICoreGraphicsUtils", targets: ["TICoreGraphicsUtils"]),
|
||||
.library(name: "TIKeychainUtils", targets: ["TIKeychainUtils"]),
|
||||
.library(name: "TITableKitUtils", targets: ["TITableKitUtils"]),
|
||||
.library(name: "TIDeeplink", targets: ["TIDeeplink"]),
|
||||
.library(name: "TIDeveloperUtils", targets: ["TIDeveloperUtils"]),
|
||||
|
||||
// MARK: - Networking
|
||||
|
||||
.library(name: "TINetworking", targets: ["TINetworking"]),
|
||||
.library(name: "TIMoyaNetworking", targets: ["TIMoyaNetworking"]),
|
||||
.library(name: "TINetworkingCache", targets: ["TINetworkingCache"]),
|
||||
|
||||
// MARK: - Maps
|
||||
|
||||
.library(name: "TIMapUtils", targets: ["TIMapUtils"]),
|
||||
.library(name: "TIAppleMapUtils", targets: ["TIAppleMapUtils"]),
|
||||
|
||||
// MARK: - Elements
|
||||
|
||||
.library(name: "OTPSwiftView", targets: ["OTPSwiftView"]),
|
||||
.library(name: "TITransitions", targets: ["TITransitions"]),
|
||||
.library(name: "TIPagination", targets: ["TIPagination"]),
|
||||
.library(name: "TIAuth", targets: ["TIAuth"]),
|
||||
.library(name: "TIEcommerce", targets: ["TIEcommerce"]),
|
||||
.library(name: "TITextProcessing", targets: ["TITextProcessing"])
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://git.svc.touchin.ru/TouchInstinct/TableKit.git", .upToNextMinor(from: "2.12.0")),
|
||||
.package(url: "https://github.com/kishikawakatsumi/KeychainAccess.git", .upToNextMajor(from: "4.2.2")),
|
||||
.package(url: "https://github.com/petropavel13/Cursors", .upToNextMajor(from: "0.5.1")),
|
||||
.package(url: "https://github.com/Alamofire/Alamofire.git", .upToNextMajor(from: "5.4.0")),
|
||||
.package(url: "https://github.com/Moya/Moya.git", .upToNextMajor(from: "15.0.0")),
|
||||
.package(url: "https://github.com/hyperoslo/Cache.git", .upToNextMajor(from: "6.0.0")),
|
||||
.package(url: "https://github.com/antlr/antlr4", .upToNextMinor(from: "4.10.1")),
|
||||
.package(url: "https://git.svc.touchin.ru/TouchInstinct/PanModal", .upToNextMinor(from: "1.3.0"))
|
||||
],
|
||||
targets: [
|
||||
|
||||
// MARK: - Application architecture
|
||||
|
||||
.target(name: "TIApplication",
|
||||
dependencies: ["TILogging", "TIFoundationUtils", "KeychainAccess"],
|
||||
path: "TIApplication/Sources",
|
||||
plugins: [.plugin(name: "TISwiftLintPlugin")]),
|
||||
|
||||
// MARK: - UIKit
|
||||
|
||||
.target(name: "TIUIKitCore", dependencies: ["TISwiftUtils"], path: "TIUIKitCore/Sources"),
|
||||
|
||||
.target(name: "TIUIElements",
|
||||
dependencies: ["TIUIKitCore", "TILogging"],
|
||||
path: "TIUIElements/Sources",
|
||||
exclude: ["../TIUIElements.app"],
|
||||
plugins: [.plugin(name: "TISwiftLintPlugin")]),
|
||||
|
||||
.target(name: "TIWebView", dependencies: ["TIUIKitCore", "TISwiftUtils"], path: "TIWebView/Sources"),
|
||||
.target(name: "TIBottomSheet",
|
||||
dependencies: ["PanModal", "TIUIElements", "TIUIKitCore", "TISwiftUtils"],
|
||||
path: "TIBottomSheet/Sources",
|
||||
exclude: ["../TIBottomSheet.app"],
|
||||
plugins: [.plugin(name: "TISwiftLintPlugin")]),
|
||||
|
||||
// MARK: - SwiftUI
|
||||
|
||||
.target(name: "TISwiftUICore",
|
||||
dependencies: ["TIUIKitCore", "TISwiftUtils"],
|
||||
path: "TISwiftUICore/Sources"),
|
||||
|
||||
// MARK: - Utils
|
||||
|
||||
.target(name: "TISwiftUtils",
|
||||
path: "TISwiftUtils/Sources",
|
||||
plugins: [.plugin(name: "TISwiftLintPlugin")]),
|
||||
|
||||
.target(name: "TIFoundationUtils",
|
||||
dependencies: ["TISwiftUtils", "TILogging"],
|
||||
path: "TIFoundationUtils",
|
||||
exclude: ["TIFoundationUtils.app"],
|
||||
resources: [
|
||||
.copy("PrivacyInfo.xcprivacy"),
|
||||
],
|
||||
plugins: [.plugin(name: "TISwiftLintPlugin")]),
|
||||
|
||||
.target(name: "TICoreGraphicsUtils",
|
||||
dependencies: [],
|
||||
path: "TICoreGraphicsUtils/Sources",
|
||||
exclude: ["../TICoreGraphicsUtils.app"],
|
||||
plugins: [.plugin(name: "TISwiftLintPlugin")]),
|
||||
|
||||
.target(name: "TIKeychainUtils",
|
||||
dependencies: ["TIFoundationUtils", "KeychainAccess"],
|
||||
path: "TIKeychainUtils/Sources",
|
||||
exclude: ["../TIKeychainUtils.app"],
|
||||
plugins: [.plugin(name: "TISwiftLintPlugin")]),
|
||||
|
||||
.target(name: "TITableKitUtils", dependencies: ["TIUIElements", "TableKit"], path: "TITableKitUtils/Sources"),
|
||||
.target(name: "TIDeeplink", dependencies: ["TIFoundationUtils"], path: "TIDeeplink", exclude: ["TIDeeplink.app"]),
|
||||
.target(name: "TIDeveloperUtils", dependencies: ["TISwiftUtils", "TIUIKitCore", "TIUIElements"], path: "TIDeveloperUtils/Sources"),
|
||||
.target(name: "TILogging", path: "TILogging/Sources", plugins: ["TISwiftLintPlugin"]),
|
||||
|
||||
// MARK: - Networking
|
||||
|
||||
.target(name: "TINetworking",
|
||||
dependencies: ["TIFoundationUtils", "Alamofire", "TILogging"],
|
||||
path: "TINetworking/Sources",
|
||||
plugins: [.plugin(name: "TISwiftLintPlugin")]),
|
||||
|
||||
.target(name: "TIMoyaNetworking",
|
||||
dependencies: ["TINetworking", "Moya"],
|
||||
path: "TIMoyaNetworking/Sources",
|
||||
plugins: [.plugin(name: "TISwiftLintPlugin")]),
|
||||
|
||||
.target(name: "TINetworkingCache",
|
||||
dependencies: ["TINetworking", "Cache"],
|
||||
path: "TINetworkingCache/Sources",
|
||||
plugins: [.plugin(name: "TISwiftLintPlugin")]),
|
||||
|
||||
// MARK: - Maps
|
||||
|
||||
.target(name: "TIMapUtils",
|
||||
dependencies: ["TILogging", "TICoreGraphicsUtils"],
|
||||
path: "TIMapUtils/Sources",
|
||||
plugins: [.plugin(name: "TISwiftLintPlugin")]),
|
||||
|
||||
.target(name: "TIAppleMapUtils",
|
||||
dependencies: ["TIMapUtils"],
|
||||
path: "TIAppleMapUtils/Sources",
|
||||
plugins: [.plugin(name: "TISwiftLintPlugin")]),
|
||||
|
||||
// MARK: - Elements
|
||||
|
||||
.target(name: "OTPSwiftView", dependencies: ["TIUIElements"], path: "OTPSwiftView/Sources"),
|
||||
.target(name: "TITransitions", path: "TITransitions/Sources"),
|
||||
.target(name: "TIPagination", dependencies: ["Cursors", "TISwiftUtils"], path: "TIPagination/Sources"),
|
||||
.target(name: "TIAuth", dependencies: ["TIUIKitCore", "TIKeychainUtils"], path: "TIAuth/Sources"),
|
||||
.target(name: "TIEcommerce", dependencies: ["TIFoundationUtils", "TISwiftUtils", "TINetworking", "TIUIKitCore", "TIUIElements"], path: "TIEcommerce/Sources"),
|
||||
.target(name: "TITextProcessing",
|
||||
dependencies: [.product(name: "Antlr4", package: "antlr4")],
|
||||
path: "TITextProcessing/Sources",
|
||||
exclude: ["../TITextProcessing.app"]),
|
||||
|
||||
.binaryTarget(name: "SwiftLintBinary",
|
||||
url: "https://github.com/realm/SwiftLint/releases/download/0.52.2/SwiftLintBinary-macos.artifactbundle.zip",
|
||||
checksum: "89651e1c87fb62faf076ef785a5b1af7f43570b2b74c6773526e0d5114e0578e"),
|
||||
|
||||
.plugin(name: "TISwiftLintPlugin",
|
||||
capability: .buildTool(),
|
||||
dependencies: ["SwiftLintBinary"]),
|
||||
|
||||
// MARK: - Tests
|
||||
|
||||
.testTarget(
|
||||
name: "TITimerTests",
|
||||
dependencies: ["TIFoundationUtils"],
|
||||
path: "Tests/TITimerTests"),
|
||||
.testTarget(
|
||||
name: "TITextProcessingTests",
|
||||
dependencies: ["TITextProcessing"],
|
||||
path: "Tests/TITextProcessingTests"),
|
||||
.testTarget(
|
||||
name: "TIFoundationUtilsTests",
|
||||
dependencies: ["TIFoundationUtils", "TISwiftUtils", "TILogging"],
|
||||
path: "Tests/TIFoundationUtilsTests")
|
||||
]
|
||||
)
|
||||
|
||||
#endif
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
//
|
||||
// Copyright (c) 2023 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import PackagePlugin
|
||||
import Foundation
|
||||
|
||||
@main
|
||||
struct SwiftLintPlugin: BuildToolPlugin {
|
||||
func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] {
|
||||
let swiftlintScriptPath = context.package.directory.appending(["build-scripts", "xcode", "build_phases", "swiftlint.sh"])
|
||||
|
||||
let swiftlintExecutablePath = try context.tool(named: "swiftlint").path
|
||||
|
||||
let srcRoot = context.package.directory.string
|
||||
let targetDir = target.directory.string
|
||||
|
||||
let relativeTargetDir = targetDir.replacingOccurrences(of: srcRoot, with: "")
|
||||
let clearRelativeTargetDir = relativeTargetDir[relativeTargetDir.index(after: relativeTargetDir.startIndex)...] // trim leading /
|
||||
|
||||
return [
|
||||
.prebuildCommand(displayName: "SwiftLint linting \(target.name)...",
|
||||
executable: swiftlintScriptPath,
|
||||
arguments: [
|
||||
swiftlintExecutablePath,
|
||||
context.package.directory.appending(subpath: "swiftlint_base.yml")
|
||||
],
|
||||
environment: [
|
||||
"SCRIPT_DIR": swiftlintScriptPath.removingLastComponent().string,
|
||||
"SRCROOT": srcRoot,
|
||||
"SCRIPT_INPUT_FILE_COUNT": "1",
|
||||
"SCRIPT_INPUT_FILE_0": clearRelativeTargetDir,
|
||||
// "FORCE_LINT": "1", // Lint all files in target (not only modified)
|
||||
// "AUTOCORRECT": "1"
|
||||
],
|
||||
outputFilesDirectory: context.package.directory)
|
||||
]
|
||||
}
|
||||
}
|
||||
139
README.md
139
README.md
|
|
@ -1,2 +1,139 @@
|
|||
# LeadKit
|
||||
LeadKit it's a iOS framework with a bunch of tools for rapid app development
|
||||
|
||||
LeadKit is the iOS framework with a bunch of tools for rapid app development.
|
||||
|
||||
This repository contains the following frameworks:
|
||||
|
||||
- [TISwiftUtils](TISwiftUtils) - a bunch of useful helpers for Swift development.
|
||||
- [TIFoundationUtils](TIFoundationUtils) - set of helpers for Foundation framework classes.
|
||||
- [TIUIKitCore](TIUIKitCore) - core ui elements and protocols from LeadKit.
|
||||
- [TISwiftUICore](TISwiftUICore) Core UI elements: protocols, views and helpers.
|
||||
- [TIUIElements](TIUIElements) - bunch of of useful protocols and views.
|
||||
- [OTPSwiftView](OTPSwiftView) - a fully customizable OTP view.
|
||||
- [TITableKitUtils](TITableKitUtils) - set of helpers for TableKit classes.
|
||||
- [TIKeychainUtils](TIKeychainUtils) - set of helpers for Keychain classes.
|
||||
- [TIPagination](TIPagination) - realisation of paginating items from a data source.
|
||||
- [TINetworking](TINetworking) - Swagger-frendly networking layer helpers.
|
||||
- [TIMoyaNetworking](TIMoyaNetworking) - Moya + Swagger network service.
|
||||
- [TIAppleMapUtils](TIAppleMapUtils) - set of helpers for map objects clustering and interacting using Apple MapKit.
|
||||
- [TIGoogleMapUtils](TIGoogleMapUtils) - set of helpers for map objects clustering and interacting using Google Maps SDK.
|
||||
- [TIYandexMapUtils](TIYandexMapUtils) - set of helpers for map objects clustering and interacting using Yandex Maps SDK.
|
||||
- [TIAuth](TIAuth) - login, registration, confirmation and other related actions
|
||||
|
||||
## Playgrounds
|
||||
|
||||
### Create new Playground
|
||||
|
||||
```sh
|
||||
$ cd TIModuleName
|
||||
|
||||
$ touch PlaygroundPodfile
|
||||
|
||||
$ echo "ENV[\"DEVELOPMENT_INSTALL\"] = \"true\"
|
||||
|
||||
target 'TIModuleName' do
|
||||
platform :ios, IOS_VERSION_NUMBER
|
||||
use_frameworks!
|
||||
|
||||
pod 'TIDependencyModuleName', :path => '../../../../TIDependencyModuleName/TIDependencyModuleName.podspec'
|
||||
pod 'TIModuleName', :path => '../../../../TIModuleName/TIModuleName.podspec'
|
||||
end" > PlaygroundPodfile
|
||||
|
||||
$ nef playground --name TIModuleName --cocoapods --custom-podfile PlaygroundPodfile
|
||||
```
|
||||
See example of `PlaygroundPodfile` in `TIFoundationUtils`
|
||||
|
||||
|
||||
### Rename/add pages to Playground
|
||||
|
||||
For every new feature in module create new Playground page with documentation in comments. See [nef markdown documentation](https://github.com/bow-swift/nef#-generating-a-markdown-project).
|
||||
|
||||
### Create symlink to nef playground
|
||||
|
||||
```sh
|
||||
$ cd TIModuleName
|
||||
$ ln -s TIModuleName.app/Contents/MacOS/TIModuleName.playground TIModuleName.playground
|
||||
```
|
||||
|
||||
### Add nef files to TIModuleName.app/.gitignore
|
||||
|
||||
```
|
||||
# gitignore nef files
|
||||
**/build/
|
||||
**/nef/
|
||||
LICENSE
|
||||
```
|
||||
|
||||
### Exclude .app bundles from package sources
|
||||
|
||||
#### SPM
|
||||
|
||||
```swift
|
||||
.target(name: "TIModuleName", dependencies: ..., path: ..., exclude: ["TIModuleName.app"]),
|
||||
```
|
||||
|
||||
#### Podspec
|
||||
|
||||
```ruby
|
||||
sources = 'your_sources_expression'
|
||||
if ENV["DEVELOPMENT_INSTALL"] # installing using :path =>
|
||||
s.source_files = sources
|
||||
s.exclude_files = s.name + '.app'
|
||||
else
|
||||
s.source_files = s.name + '/' + sources
|
||||
s.exclude_files = s.name + '/*.app'
|
||||
end
|
||||
```
|
||||
|
||||
## Docs:
|
||||
|
||||
- [TIFoundationUtils](docs/tifoundationutils)
|
||||
* [AsyncOperation](docs/tifoundationutils/asyncoperation.md)
|
||||
- [TICoreGraphicsUtils](docs/ticoregraphicsutils)
|
||||
* [DrawingOperations](docs/ticoregraphicsutils/drawingoperations.md)
|
||||
- [TIKeychainUtils](docs/tikeychainutils)
|
||||
* [SingleValueStorage](docs/tikeychainutils/singlevaluestorage.md)
|
||||
- [TIUIElements](docs/tiuielements)
|
||||
* [Skeletons](docs/tiuielements/skeletons.md)
|
||||
* [Placeholders](docs/tiuielements/placeholder.md)
|
||||
- [TITextProcessing](docs/titextprocessing)
|
||||
* [TITextProcessing](docs/titextprocessing/titextprocessing.md)
|
||||
- [TIDeeplink](docs/tideeplink/deeplinks.md)
|
||||
- [TIBottomSheet](docs/tibottomsheet/tibottomsheet.md)
|
||||
- [Semantic Commit Messages](docs/semantic-commit-messages.md) - commit message codestyle.
|
||||
- [Snippets](docs/snippets.md) - useful commands and scripts for development.
|
||||
|
||||
## Contributing
|
||||
|
||||
- Run following script in framework's folder:
|
||||
```
|
||||
./setup
|
||||
```
|
||||
|
||||
- If legacy [Source](https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/master/Sources) folder needed, [build dependencies for LeadKit.xcodeproj](https://git.svc.touchin.ru/TouchInstinct/LeadKit/blob/master/docs/snippets.md#build-dependencies-for-LeadKit.xcodeproj).
|
||||
|
||||
- Make sure the commit message codestyle is followed. More about [Semantic Commit Messages](docs/semantic-commit-messages.md).
|
||||
|
||||
## Installation
|
||||
|
||||
### SPM
|
||||
|
||||
```swift
|
||||
dependencies: [
|
||||
.package(url: "https://git.svc.touchin.ru/TouchInstinct/LeadKit.git", from: "x.y.z"),
|
||||
],
|
||||
```
|
||||
|
||||
### Cocoapods
|
||||
|
||||
```ruby
|
||||
source 'https://git.svc.touchin.ru/TouchInstinct/Podspecs.git'
|
||||
|
||||
pod 'TISwiftUtils', 'x.y.z'
|
||||
pod 'TIFoundationUtils', 'x.y.z'
|
||||
# ...
|
||||
```
|
||||
|
||||
## Legacy
|
||||
|
||||
Code located in root `Sources` folder and `LeadKit.podspec` should be treated as legacy and shouldn't be used in newly created projects. Please use TI* modules via SPM or CocoaPods.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
//
|
||||
// Copyright (c) 2018 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
/// Base collection controller configurable with view model and CollectionViewWrapperView as custom view.
|
||||
open class BaseCollectionContentController<ViewModel>: BaseScrollContentController<ViewModel, CollectionViewWrapperView> {
|
||||
|
||||
override open func createView() -> CollectionViewWrapperView {
|
||||
CollectionViewWrapperView(layout: UICollectionViewFlowLayout())
|
||||
}
|
||||
|
||||
/// Contained UICollectionView instance.
|
||||
public var collectionView: UICollectionView {
|
||||
customView.collectionView
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
//
|
||||
// Copyright (c) 2018 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import UIKit.UIViewController
|
||||
|
||||
/// Base controller that should be configured with view model.
|
||||
open class BaseConfigurableController<ViewModel>: BaseOrientationController, ConfigurableController {
|
||||
|
||||
/// A view model instance used by this controller.
|
||||
public let viewModel: ViewModel
|
||||
|
||||
/// Initializer with view model parameter.
|
||||
///
|
||||
/// - Parameter viewModel: A view model to configure this controller.
|
||||
public init(viewModel: ViewModel) {
|
||||
self.viewModel = viewModel
|
||||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
required public init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
// MARK: - ConfigurableController
|
||||
|
||||
open func addViews() {
|
||||
// override in subclass
|
||||
}
|
||||
|
||||
open func configureLayout() {
|
||||
// override in subclass
|
||||
}
|
||||
|
||||
open func bindViews() {
|
||||
// override in subclass
|
||||
}
|
||||
|
||||
open func configureAppearance() {
|
||||
// override in subclass
|
||||
}
|
||||
|
||||
open func localize() {
|
||||
// override in subclass
|
||||
}
|
||||
|
||||
open func configureBarButtons() {
|
||||
// override in subclass
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
//
|
||||
// Copyright (c) 2018 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import UIKit.UIView
|
||||
|
||||
/// Base controller configurable by view model and custom view.
|
||||
open class BaseCustomViewController<ViewModel, View: UIView>: BaseConfigurableController<ViewModel> {
|
||||
|
||||
/// Contained custom view.
|
||||
public private(set) lazy var customView = createView()
|
||||
|
||||
/// Initializer with view model and custom view parameters.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - viewModel: A view model to configure this controller.
|
||||
/// - customView: UIView instance to assign in view property.
|
||||
public override init(viewModel: ViewModel) {
|
||||
super.init(viewModel: viewModel)
|
||||
}
|
||||
|
||||
required public init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override open func loadView() {
|
||||
view = customView
|
||||
}
|
||||
|
||||
/// Creates custom view.
|
||||
///
|
||||
/// - Returns: Initialized custom view.
|
||||
open func createView() -> View {
|
||||
View()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import Foundation
|
||||
|
||||
open class BaseOrientationController: UIViewController {
|
||||
|
||||
/// Ability to set forced screen orientation
|
||||
open var forcedInterfaceOrientation: UIInterfaceOrientation?
|
||||
|
||||
open override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
|
||||
switch forcedInterfaceOrientation {
|
||||
case .landscapeLeft:
|
||||
return .landscapeLeft
|
||||
|
||||
case .landscapeRight:
|
||||
return .landscapeRight
|
||||
|
||||
case .portrait:
|
||||
return .portrait
|
||||
|
||||
case .portraitUpsideDown:
|
||||
return .portraitUpsideDown
|
||||
|
||||
default:
|
||||
return super.supportedInterfaceOrientations
|
||||
}
|
||||
}
|
||||
|
||||
open override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation {
|
||||
forcedInterfaceOrientation ?? super.preferredInterfaceOrientationForPresentation
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
import UIKit
|
||||
|
||||
open class OrientationNavigationController: UINavigationController {
|
||||
|
||||
// MARK: - Public properties
|
||||
|
||||
open var presentedOrTopViewController: UIViewController? {
|
||||
presentedViewController ?? topViewController
|
||||
}
|
||||
|
||||
open override var shouldAutorotate: Bool {
|
||||
presentedOrTopViewController?.shouldAutorotate
|
||||
?? super.shouldAutorotate
|
||||
}
|
||||
|
||||
open override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
|
||||
presentedOrTopViewController?.supportedInterfaceOrientations
|
||||
?? super.supportedInterfaceOrientations
|
||||
}
|
||||
|
||||
open override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation {
|
||||
presentedOrTopViewController?.preferredInterfaceOrientationForPresentation
|
||||
?? super.preferredInterfaceOrientationForPresentation
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
//
|
||||
// Copyright (c) 2018 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import RxSwift
|
||||
import RxCocoa
|
||||
|
||||
public typealias ScrollViewHolderView = UIView & ScrollViewHolder
|
||||
|
||||
/// Base controller configurable with view model and ScrollViewHolder custom view.
|
||||
open class BaseScrollContentController<ViewModel, View: ScrollViewHolderView>: BaseCustomViewController<ViewModel, View> {
|
||||
|
||||
private var bottomInsetDisposable: Disposable?
|
||||
private let defaultInsetsRelay = BehaviorRelay<UIEdgeInsets>(value: .zero)
|
||||
|
||||
/// Bind given driver to bottom inset of scroll view. Takes into account default bottom insets.
|
||||
///
|
||||
/// - Parameter bottomInsetDriver: Driver that emits CGFloat bottom inset changes.
|
||||
public func bindBottomInsetBinding(from bottomInsetDriver: Driver<CGFloat>) {
|
||||
bottomInsetDisposable = bottomInsetDriver
|
||||
.withLatestFrom(defaultInsetsRelay.asDriver()) {
|
||||
$0 + $1.bottom
|
||||
}
|
||||
.drive(customView.scrollView.rx.bottomInsetBinder)
|
||||
}
|
||||
|
||||
/// Unbind scroll view from previous binding.
|
||||
public func unbindBottomInsetBinding() {
|
||||
bottomInsetDisposable?.dispose()
|
||||
}
|
||||
|
||||
/// Contained UIScrollView instance.
|
||||
public var scrollView: UIScrollView {
|
||||
customView.scrollView
|
||||
}
|
||||
|
||||
/// Default insets used for contained scroll view.
|
||||
public var defaultInsets: UIEdgeInsets {
|
||||
get {
|
||||
defaultInsetsRelay.value
|
||||
}
|
||||
set {
|
||||
defaultInsetsRelay.accept(newValue)
|
||||
customView.scrollView.contentInset = newValue
|
||||
customView.scrollView.scrollIndicatorInsets = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension BaseScrollContentController {
|
||||
|
||||
/// On iOS, tvOS 11+ sets contentInsetAdjustmentBehavior to .never.
|
||||
/// On earlier versions sets automaticallyAdjustsScrollViewInsets to false.
|
||||
func disableAdjustsScrollViewInsets() {
|
||||
if #available(iOS 11.0, tvOS 11.0, *) {
|
||||
customView.scrollView.contentInsetAdjustmentBehavior = .never
|
||||
} else {
|
||||
automaticallyAdjustsScrollViewInsets = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
//
|
||||
// Copyright (c) 2018 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import TableKit
|
||||
|
||||
/// Base table controller configurable with view model and TableViewWrapperView as custom view.
|
||||
open class BaseTableContentController<ViewModel>: BaseScrollContentController<ViewModel, TableViewWrapperView> {
|
||||
|
||||
/// TableDirector binded to table view.
|
||||
public private(set) lazy var tableDirector = createTableDirector()
|
||||
|
||||
/// Creates tableDirector for table view.
|
||||
///
|
||||
/// - Returns: Initialized TableDirector.
|
||||
open func createTableDirector() -> TableDirector {
|
||||
TableDirector(tableView: tableView)
|
||||
}
|
||||
|
||||
override open func createView() -> TableViewWrapperView {
|
||||
TableViewWrapperView(tableViewStyle: .plain)
|
||||
}
|
||||
|
||||
override open func configureAppearance() {
|
||||
super.configureAppearance()
|
||||
|
||||
tableView.separatorStyle = .none
|
||||
}
|
||||
|
||||
/// Contained UITableView instance.
|
||||
public var tableView: UITableView {
|
||||
customView.tableView
|
||||
}
|
||||
}
|
||||
|
|
@ -23,13 +23,13 @@
|
|||
import RxSwift
|
||||
|
||||
/// Paging cursor implementation with enclosed cursor for fetching results
|
||||
public class FixedPageCursor<Cursor: CursorType>: CursorType where Cursor.LoadResultType == CountableRange<Int> {
|
||||
public class FixedPageCursor<Cursor: CursorType>: CursorType, RxDataSource {
|
||||
|
||||
public typealias LoadResultType = CountableRange<Int>
|
||||
public typealias ResultType = [Element]
|
||||
|
||||
private let cursor: Cursor
|
||||
fileprivate let cursor: Cursor
|
||||
|
||||
private let pageSize: Int
|
||||
fileprivate let pageSize: Int
|
||||
|
||||
/// Initializer with enclosed cursor
|
||||
///
|
||||
|
|
@ -42,19 +42,19 @@ public class FixedPageCursor<Cursor: CursorType>: CursorType where Cursor.LoadRe
|
|||
}
|
||||
|
||||
public var exhausted: Bool {
|
||||
return cursor.exhausted && cursor.count == count
|
||||
cursor.exhausted && cursor.count == count
|
||||
}
|
||||
|
||||
public private(set) var count: Int = 0
|
||||
|
||||
public subscript(index: Int) -> Cursor.Element {
|
||||
return cursor[index]
|
||||
cursor[index]
|
||||
}
|
||||
|
||||
public func loadNextBatch() -> Observable<LoadResultType> {
|
||||
return Observable.deferred {
|
||||
public func loadNextBatch() -> Single<[Cursor.Element]> {
|
||||
Single.deferred {
|
||||
if self.exhausted {
|
||||
throw CursorError.exhausted
|
||||
return .error(CursorError.exhausted)
|
||||
}
|
||||
|
||||
let restOfLoaded = self.cursor.count - self.count
|
||||
|
|
@ -63,12 +63,27 @@ public class FixedPageCursor<Cursor: CursorType>: CursorType where Cursor.LoadRe
|
|||
let startIndex = self.count
|
||||
self.count += min(restOfLoaded, self.pageSize)
|
||||
|
||||
return Observable.just(startIndex..<self.count)
|
||||
return .just(self.cursor[startIndex..<self.count])
|
||||
}
|
||||
|
||||
return self.cursor.loadNextBatch()
|
||||
.flatMap { _ in self.loadNextBatch() }
|
||||
.flatMap { _ in
|
||||
self.loadNextBatch()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// FixedPageCursor subclass with implementation of ResettableType
|
||||
public class ResettableFixedPageCursor<Cursor: ResettableCursorType>: FixedPageCursor<Cursor>, ResettableType {
|
||||
|
||||
public typealias ResultType = [Element]
|
||||
|
||||
public override init(cursor: Cursor, pageSize: Int) {
|
||||
super.init(cursor: cursor, pageSize: pageSize)
|
||||
}
|
||||
|
||||
public required init(resetFrom other: ResettableFixedPageCursor) {
|
||||
super.init(cursor: other.cursor.reset(), pageSize: other.pageSize)
|
||||
}
|
||||
}
|
||||
|
|
@ -22,30 +22,37 @@
|
|||
|
||||
import RxSwift
|
||||
|
||||
public typealias MapCursorLoadResultType = CountableRange<Int>
|
||||
|
||||
public extension CursorType where Self.LoadResultType == MapCursorLoadResultType {
|
||||
public extension CursorType {
|
||||
|
||||
/// Creates MapCursor with current cursor
|
||||
///
|
||||
/// - Parameter transform: closure to transform elements
|
||||
/// - Returns: new MapCursorInstance
|
||||
/// - Returns: new MapCursor instance
|
||||
func flatMap<T>(transform: @escaping MapCursor<Self, T>.Transform) -> MapCursor<Self, T> {
|
||||
return MapCursor(cursor: self, transform: transform)
|
||||
MapCursor(cursor: self, transform: transform)
|
||||
}
|
||||
|
||||
/// Creates ResettableMapCursor with current cursor
|
||||
///
|
||||
/// - Parameter transform: closure to transform elements
|
||||
/// - Returns: new ResettableMapCursor instance
|
||||
func flatMap<T>(transform: @escaping ResettableMapCursor<Self, T>.Transform)
|
||||
-> ResettableMapCursor<Self, T> where Self: ResettableCursorType {
|
||||
|
||||
ResettableMapCursor(cursor: self, transform: transform)
|
||||
}
|
||||
}
|
||||
|
||||
/// Map cursor implementation with enclosed cursor for fetching results
|
||||
public class MapCursor<Cursor: CursorType, T>: CursorType where Cursor.LoadResultType == MapCursorLoadResultType {
|
||||
public class MapCursor<Cursor: CursorType, T>: CursorType, RxDataSource {
|
||||
|
||||
public typealias LoadResultType = Cursor.LoadResultType
|
||||
public typealias ResultType = [Element]
|
||||
|
||||
public typealias Transform = (Cursor.Element) -> T?
|
||||
|
||||
private let cursor: Cursor
|
||||
fileprivate let cursor: Cursor
|
||||
|
||||
private let transform: Transform
|
||||
fileprivate let transform: Transform
|
||||
|
||||
private var elements: [T] = []
|
||||
|
||||
|
|
@ -60,24 +67,37 @@ public class MapCursor<Cursor: CursorType, T>: CursorType where Cursor.LoadResul
|
|||
}
|
||||
|
||||
public var exhausted: Bool {
|
||||
return cursor.exhausted
|
||||
cursor.exhausted
|
||||
}
|
||||
|
||||
public var count: Int {
|
||||
return elements.count
|
||||
elements.count
|
||||
}
|
||||
|
||||
public subscript(index: Int) -> T {
|
||||
return elements[index]
|
||||
elements[index]
|
||||
}
|
||||
|
||||
public func loadNextBatch() -> Observable<LoadResultType> {
|
||||
return cursor.loadNextBatch().map { loadedRange in
|
||||
let startIndex = self.elements.count
|
||||
self.elements += self.cursor[loadedRange].flatMap(self.transform)
|
||||
public func loadNextBatch() -> Single<[T]> {
|
||||
cursor.loadNextBatch().map { newItems in
|
||||
let transformedNewItems = newItems.compactMap(self.transform)
|
||||
self.elements += transformedNewItems
|
||||
|
||||
return startIndex..<self.elements.count
|
||||
return transformedNewItems
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// MapCursor subclass with implementation of ResettableType
|
||||
public class ResettableMapCursor<Cursor: ResettableCursorType, T>: MapCursor<Cursor, T>, ResettableType {
|
||||
|
||||
public typealias ResultType = [Cursor.Element]
|
||||
|
||||
public override init(cursor: Cursor, transform: @escaping Transform) {
|
||||
super.init(cursor: cursor, transform: transform)
|
||||
}
|
||||
|
||||
public required init(resetFrom other: ResettableMapCursor) {
|
||||
super.init(cursor: other.cursor.reset(), transform: other.transform)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
//
|
||||
// Copyright (c) 2017 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import RxSwift
|
||||
|
||||
/// Single load cursor configuration for single load operation
|
||||
public final class SingleLoadCursorConfiguration<Element>: TotalCountCursorConfiguration {
|
||||
|
||||
public typealias ResultType = [Element]
|
||||
|
||||
private let loadingSingle: Single<ResultType>
|
||||
|
||||
/// Initializer for Single with array result type.
|
||||
///
|
||||
/// - Parameter loadingSingle: Single that will emit array of result type.
|
||||
public init(loadingSingle: Single<ResultType>) {
|
||||
self.loadingSingle = loadingSingle
|
||||
}
|
||||
|
||||
public func resultSingle() -> Single<ResultType> {
|
||||
loadingSingle
|
||||
}
|
||||
|
||||
public init(resetFrom other: SingleLoadCursorConfiguration) {
|
||||
self.loadingSingle = other.loadingSingle
|
||||
}
|
||||
}
|
||||
|
||||
/// Cursor implementation for single load operation
|
||||
@available(*, deprecated, message: "Use SingleLoadCursorConfiguration with TotalCountCursor.")
|
||||
public class SingleLoadCursor<Element>: ResettableCursorType {
|
||||
|
||||
private let loadingObservable: Single<[Element]>
|
||||
|
||||
private var content: [Element] = []
|
||||
|
||||
/// Initializer for array content type
|
||||
///
|
||||
/// - Parameter loadingObservable: Single observable with element of [Element] type
|
||||
public init(loadingObservable: Single<[Element]>) {
|
||||
self.loadingObservable = loadingObservable
|
||||
}
|
||||
|
||||
public required init(resetFrom other: SingleLoadCursor) {
|
||||
self.loadingObservable = other.loadingObservable
|
||||
}
|
||||
|
||||
public private(set) var exhausted = false
|
||||
|
||||
public var count: Int {
|
||||
content.count
|
||||
}
|
||||
|
||||
public subscript(index: Int) -> Element {
|
||||
content[index]
|
||||
}
|
||||
|
||||
public func loadNextBatch() -> Single<[Element]> {
|
||||
Single.deferred {
|
||||
if self.exhausted {
|
||||
return .error(CursorError.exhausted)
|
||||
}
|
||||
|
||||
return self.loadingObservable.do(onSuccess: { [weak self] newItems in
|
||||
self?.onGot(result: newItems)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private func onGot(result: [Element]) {
|
||||
content = result
|
||||
exhausted = true
|
||||
}
|
||||
}
|
||||
|
|
@ -23,39 +23,42 @@
|
|||
import RxSwift
|
||||
|
||||
/// Stub cursor implementation for array content type
|
||||
public class StaticCursor<Element>: CursorType {
|
||||
public class StaticCursor<Element>: ResettableRxDataSourceCursor {
|
||||
|
||||
public typealias LoadResultType = CountableRange<Int>
|
||||
public typealias ResultType = [Element]
|
||||
|
||||
private let content: [Element]
|
||||
|
||||
/// Initializer for array content type
|
||||
///
|
||||
/// - Parameter content: array with elements of Elemet type
|
||||
/// - Parameter content: array with elements of Element type
|
||||
public init(content: [Element]) {
|
||||
self.content = content
|
||||
}
|
||||
|
||||
public required init(resetFrom other: StaticCursor) {
|
||||
self.content = other.content
|
||||
}
|
||||
|
||||
public private(set) var exhausted = false
|
||||
|
||||
public private(set) var count = 0
|
||||
|
||||
public subscript(index: Int) -> Element {
|
||||
return content[index]
|
||||
content[index]
|
||||
}
|
||||
|
||||
public func loadNextBatch() -> Observable<LoadResultType> {
|
||||
return Observable.deferred {
|
||||
public func loadNextBatch() -> Single<[Element]> {
|
||||
Single.deferred {
|
||||
if self.exhausted {
|
||||
throw CursorError.exhausted
|
||||
return .error(CursorError.exhausted)
|
||||
}
|
||||
|
||||
self.count = self.content.count
|
||||
|
||||
self.exhausted = true
|
||||
|
||||
return Observable.just(0..<self.count)
|
||||
return .just(self.content)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
//
|
||||
// Copyright (c) 2018 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import RxSwift
|
||||
import RxCocoa
|
||||
|
||||
open class TotalCountCursor<CursorConfiguration: TotalCountCursorConfiguration>: ResettableRxDataSourceCursor {
|
||||
|
||||
public typealias Element = CursorConfiguration.ResultType.ElementType
|
||||
public typealias ResultType = [Element]
|
||||
|
||||
private let configuration: CursorConfiguration
|
||||
|
||||
private var elements: [Element] = []
|
||||
|
||||
public private(set) var totalCount: Int = .max
|
||||
|
||||
public var exhausted: Bool {
|
||||
count >= totalCount
|
||||
}
|
||||
|
||||
public var count: Int {
|
||||
elements.count
|
||||
}
|
||||
|
||||
public subscript(index: Int) -> Element {
|
||||
elements[index]
|
||||
}
|
||||
|
||||
public init(configuration: CursorConfiguration) {
|
||||
self.configuration = configuration
|
||||
}
|
||||
|
||||
public required init(resetFrom other: TotalCountCursor) {
|
||||
configuration = other.configuration.reset()
|
||||
}
|
||||
|
||||
open func processResultFromConfigurationSingle() -> Single<CursorConfiguration.ResultType> {
|
||||
configuration.resultSingle()
|
||||
}
|
||||
|
||||
public func loadNextBatch() -> Single<[Element]> {
|
||||
processResultFromConfigurationSingle()
|
||||
.do(onSuccess: { [weak self] listingResult in
|
||||
self?.totalCount = listingResult.totalCount
|
||||
self?.elements = (self?.elements ?? []) + listingResult.results
|
||||
})
|
||||
.map {
|
||||
$0.results
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
//
|
||||
// Copyright (c) 2018 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import RxSwift
|
||||
|
||||
/// Data loading model for GeneralDataLoadingState with Single as data source.
|
||||
public final class GeneralDataLoadingModel<T>: RxDataLoadingModel<GeneralDataLoadingState<Single<T>>> {
|
||||
}
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
//
|
||||
// Copyright (c) 2018 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import RxSwift
|
||||
import RxCocoa
|
||||
|
||||
/// ViewModel that loads data from given data source with loading state tracking.
|
||||
open class GeneralDataLoadingViewModel<ResultType>: BaseViewModel, GeneralDataLoadingHandler, DisposeBagHolder {
|
||||
|
||||
public typealias LoadingModel = GeneralDataLoadingModel<ResultType>
|
||||
public typealias DataSourceType = Single<ResultType>
|
||||
public typealias LoadingState = GeneralDataLoadingState<DataSourceType>
|
||||
|
||||
private let loadingModel: LoadingModel
|
||||
|
||||
private let loadingStateRelay = BehaviorRelay<LoadingState>(value: .initial)
|
||||
|
||||
// MARK: - DisposeBagHolder
|
||||
|
||||
public let disposeBag = DisposeBag()
|
||||
|
||||
/// Initializer with single result sequence and empty result checker closure.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - dataSource: A single element sequence.
|
||||
/// - customErrorHandler: Custom error handler for state update. Pass nil for default error handling.
|
||||
/// - emptyResultChecker: Closure for checking result on empty state.
|
||||
public init(dataSource: DataSourceType,
|
||||
customErrorHandler: LoadingModel.ErrorHandler? = nil,
|
||||
emptyResultChecker: @escaping LoadingModel.EmptyResultChecker = { _ in false }) {
|
||||
|
||||
loadingModel = LoadingModel(dataSource: dataSource,
|
||||
customErrorHandler: customErrorHandler,
|
||||
emptyResultChecker: emptyResultChecker)
|
||||
|
||||
loadingModel.stateDriver
|
||||
.drive(loadingStateRelay)
|
||||
.disposed(by: disposeBag)
|
||||
|
||||
bindLoadingState(from: loadingStateDriver)
|
||||
|
||||
loadingModel.reload()
|
||||
}
|
||||
|
||||
/// Returns observable that emits current loading state.
|
||||
open var loadingStateObservable: Observable<LoadingState> {
|
||||
loadingStateRelay.asObservable()
|
||||
}
|
||||
|
||||
/// Returns driver that emits current loading state.
|
||||
open var loadingStateDriver: Driver<LoadingState> {
|
||||
loadingStateRelay.asDriver()
|
||||
}
|
||||
|
||||
/// By default returns true if loading state == .result.
|
||||
open var hasContent: Bool {
|
||||
currentLoadingState.hasResult
|
||||
}
|
||||
|
||||
/// Returns current result if it exists.
|
||||
public var currentResult: ResultType? {
|
||||
currentLoadingState.result
|
||||
}
|
||||
|
||||
/// Current state of loading process.
|
||||
private(set) public var currentLoadingState: LoadingState {
|
||||
get {
|
||||
loadingStateRelay.value
|
||||
}
|
||||
set {
|
||||
loadingStateRelay.accept(newValue)
|
||||
}
|
||||
}
|
||||
|
||||
/// Manually update loading state.
|
||||
/// Should be used only in specific situations on your own risk!
|
||||
///
|
||||
/// - Parameter newState: New loading state.
|
||||
public func updateStateManually(to newState: LoadingState) {
|
||||
currentLoadingState = newState
|
||||
}
|
||||
|
||||
/// Replaces current data source of loading model with new one.
|
||||
///
|
||||
/// - Parameter dataSource: A single element sequence.
|
||||
public func replaceDataSource(with newDataSource: DataSourceType) {
|
||||
loadingModel.replaceDataSource(with: newDataSource)
|
||||
}
|
||||
|
||||
/// Reload data.
|
||||
public func reload() {
|
||||
loadingModel.reload()
|
||||
}
|
||||
|
||||
// MARK: - GeneralDataLoadingHandler
|
||||
|
||||
open func onLoadingState() {
|
||||
// override in subclass
|
||||
}
|
||||
|
||||
open func onResultsState(result: ResultType) {
|
||||
// override in subclass
|
||||
}
|
||||
|
||||
open func onEmptyState() {
|
||||
// override in subclass
|
||||
}
|
||||
|
||||
open func onErrorState(error: Error) {
|
||||
// override in subclass
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
//
|
||||
// Copyright (c) 2018 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import RxSwift
|
||||
|
||||
public typealias RxPaginationDataLoadingModel<Cursor: ResettableRxDataSourceCursor> =
|
||||
RxDataLoadingModel<PaginationDataLoadingState<Cursor>>
|
||||
|
||||
/// Data loading model for PaginationDataLoadingState with ResettableRxDataSourceCursor as data source.
|
||||
public final class PaginationDataLoadingModel<Cursor: ResettableRxDataSourceCursor>: RxPaginationDataLoadingModel<Cursor> {
|
||||
|
||||
private enum LoadType {
|
||||
|
||||
case reload
|
||||
case retry
|
||||
case next
|
||||
}
|
||||
|
||||
override public func reload() {
|
||||
load(.reload)
|
||||
}
|
||||
|
||||
/// Attempt to load data again.
|
||||
public func retry() {
|
||||
load(.retry)
|
||||
}
|
||||
|
||||
public func loadMore() {
|
||||
load(.next)
|
||||
}
|
||||
|
||||
private func load(_ loadType: LoadType) {
|
||||
currentRequestDisposable?.dispose()
|
||||
|
||||
switch loadType {
|
||||
case .reload, .retry:
|
||||
replaceDataSource(with: dataSource.reset())
|
||||
|
||||
if loadType == .retry {
|
||||
state = .initial
|
||||
}
|
||||
|
||||
state = .initialLoading(after: state)
|
||||
|
||||
case .next:
|
||||
if case .exhausted = state {
|
||||
fatalError("You shouldn't call load(.next) after got .exhausted state!")
|
||||
}
|
||||
|
||||
state = .loadingMore(after: state)
|
||||
}
|
||||
|
||||
requestResult(from: dataSource)
|
||||
}
|
||||
|
||||
override func onGot(error: Error) {
|
||||
if case .exhausted? = error as? CursorError, case .initialLoading(let after) = state {
|
||||
switch after {
|
||||
case .initial, .empty: // cursor exhausted after creation
|
||||
state = .empty
|
||||
|
||||
default:
|
||||
super.onGot(error: error)
|
||||
}
|
||||
} else {
|
||||
super.onGot(error: error)
|
||||
}
|
||||
}
|
||||
|
||||
override func updateStateAfterResult(from dataSource: DataSourceType) {
|
||||
if dataSource.exhausted {
|
||||
state = .exhausted
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,361 @@
|
|||
//
|
||||
// Copyright (c) 2018 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import RxSwift
|
||||
import RxCocoa
|
||||
import UIScrollView_InfiniteScroll
|
||||
|
||||
/// Class that connects PaginationDataLoadingModel with UIScrollView. It handles all non-visual and visual states.
|
||||
final public class PaginationWrapper<Cursor: ResettableRxDataSourceCursor, Delegate: PaginationWrapperDelegate>
|
||||
where Cursor == Delegate.DataSourceType, Cursor.ResultType == [Cursor.Element] {
|
||||
|
||||
private typealias DataLoadingModel = PaginationDataLoadingModel<Cursor>
|
||||
|
||||
private typealias LoadingState = DataLoadingModel.NetworkOperationStateType
|
||||
|
||||
private typealias FinishInfiniteScrollCompletion = ((UIScrollView) -> Void)
|
||||
|
||||
private var wrappedView: AnyPaginationWrappable
|
||||
private let paginationViewModel: DataLoadingModel
|
||||
private weak var delegate: Delegate?
|
||||
private weak var uiDelegate: PaginationWrapperUIDelegate?
|
||||
|
||||
/// Sets the offset between the real end of the scroll view content and the scroll position,
|
||||
/// so the handler can be triggered before reaching end. Defaults to 0.0;
|
||||
public var infiniteScrollTriggerOffset: CGFloat {
|
||||
get {
|
||||
wrappedView.scrollView.infiniteScrollTriggerOffset
|
||||
}
|
||||
set {
|
||||
wrappedView.scrollView.infiniteScrollTriggerOffset = newValue
|
||||
}
|
||||
}
|
||||
|
||||
public var pullToRefreshEnabled: Bool = true {
|
||||
didSet {
|
||||
if pullToRefreshEnabled {
|
||||
createRefreshControl()
|
||||
} else {
|
||||
removeRefreshControl()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var bottom: CGFloat {
|
||||
wrappedView.scrollView.contentSize.height - wrappedView.scrollView.frame.size.height
|
||||
}
|
||||
|
||||
private let disposeBag = DisposeBag()
|
||||
|
||||
private var currentPlaceholderView: UIView?
|
||||
private var currentPlaceholderViewTopConstraint: NSLayoutConstraint?
|
||||
|
||||
/// Initializer with table view, placeholders container view, cusor and delegate parameters.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - wrappedView: UIScrollView instance to work with.
|
||||
/// - cursor: Cursor object that acts as data source.
|
||||
/// - delegate: Delegate object for data loading events handling.
|
||||
/// - uiDelegate: Delegate object for UI customization.
|
||||
public init(wrappedView: AnyPaginationWrappable,
|
||||
cursor: Cursor,
|
||||
delegate: Delegate,
|
||||
uiDelegate: PaginationWrapperUIDelegate? = nil) {
|
||||
self.wrappedView = wrappedView
|
||||
self.delegate = delegate
|
||||
self.uiDelegate = uiDelegate
|
||||
|
||||
self.paginationViewModel = PaginationDataLoadingModel(dataSource: cursor) { $0.isEmpty }
|
||||
|
||||
bindViewModelStates()
|
||||
|
||||
createRefreshControl()
|
||||
}
|
||||
|
||||
/// Method that reload all data in internal view model.
|
||||
public func reload() {
|
||||
paginationViewModel.reload()
|
||||
}
|
||||
|
||||
/// Method acts like reload, but shows initial loading view after being invoked.
|
||||
public func retry() {
|
||||
paginationViewModel.retry()
|
||||
}
|
||||
|
||||
/// Method that enables placeholders animation due pull-to-refresh interaction.
|
||||
///
|
||||
/// - Parameter scrollObservable: Observable that emits content offset as CGPoint.
|
||||
public func setScrollObservable(_ scrollObservable: Observable<CGPoint>) {
|
||||
scrollObservable
|
||||
.asDriver(onErrorJustReturn: .zero)
|
||||
.drive(scrollOffsetChanged)
|
||||
.disposed(by: disposeBag)
|
||||
}
|
||||
|
||||
// MARK: - States handling
|
||||
|
||||
private func onInitialState() {
|
||||
//
|
||||
}
|
||||
|
||||
private func onLoadingState(afterState: LoadingState) {
|
||||
if case .initial = afterState {
|
||||
wrappedView.scrollView.isUserInteractionEnabled = false
|
||||
|
||||
removeAllPlaceholderView()
|
||||
|
||||
guard let loadingIndicator = uiDelegate?.initialLoadingIndicator() else {
|
||||
return
|
||||
}
|
||||
|
||||
let loadingIndicatorView = loadingIndicator.view
|
||||
|
||||
loadingIndicatorView.translatesAutoresizingMaskIntoConstraints = true
|
||||
|
||||
wrappedView.backgroundView = loadingIndicatorView
|
||||
|
||||
loadingIndicator.startAnimating()
|
||||
|
||||
currentPlaceholderView = loadingIndicatorView
|
||||
} else {
|
||||
removeInfiniteScroll()
|
||||
wrappedView.footerView = nil
|
||||
}
|
||||
}
|
||||
|
||||
private func onLoadingMoreState(afterState: LoadingState) {
|
||||
if case .error = afterState { // user tap retry button in table footer
|
||||
uiDelegate?.footerRetryViewWillDisappear()
|
||||
wrappedView.footerView = nil
|
||||
addInfiniteScroll(withHandler: false)
|
||||
wrappedView.scrollView.beginInfiniteScroll(true)
|
||||
}
|
||||
}
|
||||
|
||||
private func onResultsState(newItems: DataLoadingModel.ResultType,
|
||||
from cursor: Cursor,
|
||||
afterState: LoadingState) {
|
||||
|
||||
wrappedView.scrollView.isUserInteractionEnabled = true
|
||||
|
||||
if case .initialLoading = afterState {
|
||||
delegate?.paginationWrapper(didReload: newItems, using: cursor)
|
||||
|
||||
removeAllPlaceholderView()
|
||||
|
||||
wrappedView.scrollView.refreshControl?.endRefreshing()
|
||||
|
||||
addInfiniteScroll(withHandler: true)
|
||||
} else if case .loadingMore = afterState {
|
||||
delegate?.paginationWrapper(didLoad: newItems, using: cursor)
|
||||
|
||||
removeAllPlaceholderView()
|
||||
addInfiniteScrollWithHandler()
|
||||
}
|
||||
}
|
||||
|
||||
private func onErrorState(error: Error, afterState: LoadingState) {
|
||||
if case .initialLoading = afterState {
|
||||
defer {
|
||||
wrappedView.scrollView.refreshControl?.endRefreshing()
|
||||
}
|
||||
|
||||
delegate?.clearData()
|
||||
|
||||
let customErrorHandling = uiDelegate?.customInitialLoadingErrorHandling(for: error) ?? false
|
||||
guard !customErrorHandling, let errorView = uiDelegate?.errorPlaceholder(for: error) else {
|
||||
return
|
||||
}
|
||||
|
||||
replacePlaceholderViewIfNeeded(with: errorView)
|
||||
} else {
|
||||
guard let retryView = uiDelegate?.footerRetryView(),
|
||||
let retryViewHeight = uiDelegate?.footerRetryViewHeight() else {
|
||||
removeInfiniteScroll()
|
||||
return
|
||||
}
|
||||
|
||||
retryView.frame = CGRect(x: 0, y: 0, width: wrappedView.scrollView.bounds.width, height: retryViewHeight)
|
||||
retryView.button.addTarget(self, action: #selector(retryEvent), for: .touchUpInside)
|
||||
|
||||
uiDelegate?.footerRetryViewWillAppear()
|
||||
|
||||
removeInfiniteScroll { scrollView in
|
||||
self.wrappedView.footerView = retryView
|
||||
|
||||
let shouldUpdateContentOffset = Int(scrollView.contentOffset.y + retryViewHeight) >= Int(self.bottom)
|
||||
|
||||
if shouldUpdateContentOffset {
|
||||
let newContentOffset = CGPoint(x: 0, y: scrollView.contentOffset.y + retryViewHeight)
|
||||
scrollView.setContentOffset(newContentOffset, animated: true)
|
||||
|
||||
if #available(iOS 13, *) {
|
||||
scrollView.setContentOffset(newContentOffset, animated: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func retryEvent() {
|
||||
paginationViewModel.loadMore()
|
||||
}
|
||||
|
||||
private func onEmptyState() {
|
||||
defer {
|
||||
wrappedView.scrollView.refreshControl?.endRefreshing()
|
||||
}
|
||||
|
||||
delegate?.clearData()
|
||||
|
||||
guard let emptyView = uiDelegate?.emptyPlaceholder() else {
|
||||
return
|
||||
}
|
||||
|
||||
replacePlaceholderViewIfNeeded(with: emptyView)
|
||||
}
|
||||
|
||||
private func replacePlaceholderViewIfNeeded(with placeholderView: UIView) {
|
||||
wrappedView.scrollView.isUserInteractionEnabled = true
|
||||
removeAllPlaceholderView()
|
||||
|
||||
placeholderView.translatesAutoresizingMaskIntoConstraints = false
|
||||
placeholderView.isHidden = false
|
||||
|
||||
// I was unable to add pull-to-refresh placeholder scroll behaviour without this trick
|
||||
let placeholderWrapperView = UIView()
|
||||
placeholderWrapperView.addSubview(placeholderView)
|
||||
|
||||
let leadingConstraint = placeholderView.leadingAnchor.constraint(equalTo: placeholderWrapperView.leadingAnchor)
|
||||
let trailingConstraint = placeholderView.trailingAnchor.constraint(equalTo: placeholderWrapperView.trailingAnchor)
|
||||
let topConstraint = placeholderView.topAnchor.constraint(equalTo: placeholderWrapperView.topAnchor)
|
||||
let bottomConstraint = placeholderView.bottomAnchor.constraint(equalTo: placeholderWrapperView.bottomAnchor)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
leadingConstraint,
|
||||
trailingConstraint,
|
||||
topConstraint,
|
||||
bottomConstraint
|
||||
])
|
||||
|
||||
currentPlaceholderViewTopConstraint = topConstraint
|
||||
|
||||
wrappedView.backgroundView = placeholderWrapperView
|
||||
|
||||
currentPlaceholderView = placeholderView
|
||||
}
|
||||
|
||||
// MARK: - private stuff
|
||||
|
||||
private func onExhaustedState() {
|
||||
removeInfiniteScroll()
|
||||
removeAllPlaceholderView()
|
||||
}
|
||||
|
||||
private func addInfiniteScrollWithHandler() {
|
||||
removeInfiniteScroll()
|
||||
addInfiniteScroll(withHandler: true)
|
||||
}
|
||||
|
||||
private func addInfiniteScroll(withHandler: Bool) {
|
||||
if withHandler {
|
||||
wrappedView.scrollView.addInfiniteScroll { [weak paginationViewModel] _ in
|
||||
paginationViewModel?.loadMore()
|
||||
}
|
||||
} else {
|
||||
wrappedView.scrollView.addInfiniteScroll { _ in }
|
||||
}
|
||||
|
||||
wrappedView.scrollView.infiniteScrollIndicatorView = uiDelegate?.loadingMoreIndicator()?.view
|
||||
}
|
||||
|
||||
private func removeInfiniteScroll(with completion: FinishInfiniteScrollCompletion? = nil) {
|
||||
wrappedView.scrollView.finishInfiniteScroll(completion: completion)
|
||||
wrappedView.scrollView.removeInfiniteScroll()
|
||||
}
|
||||
|
||||
private func createRefreshControl() {
|
||||
let refreshControl = UIRefreshControl()
|
||||
refreshControl.addTarget(self, action: #selector(refreshAction), for: .valueChanged)
|
||||
|
||||
wrappedView.scrollView.refreshControl = refreshControl
|
||||
}
|
||||
|
||||
@objc private func refreshAction() {
|
||||
// it is implemented the combined behavior of `touchUpInside` and `touchUpOutside` using `CFRunLoopPerformBlock`,
|
||||
// which `UIRefreshControl` does not support
|
||||
CFRunLoopPerformBlock(CFRunLoopGetMain(), CFRunLoopMode.defaultMode.rawValue) { [weak self] in
|
||||
self?.reload()
|
||||
}
|
||||
}
|
||||
|
||||
private func removeRefreshControl() {
|
||||
wrappedView.scrollView.refreshControl = nil
|
||||
}
|
||||
|
||||
private func bindViewModelStates() {
|
||||
paginationViewModel.stateDriver
|
||||
.drive(stateChanged)
|
||||
.disposed(by: disposeBag)
|
||||
}
|
||||
|
||||
private func removeAllPlaceholderView() {
|
||||
wrappedView.backgroundView = nil
|
||||
wrappedView.footerView = nil
|
||||
}
|
||||
}
|
||||
|
||||
private extension PaginationWrapper {
|
||||
|
||||
private var stateChanged: Binder<LoadingState> {
|
||||
Binder(self) { base, value in
|
||||
switch value {
|
||||
case .initial:
|
||||
base.onInitialState()
|
||||
|
||||
case let .initialLoading(after):
|
||||
base.onLoadingState(afterState: after)
|
||||
|
||||
case let .loadingMore(after):
|
||||
base.onLoadingMoreState(afterState: after)
|
||||
|
||||
case let .results(newItems, from, after):
|
||||
base.onResultsState(newItems: newItems, from: from, afterState: after)
|
||||
|
||||
case let .error(error, after):
|
||||
base.onErrorState(error: error, afterState: after)
|
||||
|
||||
case .empty:
|
||||
base.onEmptyState()
|
||||
|
||||
case .exhausted:
|
||||
base.onExhaustedState()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var scrollOffsetChanged: Binder<CGPoint> {
|
||||
Binder(self) { base, value in
|
||||
base.currentPlaceholderViewTopConstraint?.constant = -value.y
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
//
|
||||
// Copyright (c) 2018 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import RxSwift
|
||||
import RxCocoa
|
||||
|
||||
open class RxDataLoadingModel<LoadingStateType: DataLoadingState>: RxNetworkOperationModel<LoadingStateType>
|
||||
where LoadingStateType.DataSourceType: RxDataSource {
|
||||
|
||||
public typealias EmptyResultChecker = (ResultType) -> Bool
|
||||
|
||||
let emptyResultChecker: EmptyResultChecker
|
||||
|
||||
/// Model initializer with data source, empty result checker and custom error handler.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - dataSource: Data source for data loading.
|
||||
/// - customErrorHandler: Custom error handler for state update. Pass nil for default error handling.
|
||||
/// - emptyResultChecker: Empty result checker closure.
|
||||
public init(dataSource: DataSourceType,
|
||||
customErrorHandler: ErrorHandler? = nil,
|
||||
emptyResultChecker: @escaping EmptyResultChecker) {
|
||||
|
||||
self.emptyResultChecker = emptyResultChecker
|
||||
|
||||
super.init(dataSource: dataSource, customErrorHandler: customErrorHandler)
|
||||
}
|
||||
|
||||
open func reload() {
|
||||
execute()
|
||||
}
|
||||
|
||||
override func onGot(result: ResultType, from dataSource: DataSourceType) {
|
||||
if emptyResultChecker(result) {
|
||||
state = .emptyState
|
||||
} else {
|
||||
super.onGot(result: result, from: dataSource)
|
||||
}
|
||||
|
||||
updateStateAfterResult(from: dataSource)
|
||||
}
|
||||
|
||||
func updateStateAfterResult(from dataSource: DataSourceType) {
|
||||
// override in subcass if needed
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
//
|
||||
// Copyright (c) 2018 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import RxSwift
|
||||
import RxCocoa
|
||||
|
||||
open class RxNetworkOperationModel<LoadingStateType: NetworkOperationState>: NetworkOperationModel
|
||||
where LoadingStateType.DataSourceType: RxDataSource {
|
||||
|
||||
public typealias DataSourceType = LoadingStateType.DataSourceType
|
||||
public typealias ResultType = DataSourceType.ResultType
|
||||
|
||||
public typealias ErrorHandler = (Error, LoadingStateType) -> LoadingStateType
|
||||
|
||||
private let stateRelay = BehaviorRelay<LoadingStateType>(value: .initialState)
|
||||
var currentRequestDisposable: Disposable?
|
||||
|
||||
private(set) var dataSource: DataSourceType
|
||||
|
||||
private let errorHandler: ErrorHandler
|
||||
|
||||
open var stateDriver: Driver<LoadingStateType> {
|
||||
stateRelay.asDriver()
|
||||
}
|
||||
|
||||
/// Model initializer with data source and custom error handler.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - dataSource: Data source for network operation.
|
||||
/// - customErrorHandler: Custom error handler for state update. Pass nil for default error handling.
|
||||
public init(dataSource: DataSourceType, customErrorHandler: ErrorHandler? = nil) {
|
||||
self.errorHandler = customErrorHandler ?? { .errorState(error: $0, after: $1) }
|
||||
self.dataSource = dataSource
|
||||
}
|
||||
|
||||
/// Performs request to given data source
|
||||
public func execute() {
|
||||
currentRequestDisposable?.dispose()
|
||||
|
||||
state = .initialLoadingState(after: state)
|
||||
|
||||
requestResult(from: dataSource)
|
||||
}
|
||||
|
||||
/// Replaces current data source with new one.
|
||||
///
|
||||
/// - Parameter newDataSource: A new data source to use.
|
||||
public func replaceDataSource(with newDataSource: DataSourceType) {
|
||||
dataSource = newDataSource
|
||||
}
|
||||
|
||||
func onGot(error: Error) {
|
||||
state = errorHandler(error, state)
|
||||
}
|
||||
|
||||
func onGot(result: ResultType, from dataSource: DataSourceType) {
|
||||
state = .resultState(result: result,
|
||||
from: dataSource,
|
||||
after: state)
|
||||
}
|
||||
|
||||
func requestResult(from dataSource: DataSourceType) {
|
||||
currentRequestDisposable = dataSource
|
||||
.resultSingle()
|
||||
.observe(on: MainScheduler.instance)
|
||||
.subscribe(onSuccess: { [weak self] result in
|
||||
self?.onGot(result: result, from: dataSource)
|
||||
}, onFailure: { [weak self] error in
|
||||
self?.onGot(error: error)
|
||||
})
|
||||
}
|
||||
|
||||
var state: LoadingStateType {
|
||||
get {
|
||||
stateRelay.value
|
||||
}
|
||||
set {
|
||||
stateRelay.accept(newValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,202 @@
|
|||
//
|
||||
// Copyright (c) 2019 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import TableKit
|
||||
import RxSwift
|
||||
import UIKit
|
||||
|
||||
public typealias SearchResultsController = UIViewController & SearchResultsViewController
|
||||
|
||||
/// Class that allows to enter text for search and then displays search results in table view
|
||||
open class BaseSearchViewController < Item,
|
||||
ItemViewModel,
|
||||
ViewModel,
|
||||
CustomView: UIView & TableViewHolder>: BaseCustomViewController<ViewModel, CustomView>
|
||||
where ViewModel: BaseSearchViewModel<Item, ItemViewModel> {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
private let disposeBag = DisposeBag()
|
||||
private let searchResultsController: SearchResultsController
|
||||
private let searchController: UISearchController
|
||||
private var didEnterText = false
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
public init(viewModel: ViewModel, searchResultsController: SearchResultsController) {
|
||||
self.searchResultsController = searchResultsController
|
||||
self.searchController = UISearchController(searchResultsController: searchResultsController)
|
||||
super.init(viewModel: viewModel)
|
||||
initialLoadView()
|
||||
}
|
||||
|
||||
required public init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
// MARK: - Configurable Controller
|
||||
|
||||
open override func bindViews() {
|
||||
super.bindViews()
|
||||
viewModel.itemsViewModelsDriver
|
||||
.drive(with: self) { owner, viewModels in
|
||||
owner.handle(itemViewModels: viewModels)
|
||||
}
|
||||
.disposed(by: disposeBag)
|
||||
|
||||
Observable.merge(searchResults, resetResults)
|
||||
.subscribe(with: self) { owner, state in
|
||||
owner.handle(searchResultsState: state)
|
||||
}
|
||||
.disposed(by: disposeBag)
|
||||
|
||||
let searchText = searchController.searchBar.rx.text
|
||||
.changed
|
||||
.do(onNext: { [weak self] text in
|
||||
self?.handle(searchText: text)
|
||||
})
|
||||
.map { $0 ?? "" }
|
||||
|
||||
viewModel.bind(searchText: searchText)
|
||||
.disposed(by: disposeBag)
|
||||
}
|
||||
|
||||
open override func addViews() {
|
||||
super.addViews()
|
||||
|
||||
if #available(iOS 11.0, *) {
|
||||
navigationItem.searchController = searchController
|
||||
} else {
|
||||
customView.tableView.tableHeaderView = searchController.searchBar
|
||||
}
|
||||
searchController.view.addSubview(statusBarView)
|
||||
}
|
||||
|
||||
open override func configureAppearance() {
|
||||
super.configureAppearance()
|
||||
definesPresentationContext = true
|
||||
configureSearchBarAppearance(searchController.searchBar)
|
||||
customView.tableView.tableHeaderView?.backgroundColor = searchBarColor
|
||||
}
|
||||
|
||||
open override func localize() {
|
||||
super.localize()
|
||||
|
||||
searchController.searchBar.placeholder = searchBarPlaceholder
|
||||
}
|
||||
|
||||
// MARK: - Search Controller Functionality
|
||||
|
||||
open func createRows(from itemsViewModels: [ItemViewModel]) -> [Row] {
|
||||
assertionFailure("createRows(from:) has not been implemented")
|
||||
return []
|
||||
}
|
||||
|
||||
open var searchBarPlaceholder: String {
|
||||
""
|
||||
}
|
||||
|
||||
open var searchBarColor: UIColor {
|
||||
.gray
|
||||
}
|
||||
|
||||
open var statusBarView: UIView {
|
||||
let statusBarSize = statusBarFrame().size
|
||||
let statusBarView = UIView(frame: CGRect(x: 0,
|
||||
y: 0,
|
||||
width: statusBarSize.width,
|
||||
height: statusBarSize.height))
|
||||
statusBarView.backgroundColor = statusBarColor
|
||||
|
||||
return statusBarView
|
||||
}
|
||||
|
||||
open var statusBarColor: UIColor {
|
||||
.black
|
||||
}
|
||||
|
||||
open func updateContent(with viewModels: [ItemViewModel]) {
|
||||
// override in subclass
|
||||
}
|
||||
|
||||
open func stateForUpdate(with viewModels: [ItemViewModel]) -> SearchResultsViewControllerState {
|
||||
let rows = createRows(from: viewModels)
|
||||
return .rowsContent(rows: rows)
|
||||
}
|
||||
|
||||
open var resetResults: Observable<SearchResultsViewControllerState> {
|
||||
searchController.rx.willPresent
|
||||
.map { SearchResultsViewControllerState.initial }
|
||||
}
|
||||
|
||||
open var searchResults: Observable<SearchResultsViewControllerState> {
|
||||
viewModel.searchResultsDriver
|
||||
.asObservable()
|
||||
.compactMap { [weak self] viewModels -> SearchResultsViewControllerState? in
|
||||
self?.stateForUpdate(with: viewModels)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
open func handle(itemViewModels viewModels: [ItemViewModel]) {
|
||||
updateContent(with: viewModels)
|
||||
}
|
||||
|
||||
open func handle(searchResultsState state: SearchResultsViewControllerState) {
|
||||
searchResultsController.update(for: state)
|
||||
}
|
||||
|
||||
open func handle(searchText: String?) {
|
||||
setTableViewInsets()
|
||||
}
|
||||
|
||||
private func setTableViewInsets() {
|
||||
guard !didEnterText else {
|
||||
return
|
||||
}
|
||||
didEnterText = true
|
||||
searchResultsController.searchResultsView.tableView.contentInset = tableViewInsets
|
||||
searchResultsController.searchResultsView.tableView.scrollIndicatorInsets = tableViewInsets
|
||||
}
|
||||
|
||||
open func statusBarFrame() -> CGRect {
|
||||
/// override in subclass
|
||||
return .zero
|
||||
}
|
||||
|
||||
open func configureSearchBarAppearance(_ searchBar: UISearchBar) {
|
||||
// override in subclass
|
||||
}
|
||||
}
|
||||
|
||||
extension BaseSearchViewController {
|
||||
open var tableViewInsets: UIEdgeInsets {
|
||||
let searchBarHeight = searchController.searchBar.frame.height
|
||||
let statusBarHeight = statusBarFrame().height
|
||||
|
||||
return UIEdgeInsets(top: searchBarHeight + statusBarHeight,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
right: 0)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
//
|
||||
// Copyright (c) 2019 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import RxSwift
|
||||
import RxCocoa
|
||||
|
||||
/// ViewModel that loads data from a given data source and performs search among results
|
||||
open class BaseSearchViewModel<Item, ItemViewModel>: GeneralDataLoadingViewModel<[Item]> {
|
||||
|
||||
public typealias ItemsList = [Item]
|
||||
|
||||
private let searchTextRelay = BehaviorRelay(value: "")
|
||||
|
||||
public init(dataSource: Single<ItemsList>) {
|
||||
super.init(dataSource: dataSource, emptyResultChecker: { $0.isEmpty })
|
||||
}
|
||||
|
||||
open var itemsViewModelsDriver: Driver<[ItemViewModel]> {
|
||||
loadingResultObservable
|
||||
.compactMap { [weak self] items in
|
||||
self?.viewModels(from: items)
|
||||
}
|
||||
.flatMap { Observable.from(optional: $0) }
|
||||
.share(replay: 1, scope: .forever)
|
||||
.asDriver(onErrorDriveWith: .empty())
|
||||
}
|
||||
|
||||
open var searchDebounceInterval: RxTimeInterval {
|
||||
.seconds(1)
|
||||
}
|
||||
|
||||
open var searchResultsDriver: Driver<[ItemViewModel]> {
|
||||
searchTextRelay.debounce(searchDebounceInterval, scheduler: MainScheduler.instance)
|
||||
.withLatestFrom(loadingResultObservable) { ($0, $1) }
|
||||
.flatMapLatest { [weak self] searchText, items -> Observable<ItemsList> in
|
||||
self?.search(by: searchText, from: items).asObservable() ?? .empty()
|
||||
}
|
||||
.compactMap { [weak self] items in
|
||||
self?.viewModels(from: items)
|
||||
}
|
||||
.flatMap { Observable.from(optional: $0) }
|
||||
.share(replay: 1, scope: .forever)
|
||||
.asDriver(onErrorDriveWith: .empty())
|
||||
}
|
||||
|
||||
open func viewModel(from item: Item) -> ItemViewModel {
|
||||
fatalError("viewModel(from:) has not been implemented")
|
||||
}
|
||||
|
||||
open func search(by searchString: String, from items: ItemsList) -> Single<ItemsList> {
|
||||
fatalError("searchEngine(for:) has not been implemented")
|
||||
}
|
||||
|
||||
open func bind(searchText: Observable<String>) -> Disposable {
|
||||
searchText.bind(to: searchTextRelay)
|
||||
}
|
||||
|
||||
private func viewModels(from items: ItemsList) -> [ItemViewModel] {
|
||||
items.map { self.viewModel(from: $0) }
|
||||
}
|
||||
|
||||
open var loadingResultObservable: Observable<ResultType> {
|
||||
loadingStateDriver
|
||||
.asObservable()
|
||||
.map { $0.result }
|
||||
.flatMap { Observable.from(optional: $0) }
|
||||
}
|
||||
|
||||
open var loadingErrorObservable: Observable<Error> {
|
||||
loadingStateDriver
|
||||
.asObservable()
|
||||
.map { $0.error }
|
||||
.flatMap { Observable.from(optional: $0) }
|
||||
}
|
||||
|
||||
open var firstLoadingResultObservable: Single<ResultType> {
|
||||
loadingResultObservable
|
||||
.take(1)
|
||||
.asSingle()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,154 @@
|
|||
//
|
||||
// Copyright (c) 2017 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import RxSwift
|
||||
import RxCocoa
|
||||
import Alamofire
|
||||
import RxAlamofire
|
||||
|
||||
/// Base network service implementation build on top of LeadKit extensions for Alamofire.
|
||||
/// Has an ability to automatically show / hide network activity indicator
|
||||
open class NetworkService {
|
||||
|
||||
/// Enable synchronization for setting behaviour relay from different thread
|
||||
private let lock = NSRecursiveLock()
|
||||
|
||||
private let requestCountRelay = BehaviorRelay(value: 0)
|
||||
|
||||
public let configuration: NetworkServiceConfiguration
|
||||
public let sessionManager: SessionManager
|
||||
|
||||
/// Driver that emits true when active requests count != 0 and false otherwise.
|
||||
public var isActivityIndicatorVisibleDriver: Driver<Bool> {
|
||||
requestCountRelay.asDriver().map { $0 != 0 }.distinctUntilChanged()
|
||||
}
|
||||
|
||||
/// - Parameter sessionManager: Alamofire.SessionManager to use for requests
|
||||
/// Creates new instance of NetworkService with given Alamofire session manager
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - configuration: instance of NetworkServiceConfiguration to configure network service.
|
||||
public init(configuration: NetworkServiceConfiguration) {
|
||||
|
||||
self.configuration = configuration
|
||||
self.sessionManager = configuration.sessionManager
|
||||
}
|
||||
|
||||
/// Perform reactive request to get mapped ObservableMappable model and http response
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - parameters: api parameters to pass to Alamofire
|
||||
/// - additionalValidStatusCodes: set of additional valid status codes
|
||||
/// - decoder: json decoder to decode response data
|
||||
/// - Returns: Observable of tuple containing (HTTPURLResponse, ObservableMappable)
|
||||
public func rxObservableRequest<T: ObservableMappable>(with parameters: ApiRequestParameters,
|
||||
additionalValidStatusCodes: Set<Int> = [],
|
||||
decoder: JSONDecoder = JSONDecoder())
|
||||
-> Observable<SessionManager.ModelResponse<T>> {
|
||||
|
||||
sessionManager.rx.responseObservableModel(requestParameters: parameters,
|
||||
additionalValidStatusCodes: additionalValidStatusCodes,
|
||||
decoder: decoder)
|
||||
.counterTracking(for: self)
|
||||
}
|
||||
|
||||
/// Perform reactive request to get mapped ImmutableMappable model and http response
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - parameters: api parameters to pass to Alamofire
|
||||
/// - additionalValidStatusCodes: set of additional valid status codes
|
||||
/// - decoder: json decoder to decode response data
|
||||
/// - Returns: Observable of tuple containing (HTTPURLResponse, ImmutableMappable)
|
||||
public func rxRequest<T: Decodable>(with parameters: ApiRequestParameters,
|
||||
additionalValidStatusCodes: Set<Int> = [],
|
||||
decoder: JSONDecoder = JSONDecoder())
|
||||
-> Observable<SessionManager.ModelResponse<T>> {
|
||||
|
||||
sessionManager.rx.responseModel(requestParameters: parameters,
|
||||
additionalValidStatusCodes: additionalValidStatusCodes,
|
||||
decoder: decoder)
|
||||
.counterTracking(for: self)
|
||||
}
|
||||
|
||||
/// Perform reactive request to get data and http response
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - parameters: api parameters to pass to Alamofire
|
||||
/// - additionalValidStatusCodes: set of additional valid status codes
|
||||
/// - Returns: Observable of tuple containing (HTTPURLResponse, Data)
|
||||
public func rxDataRequest(with parameters: ApiRequestParameters, additionalValidStatusCodes: Set<Int> = [])
|
||||
-> Observable<SessionManager.DataResponse> {
|
||||
|
||||
sessionManager.rx.responseData(requestParameters: parameters,
|
||||
additionalValidStatusCodes: additionalValidStatusCodes)
|
||||
.counterTracking(for: self)
|
||||
}
|
||||
|
||||
/// Perform reactive request to upload data and get Observable model and http response
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - parameters: api upload parameters to pass Alamofire
|
||||
/// - additionalValidStatusCodes: set of additional valid status codes
|
||||
/// - decoder: json decoder to decode response data
|
||||
/// - Returns: Observable of model response
|
||||
public func rxUploadRequest<T: Decodable>(with parameters: ApiUploadRequestParameters,
|
||||
additionalValidStatusCodes: Set<Int> = [],
|
||||
decoder: JSONDecoder = JSONDecoder())
|
||||
-> Observable<SessionManager.ModelResponse<T>> {
|
||||
|
||||
sessionManager.rx.uploadResponseModel(requestParameters: parameters,
|
||||
additionalValidStatusCodes: additionalValidStatusCodes,
|
||||
decoder: decoder)
|
||||
.counterTracking(for: self)
|
||||
}
|
||||
}
|
||||
|
||||
private extension NetworkService {
|
||||
|
||||
func increaseRequestCounter() {
|
||||
lock.lock()
|
||||
requestCountRelay.accept(requestCountRelay.value + 1)
|
||||
lock.unlock()
|
||||
}
|
||||
|
||||
func decreaseRequestCounter() {
|
||||
lock.lock()
|
||||
requestCountRelay.accept(requestCountRelay.value - 1)
|
||||
lock.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
public extension Observable {
|
||||
|
||||
/// Increase and descrease NetworkService request counter on subscribe and dispose
|
||||
/// (used to show / hide activity indicator)
|
||||
///
|
||||
/// - Parameter networkService: NetworkService to operate on it
|
||||
/// - Returns: The source sequence with the side-effecting behavior applied.
|
||||
func counterTracking(for networkService: NetworkService) -> Observable<Observable.Element> {
|
||||
`do`(onSubscribe: {
|
||||
networkService.increaseRequestCounter()
|
||||
}, onDispose: {
|
||||
networkService.decreaseRequestCounter()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
//
|
||||
// Copyright (c) 2018 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import Alamofire
|
||||
|
||||
/// Session Manager stored in NetworkService
|
||||
open class SessionManager: Alamofire.Session {
|
||||
|
||||
/// Response with HTTP URL Response and target object
|
||||
public typealias ModelResponse<T> = (response: HTTPURLResponse, model: T)
|
||||
|
||||
/// Response with HTTP URL Response and data
|
||||
public typealias DataResponse = (response: HTTPURLResponse, data: Data)
|
||||
|
||||
/// Acceptable status codes for validation
|
||||
public let acceptableStatusCodes: Set<Int>
|
||||
|
||||
/// Dispatch Queue on which mapping is performed
|
||||
public let mappingQueue: DispatchQueue
|
||||
|
||||
public init(configuration: URLSessionConfiguration,
|
||||
serverTrustManager: ServerTrustManager,
|
||||
acceptableStatusCodes: Set<Int>,
|
||||
mappingQueue: DispatchQueue) {
|
||||
|
||||
self.acceptableStatusCodes = acceptableStatusCodes
|
||||
self.mappingQueue = mappingQueue
|
||||
|
||||
let delegate = SessionDelegate()
|
||||
|
||||
let delegateQueue = OperationQueue()
|
||||
delegateQueue.underlyingQueue = mappingQueue
|
||||
|
||||
let session = URLSession(configuration: configuration, delegate: delegate, delegateQueue: delegateQueue)
|
||||
|
||||
super.init(session: session,
|
||||
delegate: delegate,
|
||||
rootQueue: mappingQueue,
|
||||
serverTrustManager: serverTrustManager)
|
||||
}
|
||||
|
||||
public init(session: URLSession,
|
||||
delegate: SessionDelegate,
|
||||
serverTrustManager: ServerTrustManager,
|
||||
acceptableStatusCodes: Set<Int>,
|
||||
mappingQueue: DispatchQueue) {
|
||||
|
||||
self.acceptableStatusCodes = acceptableStatusCodes
|
||||
self.mappingQueue = mappingQueue
|
||||
|
||||
session.delegateQueue.underlyingQueue = mappingQueue
|
||||
|
||||
super.init(session: session,
|
||||
delegate: delegate,
|
||||
rootQueue: mappingQueue,
|
||||
serverTrustManager: serverTrustManager)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
//
|
||||
// Copyright (c) 2020 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import RxCocoa
|
||||
import RxSwift
|
||||
|
||||
open class BaseTappableViewModel<PayloadType> {
|
||||
private let tapRelay = PublishRelay<PayloadType>()
|
||||
|
||||
public var tapDriver: Driver<PayloadType> {
|
||||
tapRelay.asDriver(onErrorDriveWith: .empty())
|
||||
}
|
||||
|
||||
public var tapObservable: Observable<PayloadType> {
|
||||
tapRelay.asObservable()
|
||||
}
|
||||
|
||||
public func bind(tapObservable: Observable<PayloadType>) -> Disposable {
|
||||
tapObservable.bind(to: tapRelay)
|
||||
}
|
||||
|
||||
public func tap(payload: PayloadType) {
|
||||
tapRelay.accept(payload)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
//
|
||||
// Copyright (c) 2020 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
open class VoidTappableViewModel: BaseTappableViewModel<Void> {
|
||||
public func tap() {
|
||||
tap(payload: ())
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
//
|
||||
// Copyright (c) 2018 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
/// Placeholder view visual attributes without layout.
|
||||
open class BasePlaceholderViewModel {
|
||||
|
||||
/// Title text with text attributes.
|
||||
public let title: ViewText
|
||||
/// Description text with text attributes.
|
||||
public let description: ViewText?
|
||||
/// Center image of placeholder.
|
||||
public let centerImage: UIImage?
|
||||
/// Button title with text attributes.
|
||||
public let buttonTitle: ViewText?
|
||||
/// Placeholder background.
|
||||
public let background: ViewBackground
|
||||
|
||||
/// Memberwise initializer.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - title: Title text with text attributes.
|
||||
/// - description: Description text with text attributes.
|
||||
/// - centerImage: Center image of placeholder.
|
||||
/// - buttonTitle: Button title with text attributes.
|
||||
/// - background: Placeholder background.
|
||||
public init(title: ViewText,
|
||||
description: ViewText? = nil,
|
||||
centerImage: UIImage? = nil,
|
||||
buttonTitle: ViewText? = nil,
|
||||
background: ViewBackground = .color(.clear)) {
|
||||
|
||||
self.title = title
|
||||
self.description = description
|
||||
self.centerImage = centerImage
|
||||
self.buttonTitle = buttonTitle
|
||||
self.background = background
|
||||
}
|
||||
}
|
||||
|
||||
public extension BasePlaceholderViewModel {
|
||||
|
||||
/// Returns true if description is not nil.
|
||||
var hasDescription: Bool {
|
||||
description != nil
|
||||
}
|
||||
|
||||
/// Returns true buttonTitle is not nil.
|
||||
var hasButton: Bool {
|
||||
buttonTitle != nil
|
||||
}
|
||||
|
||||
/// Returns true if centerImage is not nil.
|
||||
var hasCenterImage: Bool {
|
||||
centerImage != nil
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
//
|
||||
// Copyright (c) 2018 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
/// Layoutless placeholder view. This class is used as views holder & configurator.
|
||||
/// You should inherit it and implement layout.
|
||||
open class BasePlaceholderView: ButtonHolderView, InitializableView {
|
||||
|
||||
/// Title label of placeholder view.
|
||||
public let titleLabel = UILabel()
|
||||
/// Description label of placeholder view.
|
||||
public let descriptionLabel = UILabel()
|
||||
|
||||
/// Center image view of placeholder view.
|
||||
public let centerImageView = UIImageView()
|
||||
/// Action button of placeholder view.
|
||||
public private(set) lazy var button = createButton()
|
||||
|
||||
/// Background image view of placeholder view.
|
||||
public let backgroundImageView = UIImageView()
|
||||
|
||||
public override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
initializeView()
|
||||
}
|
||||
|
||||
required public init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
// MARK: - Subclass override
|
||||
|
||||
/// Override to create your own subclass button.
|
||||
///
|
||||
/// - Returns: UIButton (sub)class.
|
||||
open func createButton() -> UIButton {
|
||||
UIButton()
|
||||
}
|
||||
|
||||
// MARK: - InitializableView
|
||||
|
||||
open func addViews() {
|
||||
// override in subclass
|
||||
}
|
||||
|
||||
open func bindViews() {
|
||||
// override in subclass
|
||||
}
|
||||
|
||||
open func configureAppearance() {
|
||||
// override in subclass
|
||||
}
|
||||
|
||||
open func localize() {
|
||||
// override in subclass
|
||||
}
|
||||
|
||||
open func configureLayout() {
|
||||
// override in subclass
|
||||
}
|
||||
}
|
||||
|
||||
public extension BasePlaceholderView {
|
||||
|
||||
/// Method for base configuration BasePlaceholderView instance.
|
||||
///
|
||||
/// - Parameter viewModel: Placeholder view visual attributes without layout.
|
||||
func baseConfigure(with viewModel: BasePlaceholderViewModel) {
|
||||
titleLabel.configure(with: viewModel.title)
|
||||
|
||||
descriptionLabel.isHidden = !viewModel.hasDescription
|
||||
viewModel.description?.configure(view: descriptionLabel)
|
||||
|
||||
centerImageView.isHidden = !viewModel.hasCenterImage
|
||||
centerImageView.image = viewModel.centerImage
|
||||
|
||||
viewModel.background.configure(backgroundView: self,
|
||||
backgroundImageView: backgroundImageView)
|
||||
|
||||
button.isHidden = !viewModel.hasButton
|
||||
viewModel.buttonTitle?.configure(view: button)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
//
|
||||
// Copyright (c) 2020 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import RxSwift
|
||||
|
||||
open class BaseRxTableViewCell: UITableViewCell, InitializableView, DisposeBagHolder {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
public var disposeBag = DisposeBag()
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
override public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: .default, reuseIdentifier: reuseIdentifier)
|
||||
|
||||
initializeView()
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required public init?(coder aDecoder: NSCoder) {
|
||||
super.init(coder: aDecoder)
|
||||
}
|
||||
|
||||
// MARK: - Override
|
||||
|
||||
override open func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
|
||||
disposeBag = DisposeBag()
|
||||
}
|
||||
|
||||
// MARK: - InitializableView
|
||||
|
||||
open func addViews() {
|
||||
// overriding
|
||||
}
|
||||
|
||||
open func bindViews() {
|
||||
// overriding
|
||||
}
|
||||
|
||||
open func configureLayout() {
|
||||
// overriding
|
||||
}
|
||||
|
||||
open func configureAppearance() {
|
||||
selectionStyle = .none
|
||||
}
|
||||
|
||||
open func localize() {
|
||||
// overriding
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
//
|
||||
// Copyright (c) 2018 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
/// The main purpose of this class is to fix empty space on top of the screen
|
||||
/// when view controller view is UICollectionView.
|
||||
open class CollectionViewWrapperView: ScrollViewHolderView, CollectionViewHolder {
|
||||
|
||||
/// Contained collection view.
|
||||
public let collectionView: UICollectionView
|
||||
|
||||
/// Initializer with collection view layout parameter.
|
||||
///
|
||||
/// - Parameter layout: UICollectionViewLayout to pass in UICollectionView init.
|
||||
public init(layout: UICollectionViewLayout) {
|
||||
self.collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
||||
self.collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
|
||||
super.init(frame: .zero)
|
||||
|
||||
addSubview(collectionView)
|
||||
}
|
||||
|
||||
public required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
//
|
||||
// Copyright (c) 2020 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import RxSwift
|
||||
import TableKit
|
||||
|
||||
open class ContainerTableCell<TView: UIView>: BaseRxTableViewCell, ConfigurableCell where TView: ConfigurableView {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
private let wrappedView = TView()
|
||||
|
||||
open var shouldConfigureDefaultConstraints: Bool {
|
||||
true
|
||||
}
|
||||
|
||||
open var contentInsets: UIEdgeInsets {
|
||||
.zero
|
||||
}
|
||||
|
||||
open var contentViewBackgroundColor: UIColor {
|
||||
.clear
|
||||
}
|
||||
|
||||
// MARK: - ConfigurableCell
|
||||
|
||||
open func configure(with viewModel: TView.ViewModelType) {
|
||||
disposeBag = DisposeBag()
|
||||
wrappedView.configure(with: viewModel)
|
||||
}
|
||||
|
||||
// MARK: - InitializableView
|
||||
|
||||
override open func addViews() {
|
||||
super.addViews()
|
||||
|
||||
contentView.addSubview(wrappedView)
|
||||
}
|
||||
|
||||
override open func configureLayout() {
|
||||
super.configureLayout()
|
||||
|
||||
if shouldConfigureDefaultConstraints {
|
||||
wrappedView.snp.makeConstraints {
|
||||
$0.edges.equalToSuperview().inset(contentInsets)
|
||||
}
|
||||
} else {
|
||||
configureCustomConstraints(forWrappedView: wrappedView)
|
||||
}
|
||||
}
|
||||
|
||||
override open func configureAppearance() {
|
||||
super.configureAppearance()
|
||||
|
||||
contentView.backgroundColor = contentViewBackgroundColor
|
||||
backgroundColor = contentViewBackgroundColor
|
||||
}
|
||||
|
||||
open func configureCustomConstraints(forWrappedView view: TView) { }
|
||||
}
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
//
|
||||
// Copyright (c) 2019 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import RxCocoa
|
||||
import RxSwift
|
||||
|
||||
/// class that is a CustomizableButtonView subview and gives it a button functionality
|
||||
open class CustomizableButton: UIButton {
|
||||
|
||||
// MARK: - Constants
|
||||
|
||||
private let defaultBackgroundColor = UIColor.white
|
||||
|
||||
// MARK: - Background
|
||||
|
||||
private var backgroundColors: [UIControl.State: UIColor] = [:] {
|
||||
didSet {
|
||||
updateBackgroundColor()
|
||||
}
|
||||
}
|
||||
|
||||
func set(backgroundColors: [UIControl.State: UIColor]) {
|
||||
backgroundColors.forEach { setBackgroundColor($1, for: $0) }
|
||||
}
|
||||
|
||||
func setBackgroundColor(_ color: UIColor, for state: UIControl.State) {
|
||||
backgroundColors[state] = color
|
||||
}
|
||||
|
||||
func backgroundColor(for state: UIControl.State) -> UIColor? {
|
||||
backgroundColors[state]
|
||||
}
|
||||
|
||||
private func updateBackgroundColor() {
|
||||
if isEnabled {
|
||||
if isHighlighted {
|
||||
updateBackgroundColor(to: .highlighted)
|
||||
} else {
|
||||
updateBackgroundColor(to: .normal)
|
||||
}
|
||||
} else {
|
||||
updateBackgroundColor(to: .disabled)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateBackgroundColor(to state: UIControl.State) {
|
||||
if let stateColor = backgroundColor(for: state) {
|
||||
backgroundColor = stateColor
|
||||
} else if state != .normal, let normalStateColor = backgroundColor(for: .normal) {
|
||||
backgroundColor = normalStateColor
|
||||
} else {
|
||||
backgroundColor = defaultBackgroundColor
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Title
|
||||
|
||||
func set(titleColors: [UIControl.State: UIColor]) {
|
||||
titleColors.forEach { setTitleColor($1, for: $0) }
|
||||
}
|
||||
|
||||
func set(titles: [UIControl.State: String]) {
|
||||
titles.forEach { setTitle($1, for: $0) }
|
||||
}
|
||||
|
||||
func set(attributtedTitles: [UIControl.State: NSAttributedString]) {
|
||||
attributtedTitles.forEach { setAttributedTitle($1, for: $0) }
|
||||
}
|
||||
|
||||
// MARK: - Images
|
||||
|
||||
func set(images: [UIControl.State: UIImage]) {
|
||||
images.forEach { setImage($1, for: $0) }
|
||||
}
|
||||
|
||||
// MARK: - State
|
||||
|
||||
override open var isEnabled: Bool {
|
||||
didSet {
|
||||
updateBackgroundColor()
|
||||
}
|
||||
}
|
||||
|
||||
override open var isHighlighted: Bool {
|
||||
didSet {
|
||||
updateBackgroundColor()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,342 @@
|
|||
//
|
||||
// Copyright (c) 2019 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import RxSwift
|
||||
|
||||
public typealias Spinner = UIView & Animatable
|
||||
|
||||
public struct CustomizableButtonState: OptionSet {
|
||||
|
||||
// MARK: - OptionSet conformance
|
||||
|
||||
public let rawValue: Int
|
||||
|
||||
public init(rawValue: Int) {
|
||||
self.rawValue = rawValue
|
||||
}
|
||||
|
||||
// MARK: - States
|
||||
|
||||
public static let highlighted = CustomizableButtonState(rawValue: 1 << 1)
|
||||
public static let normal = CustomizableButtonState(rawValue: 1 << 2)
|
||||
public static let enabled = CustomizableButtonState(rawValue: 1 << 3)
|
||||
public static let disabled = CustomizableButtonState(rawValue: 1 << 4)
|
||||
public static let loading = CustomizableButtonState(rawValue: 1 << 5)
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
public var isLoading: Bool {
|
||||
contains(.loading)
|
||||
}
|
||||
}
|
||||
|
||||
/// container class that acts like a button and provides great customization
|
||||
open class CustomizableButtonView: UIView, InitializableView, ConfigurableView {
|
||||
|
||||
// MARK: - Stored Properties
|
||||
|
||||
public private(set) var disposeBag = DisposeBag()
|
||||
|
||||
private let button = CustomizableButton()
|
||||
|
||||
open var tapOnDisabledButton: VoidBlock?
|
||||
|
||||
public var shadowView = UIView() {
|
||||
willSet {
|
||||
shadowView.removeFromSuperview()
|
||||
}
|
||||
didSet {
|
||||
insertSubview(shadowView, at: 0)
|
||||
configureShadowViewConstraints()
|
||||
}
|
||||
}
|
||||
|
||||
public var spinnerView: Spinner? {
|
||||
willSet {
|
||||
removeSpinner()
|
||||
}
|
||||
didSet {
|
||||
if spinnerView != nil {
|
||||
addSpinner()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public var appearance = Appearance() {
|
||||
didSet {
|
||||
configureAppearance()
|
||||
configureConstraints()
|
||||
}
|
||||
}
|
||||
|
||||
public var buttonTitle: String = "" {
|
||||
willSet {
|
||||
button.text = newValue
|
||||
}
|
||||
}
|
||||
|
||||
public var hidesLabelWhenLoading = false
|
||||
|
||||
// MARK: - Computed Properties
|
||||
|
||||
public var tapObservable: Observable<Void> {
|
||||
button.rx.tap.asObservable()
|
||||
}
|
||||
|
||||
override open var forFirstBaselineLayout: UIView {
|
||||
button.forFirstBaselineLayout
|
||||
}
|
||||
|
||||
override open var forLastBaselineLayout: UIView {
|
||||
button.forLastBaselineLayout
|
||||
}
|
||||
|
||||
override open func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
if var touchPoint = touches.first?.location(in: self) {
|
||||
touchPoint = convert(touchPoint, to: self)
|
||||
if button.frame.contains(touchPoint) && !button.isEnabled {
|
||||
tapOnDisabledButton?()
|
||||
}
|
||||
}
|
||||
super.touchesBegan(touches, with: event)
|
||||
}
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
override public init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
initializeView()
|
||||
}
|
||||
|
||||
required public init?(coder aDecoder: NSCoder) {
|
||||
super.init(coder: aDecoder)
|
||||
initializeView()
|
||||
}
|
||||
|
||||
// MARK: - UI
|
||||
|
||||
override open func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
if shadowView.layer.cornerRadius == 0 {
|
||||
shadowView.layer.shadowPath = UIBezierPath(rect: button.bounds).cgPath
|
||||
}
|
||||
}
|
||||
|
||||
private func set(active: Bool) {
|
||||
if hidesLabelWhenLoading {
|
||||
button.titleLabel?.layer.opacity = active ? 0 : 1
|
||||
}
|
||||
|
||||
spinnerView?.isHidden = !active
|
||||
|
||||
if active {
|
||||
spinnerView?.startAnimating()
|
||||
} else {
|
||||
spinnerView?.stopAnimating()
|
||||
}
|
||||
}
|
||||
|
||||
private func addSpinner() {
|
||||
if let spinner = spinnerView {
|
||||
addSubview(spinner)
|
||||
configureSpinnerConstraints()
|
||||
spinner.isHidden = true
|
||||
}
|
||||
}
|
||||
|
||||
private func removeSpinner() {
|
||||
if spinnerView != nil {
|
||||
self.spinnerView?.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Layout
|
||||
|
||||
private func configureConstraints() {
|
||||
button.pinToSuperview(with: appearance.buttonInsets)
|
||||
configureShadowViewConstraints()
|
||||
layoutIfNeeded()
|
||||
}
|
||||
|
||||
private func configureSpinnerConstraints() {
|
||||
guard let spinnerView = spinnerView else {
|
||||
return
|
||||
}
|
||||
spinnerView.translatesAutoresizingMaskIntoConstraints = false
|
||||
var constraints = [NSLayoutConstraint]()
|
||||
switch appearance.spinnerPosition {
|
||||
case .center:
|
||||
constraints = [
|
||||
spinnerView.centerXAnchor.constraint(equalTo: button.centerXAnchor),
|
||||
spinnerView.centerYAnchor.constraint(equalTo: button.centerYAnchor)
|
||||
]
|
||||
|
||||
case .leftToText(let offset):
|
||||
if let buttonLabel = button.titleLabel {
|
||||
constraints = [
|
||||
spinnerView.centerYAnchor.constraint(equalTo: buttonLabel.centerYAnchor),
|
||||
spinnerView.trailingAnchor.constraint(equalTo: buttonLabel.leadingAnchor, constant: -offset)
|
||||
]
|
||||
}
|
||||
|
||||
case .rightToText(let offset):
|
||||
if let buttonLabel = button.titleLabel {
|
||||
constraints = [
|
||||
spinnerView.centerYAnchor.constraint(equalTo: buttonLabel.centerYAnchor),
|
||||
spinnerView.leadingAnchor.constraint(equalTo: buttonLabel.trailingAnchor, constant: offset)
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
NSLayoutConstraint.activate(constraints)
|
||||
}
|
||||
|
||||
private func configureShadowViewConstraints() {
|
||||
shadowView.constraintToEdges(of: button, with: .zero)
|
||||
}
|
||||
|
||||
// MARK: - Initializable View
|
||||
|
||||
open func addViews() {
|
||||
addSubviews(shadowView, button)
|
||||
}
|
||||
|
||||
open func configureAppearance() {
|
||||
button.titleLabel?.numberOfLines = appearance.numberOfLines
|
||||
button.titleLabel?.font = appearance.buttonFont
|
||||
button.alpha = appearance.alpha
|
||||
|
||||
button.set(attributtedTitles: appearance.buttonStateAttributtedTitles)
|
||||
button.set(titleColors: appearance.buttonTitleStateColors)
|
||||
button.set(images: appearance.buttonStateIcons)
|
||||
button.set(backgroundColors: appearance.buttonBackgroundStateColors)
|
||||
|
||||
let offset = appearance.buttonIconOffset
|
||||
button.imageEdgeInsets = UIEdgeInsets(top: offset.vertical,
|
||||
left: offset.horizontal,
|
||||
bottom: -offset.vertical,
|
||||
right: -offset.horizontal)
|
||||
|
||||
if let cornerRadius = appearance.buttonCornerRadius {
|
||||
button.layer.cornerRadius = cornerRadius
|
||||
} else {
|
||||
button.layer.cornerRadius = 0
|
||||
}
|
||||
|
||||
setNeedsDisplay()
|
||||
}
|
||||
|
||||
open func configure(with viewModel: CustomizableButtonViewModel) {
|
||||
disposeBag = DisposeBag()
|
||||
viewModel.stateDriver.drive(stateBinder).disposed(by: disposeBag)
|
||||
viewModel.bind(tapObservable: tapObservable).disposed(by: disposeBag)
|
||||
|
||||
button.text = viewModel.buttonTitle
|
||||
appearance = viewModel.appearance
|
||||
}
|
||||
|
||||
private var stateBinder: Binder<CustomizableButtonState> {
|
||||
Binder(self) { base, value in
|
||||
base.configureButton(withState: value)
|
||||
base.onStateChange(value)
|
||||
}
|
||||
}
|
||||
|
||||
open func onStateChange(_ state: CustomizableButtonState) {
|
||||
/// override in subclass
|
||||
}
|
||||
|
||||
open func configureButton(withState state: CustomizableButtonState) {
|
||||
button.isEnabled = ![.disabled, .loading].contains(state)
|
||||
isUserInteractionEnabled = button.isEnabled
|
||||
button.isHighlighted = state.contains(.highlighted) && !state.contains(.normal)
|
||||
set(active: state.contains(.loading))
|
||||
setNeedsDisplay()
|
||||
}
|
||||
}
|
||||
|
||||
private extension UIView {
|
||||
func constraintToEdges(of view: UIView, with offset: UIEdgeInsets) {
|
||||
translatesAutoresizingMaskIntoConstraints = false
|
||||
let constraints = [
|
||||
leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: offset.left),
|
||||
trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: offset.right),
|
||||
topAnchor.constraint(equalTo: view.topAnchor, constant: offset.top),
|
||||
bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: offset.bottom)
|
||||
]
|
||||
NSLayoutConstraint.activate(constraints)
|
||||
}
|
||||
}
|
||||
|
||||
public extension CustomizableButtonView {
|
||||
struct Appearance {
|
||||
|
||||
public var buttonFont: UIFont
|
||||
public var buttonStateAttributtedTitles: [UIControl.State: NSAttributedString]
|
||||
public var buttonTitleStateColors: [UIControl.State: UIColor]
|
||||
public var buttonBackgroundStateColors: [UIControl.State: UIColor]
|
||||
public var buttonStateIcons: [UIControl.State: UIImage]
|
||||
public var buttonIconOffset: UIOffset
|
||||
public var buttonInsets: UIEdgeInsets
|
||||
public var buttonCornerRadius: CGFloat?
|
||||
public var spinnerPosition: SpinnerPosition
|
||||
public var numberOfLines: Int
|
||||
public var alpha: CGFloat
|
||||
|
||||
public init(buttonFont: UIFont = .systemFont(ofSize: 15),
|
||||
buttonStateAttributtedTitles: [UIControl.State: NSAttributedString] = [:],
|
||||
buttonTitleStateColors: [UIControl.State: UIColor] = [:],
|
||||
buttonBackgroundStateColors: [UIControl.State: UIColor] = [:],
|
||||
buttonStateIcons: [UIControl.State: UIImage] = [:],
|
||||
buttonIconOffset: UIOffset = .zero,
|
||||
buttonInsets: UIEdgeInsets = .zero,
|
||||
buttonCornerRadius: CGFloat? = nil,
|
||||
spinnerPosition: SpinnerPosition = .center,
|
||||
numberOfLines: Int = 0,
|
||||
alpha: CGFloat = 1) {
|
||||
|
||||
self.buttonFont = buttonFont
|
||||
self.buttonStateAttributtedTitles = buttonStateAttributtedTitles
|
||||
self.buttonTitleStateColors = buttonTitleStateColors
|
||||
self.buttonBackgroundStateColors = buttonBackgroundStateColors
|
||||
self.buttonStateIcons = buttonStateIcons
|
||||
self.buttonIconOffset = buttonIconOffset
|
||||
self.buttonInsets = buttonInsets
|
||||
self.buttonCornerRadius = buttonCornerRadius
|
||||
self.spinnerPosition = spinnerPosition
|
||||
self.numberOfLines = numberOfLines
|
||||
self.alpha = alpha
|
||||
}
|
||||
}
|
||||
|
||||
enum SpinnerPosition {
|
||||
case center
|
||||
case leftToText(offset: CGFloat)
|
||||
case rightToText(offset: CGFloat)
|
||||
}
|
||||
}
|
||||
|
||||
extension UIControl.State: Hashable {
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(Int(rawValue))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
//
|
||||
// Copyright (c) 2019 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import RxCocoa
|
||||
import RxSwift
|
||||
|
||||
/// viewModel class for CustomizableButtonView configuration
|
||||
open class CustomizableButtonViewModel {
|
||||
|
||||
public typealias Appearance = CustomizableButtonView.Appearance
|
||||
|
||||
private let stateRelay = BehaviorRelay(value: CustomizableButtonState.enabled)
|
||||
private let tapRelay = BehaviorRelay(value: ())
|
||||
public let appearance: Appearance
|
||||
public let buttonTitle: String
|
||||
|
||||
public init(buttonTitle: String, appearance: Appearance) {
|
||||
self.buttonTitle = buttonTitle
|
||||
self.appearance = appearance
|
||||
}
|
||||
|
||||
open var stateDriver: Driver<CustomizableButtonState> {
|
||||
stateRelay.asDriver()
|
||||
}
|
||||
|
||||
func bind(tapObservable: Observable<Void>) -> Disposable {
|
||||
tapObservable.bind(to: tapRelay)
|
||||
}
|
||||
|
||||
public var tapDriver: Driver<Void> {
|
||||
tapRelay.asDriver()
|
||||
}
|
||||
|
||||
public func updateState(with newState: CustomizableButtonState) {
|
||||
stateRelay.accept(newState)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
//
|
||||
// Copyright (c) 2018 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
internal final class TextPlaceholderView: UIView {
|
||||
|
||||
enum PlaceholderText: String {
|
||||
|
||||
case empty = "There is nothing here"
|
||||
case error = "An error has occurred"
|
||||
case loading = "Loading..."
|
||||
case retry = "Retry"
|
||||
case retryLoadMore = "Retry load more"
|
||||
}
|
||||
|
||||
init(title: PlaceholderText) {
|
||||
super.init(frame: .zero)
|
||||
|
||||
let label = UILabel()
|
||||
label.text = title.rawValue
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
addSubview(label)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
label.centerXAnchor.constraint(equalTo: centerXAnchor),
|
||||
label.centerYAnchor.constraint(equalTo: centerYAnchor)
|
||||
])
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
//
|
||||
// Copyright (c) 2018 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
internal final class TextWithButtonPlaceholder: UIView {
|
||||
|
||||
typealias TapHandler = () -> Void
|
||||
|
||||
private let tapHandler: TapHandler
|
||||
|
||||
init(title: TextPlaceholderView.PlaceholderText,
|
||||
buttonTitle: TextPlaceholderView.PlaceholderText,
|
||||
tapHandler: @escaping TapHandler) {
|
||||
|
||||
self.tapHandler = tapHandler
|
||||
|
||||
super.init(frame: .zero)
|
||||
|
||||
let textPlaceholder = TextPlaceholderView(title: title)
|
||||
|
||||
let button = UIButton(type: .custom)
|
||||
button.backgroundColor = .lightGray
|
||||
button.setTitle(buttonTitle.rawValue, for: .normal)
|
||||
button.addTarget(self, action: #selector(buttonDidTapped(_:)), for: .touchUpInside)
|
||||
|
||||
let stackView = UIStackView(arrangedSubviews: [textPlaceholder, button])
|
||||
stackView.axis = .vertical
|
||||
|
||||
addSubview(stackView)
|
||||
|
||||
stackView.setToCenter(withSize: nil)
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
@objc private func buttonDidTapped(_ button: UIButton) {
|
||||
tapHandler()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
//
|
||||
// Copyright (c) 2017 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import TableKit
|
||||
|
||||
/// Empty cell class. Do not use it directly.
|
||||
/// - see: `EmptyCellRow`
|
||||
public final class EmptyCell: SeparatorCell, AppearanceConfigurable, ConfigurableCell {
|
||||
public struct Appearance {
|
||||
let color: UIColor
|
||||
|
||||
public init(color: UIColor = .clear) {
|
||||
self.color = color
|
||||
}
|
||||
}
|
||||
|
||||
public override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
|
||||
resetAppearance()
|
||||
}
|
||||
|
||||
public required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
public func configure(appearance: Appearance) {
|
||||
selectionStyle = .none
|
||||
backgroundColor = .clear
|
||||
contentView.backgroundColor = appearance.color
|
||||
}
|
||||
|
||||
public func configure(with _: Void) { }
|
||||
|
||||
public override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
|
||||
resetAppearance()
|
||||
}
|
||||
|
||||
private func resetAppearance() {
|
||||
configure(appearance: Appearance())
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
//
|
||||
// Copyright (c) 2017 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import TableKit
|
||||
|
||||
/// Class that
|
||||
/// - Simulates spacing with no-breaking constraints
|
||||
/// - Can end editing on click
|
||||
public final class EmptyCellRow: TableRow<EmptyCell> {
|
||||
|
||||
private let rowHeight: CGFloat
|
||||
|
||||
/// Provide height with color to create row
|
||||
/// - parameter height: Height of row
|
||||
/// - parameter color: Color of row
|
||||
/// - parameter endEditingOnClick: Will cell end editing for neighbour currently active UIControl subclasses
|
||||
/// - returns: Fully configured EmptyCellRow
|
||||
public init(height: CGFloat, endEditingOnClick: Bool = false) {
|
||||
rowHeight = height
|
||||
|
||||
super.init(item: ())
|
||||
|
||||
if endEditingOnClick {
|
||||
self.on(.click) { options in
|
||||
options.cell?.window?.endEditing(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Used for set custom height to each cell, not for each cell type
|
||||
override public var defaultHeight: CGFloat? {
|
||||
rowHeight
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
//
|
||||
// Copyright (c) 2019 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
/// Default view model for LabelTableViewCell.
|
||||
open class LabelCellViewModel {
|
||||
|
||||
public let viewText: ViewText
|
||||
public let contentBackground: ViewBackground
|
||||
public let contentInsets: UIEdgeInsets
|
||||
public let labelInsets: UIEdgeInsets
|
||||
public let separatorType: CellSeparatorType
|
||||
|
||||
/// Memberwise initializer.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - viewText: View text to configure label.
|
||||
/// - contentBackground: View background to configure background.
|
||||
/// - contentInsets: Content insets to use for layout whole content.
|
||||
/// - labelInsets: Label insets to use for layout label.
|
||||
/// - separatorType: Separator type to use for separators.
|
||||
public init(viewText: ViewText,
|
||||
contentBackground: ViewBackground = .color(.clear),
|
||||
contentInsets: UIEdgeInsets = .zero,
|
||||
labelInsets: UIEdgeInsets = .zero,
|
||||
separatorType: CellSeparatorType = .none) {
|
||||
|
||||
self.viewText = viewText
|
||||
self.contentBackground = contentBackground
|
||||
self.contentInsets = contentInsets
|
||||
self.labelInsets = labelInsets
|
||||
self.separatorType = separatorType
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
//
|
||||
// Copyright (c) 2019 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import TableKit
|
||||
import SnapKit
|
||||
|
||||
/// Label cell with separators, includes background image view.
|
||||
open class LabelTableViewCell: SeparatorCell, InitializableView, ConfigurableCell {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
private let label = UILabel()
|
||||
private let backgroundImageView = UIImageView()
|
||||
private let contentContainerView = UIView()
|
||||
|
||||
private var viewModel: LabelCellViewModel?
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
public override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
initializeView()
|
||||
}
|
||||
|
||||
public required init?(coder aDecoder: NSCoder) {
|
||||
super.init(coder: aDecoder)
|
||||
initializeView()
|
||||
}
|
||||
|
||||
open override func updateConstraints() {
|
||||
let topSeparatorHeight = viewModel?.separatorType.topConfiguration?.totalHeight ?? 0
|
||||
let bottomSeparatorHeight = viewModel?.separatorType.bottomConfiguration?.totalHeight ?? 0
|
||||
|
||||
contentContainerView.snp.remakeConstraints { make in
|
||||
make.top.equalToSuperview().inset(contentInsets.top + topSeparatorHeight)
|
||||
make.leading.equalToSuperview().inset(contentInsets.left)
|
||||
make.trailing.equalToSuperview().inset(contentInsets.right)
|
||||
make.bottom.equalToSuperview().inset(contentInsets.bottom + bottomSeparatorHeight)
|
||||
}
|
||||
|
||||
label.snp.remakeConstraints { make in
|
||||
make.edges.equalToSuperview().inset(labelInsets)
|
||||
}
|
||||
|
||||
backgroundImageView.snp.remakeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
|
||||
super.updateConstraints()
|
||||
}
|
||||
|
||||
// MARK: - InitializableView
|
||||
|
||||
open func addViews() {
|
||||
contentView.addSubview(contentContainerView)
|
||||
contentContainerView.addSubviews(backgroundImageView, label)
|
||||
}
|
||||
|
||||
open func configureAppearance() {
|
||||
selectionStyle = .none
|
||||
backgroundColor = .clear
|
||||
|
||||
contentView.backgroundColor = .clear
|
||||
|
||||
configureAppearance(of: label, backgroundImageView: backgroundImageView)
|
||||
}
|
||||
|
||||
// MARK: - ConfigurableCell
|
||||
|
||||
public func configure(with viewModel: LabelCellViewModel) {
|
||||
configureLabelCell(with: viewModel)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private var labelInsets: UIEdgeInsets {
|
||||
viewModel?.labelInsets ?? .zero
|
||||
}
|
||||
|
||||
private var contentInsets: UIEdgeInsets {
|
||||
viewModel?.contentInsets ?? .zero
|
||||
}
|
||||
|
||||
// MARK: - Subclass methods to override
|
||||
|
||||
/// Callback for label and background image view appearance configuration.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - label: Internal UILabel instance to configure.
|
||||
/// - backgroundImageView: Internal UIImageView instance to configure.
|
||||
open func configureAppearance(of label: UILabel, backgroundImageView: UIImageView) {
|
||||
label.numberOfLines = 0
|
||||
}
|
||||
|
||||
// MARK: - Configuration methods
|
||||
|
||||
/// Convenient method for configuration cell with LabelCellViewModel.
|
||||
///
|
||||
/// - Parameter viewModel: LabelCellViewModel instance.
|
||||
public func configureLabelCell(with viewModel: LabelCellViewModel) {
|
||||
self.viewModel = viewModel
|
||||
|
||||
configureSeparator(with: viewModel.separatorType)
|
||||
configureLabelText(with: viewModel.viewText)
|
||||
configureContentBackground(with: viewModel.contentBackground)
|
||||
|
||||
setNeedsUpdateConstraints()
|
||||
}
|
||||
|
||||
/// Method for background configuration.
|
||||
///
|
||||
/// - Parameter contentBackground: Content background to use as background.
|
||||
public func configureContentBackground(with contentBackground: ViewBackground) {
|
||||
contentBackground.configure(backgroundView: contentContainerView,
|
||||
backgroundImageView: backgroundImageView)
|
||||
}
|
||||
|
||||
/// Method for text configuration.
|
||||
///
|
||||
/// - Parameter viewText: View text to use as background.
|
||||
public func configureLabelText(with viewText: ViewText) {
|
||||
label.configure(with: viewText)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,194 @@
|
|||
//
|
||||
// Copyright (c) 2017 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import TableKit
|
||||
|
||||
private enum Constants {
|
||||
static let defaultSeparatorHeight = CGFloat(pixels: 1)
|
||||
}
|
||||
|
||||
/// Base cell that provides separator support
|
||||
/// Take note that:
|
||||
/// - in `configure(with:)` you must call `configureSeparator(with:)`
|
||||
/// - separators are simple views, that located on `contentView`.
|
||||
/// - if you hide that with another view that fully hide you can use that method `moveSeparators(to:)`
|
||||
open class SeparatorCell: UITableViewCell {
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
/// Configure separator with viewModel
|
||||
/// - parameter separatorType: type of separators
|
||||
public func configureSeparator(with separatorType: CellSeparatorType) {
|
||||
topView.isHidden = separatorType.topIsHidden
|
||||
bottomView.isHidden = separatorType.bottomIsHidden
|
||||
|
||||
switch separatorType {
|
||||
case .none:
|
||||
break
|
||||
|
||||
case .bottom(let configuration):
|
||||
updateBottomSeparator(with: configuration)
|
||||
setNeedsUpdateConstraints()
|
||||
|
||||
case .top(let configuration):
|
||||
updateTopSeparator(with: configuration)
|
||||
setNeedsUpdateConstraints()
|
||||
|
||||
case let .full(topConfiguration, bottomConfiguration):
|
||||
updateTopSeparator(with: topConfiguration)
|
||||
updateBottomSeparator(with: bottomConfiguration)
|
||||
setNeedsUpdateConstraints()
|
||||
}
|
||||
}
|
||||
|
||||
/// Move separator upward in hierarchy
|
||||
public func bringSeparatorsToFront() {
|
||||
contentView.bringSubviewToFront(topView)
|
||||
contentView.bringSubviewToFront(bottomView)
|
||||
}
|
||||
|
||||
/// Move separator backward in hierarchy
|
||||
public func sendSeparatorsToBack() {
|
||||
contentView.sendSubviewToBack(topView)
|
||||
contentView.sendSubviewToBack(bottomView)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
// swiftlint:disable implicitly_unwrapped_optional
|
||||
|
||||
private var topView: UIView!
|
||||
private var bottomView: UIView!
|
||||
|
||||
// top separator constraints
|
||||
private var topViewLeftConstraint: NSLayoutConstraint!
|
||||
private var topViewRightConstraint: NSLayoutConstraint!
|
||||
private var topViewTopConstraint: NSLayoutConstraint!
|
||||
private var topViewHeightConstraint: NSLayoutConstraint!
|
||||
|
||||
// bottom separator constraints
|
||||
private var bottomViewLeftConstraint: NSLayoutConstraint!
|
||||
private var bottomViewRightConstraint: NSLayoutConstraint!
|
||||
private var bottomViewBottomConstraint: NSLayoutConstraint!
|
||||
private var bottomViewHeightConstraint: NSLayoutConstraint!
|
||||
|
||||
// swiftlint:enable implicitly_unwrapped_optional
|
||||
|
||||
private var topSeparatorInsets = UIEdgeInsets.zero
|
||||
private var bottomSeparatorInsets = UIEdgeInsets.zero
|
||||
|
||||
private var topSeparatorHeight = Constants.defaultSeparatorHeight
|
||||
private var bottomSeparatorHeight = Constants.defaultSeparatorHeight
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
public required init?(coder aDecoder: NSCoder) {
|
||||
super.init(coder: aDecoder)
|
||||
|
||||
configureLineViews()
|
||||
}
|
||||
|
||||
public override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
|
||||
configureLineViews()
|
||||
}
|
||||
|
||||
override open func updateConstraints() {
|
||||
topViewTopConstraint.constant = topSeparatorInsets.top
|
||||
topViewLeftConstraint.constant = topSeparatorInsets.left
|
||||
topViewRightConstraint.constant = topSeparatorInsets.right
|
||||
topViewHeightConstraint.constant = topSeparatorHeight
|
||||
|
||||
bottomViewLeftConstraint.constant = bottomSeparatorInsets.left
|
||||
bottomViewRightConstraint.constant = bottomSeparatorInsets.right
|
||||
bottomViewBottomConstraint.constant = bottomSeparatorInsets.bottom
|
||||
bottomViewHeightConstraint.constant = bottomSeparatorHeight
|
||||
|
||||
super.updateConstraints()
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func configureLineViews() {
|
||||
topView = createSeparatorView()
|
||||
bottomView = createSeparatorView()
|
||||
|
||||
createConstraints()
|
||||
}
|
||||
|
||||
private func createSeparatorView() -> UIView {
|
||||
let view = UIView()
|
||||
view.isHidden = true
|
||||
view.backgroundColor = .black
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(view)
|
||||
return view
|
||||
}
|
||||
|
||||
private func updateTopSeparator(with configuration: SeparatorConfiguration) {
|
||||
topView.backgroundColor = configuration.color
|
||||
topSeparatorHeight = configuration.height
|
||||
topSeparatorInsets = configuration.insets
|
||||
}
|
||||
|
||||
private func updateBottomSeparator(with configuration: SeparatorConfiguration) {
|
||||
bottomView.backgroundColor = configuration.color
|
||||
bottomSeparatorHeight = configuration.height
|
||||
bottomSeparatorInsets = configuration.insets
|
||||
}
|
||||
|
||||
private func createConstraints() {
|
||||
// height
|
||||
topViewHeightConstraint = topView.heightAnchor.constraint(equalToConstant: topSeparatorHeight)
|
||||
topViewHeightConstraint.isActive = true
|
||||
|
||||
bottomViewHeightConstraint = bottomView.heightAnchor.constraint(equalToConstant: bottomSeparatorHeight)
|
||||
bottomViewHeightConstraint.isActive = true
|
||||
|
||||
// top separator
|
||||
topViewTopConstraint = topView.topAnchor.constraint(equalTo: contentView.topAnchor)
|
||||
topViewTopConstraint.isActive = true
|
||||
|
||||
topViewRightConstraint = contentView.rightAnchor.constraint(equalTo: topView.rightAnchor)
|
||||
topViewRightConstraint.isActive = true
|
||||
|
||||
topViewLeftConstraint = topView.leftAnchor.constraint(equalTo: contentView.leftAnchor)
|
||||
topViewLeftConstraint.isActive = true
|
||||
|
||||
// bottom separator
|
||||
bottomViewRightConstraint = contentView.rightAnchor.constraint(equalTo: bottomView.rightAnchor)
|
||||
bottomViewRightConstraint.isActive = true
|
||||
|
||||
bottomViewLeftConstraint = bottomView.leftAnchor.constraint(equalTo: contentView.leftAnchor)
|
||||
bottomViewLeftConstraint.isActive = true
|
||||
|
||||
bottomViewBottomConstraint = bottomView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
|
||||
bottomViewBottomConstraint.isActive = true
|
||||
}
|
||||
|
||||
open override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
configureSeparator(with: .none)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
//
|
||||
// Copyright (c) 2017 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
/// Separator configuration. Supports positioning, color and height per each separator
|
||||
public struct SeparatorConfiguration {
|
||||
|
||||
public let color: UIColor
|
||||
public let insets: UIEdgeInsets
|
||||
public let height: CGFloat
|
||||
|
||||
/// Initialize configuration with parameters
|
||||
/// - parameter color: Color must be provided
|
||||
/// - parameter insets: Insets for separator. Default is no insets
|
||||
/// - parameter height: Height for separator. Default is 1 pixel
|
||||
/// - returns: Ready to use separator configuration
|
||||
public init(color: UIColor, insets: UIEdgeInsets = .zero, height: CGFloat = CGFloat(pixels: 1)) {
|
||||
self.color = color
|
||||
self.insets = insets
|
||||
self.height = height
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
//
|
||||
// Copyright (c) 2017 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import TableKit
|
||||
|
||||
/// Class that used to configure separators when multiply cells presented in one section
|
||||
/// Holds TableRow<T> with any model inherited from BaseCellViewModel
|
||||
public final class SeparatorRowBox {
|
||||
private let setSeparatorHandler: (CellSeparatorType) -> Void
|
||||
|
||||
public func set(separatorType: CellSeparatorType) {
|
||||
setSeparatorHandler(separatorType)
|
||||
}
|
||||
|
||||
public let row: Row
|
||||
|
||||
/// Initialize AnyBaseTableRow with tableRow
|
||||
/// - parameter row: TableRow which `cell` conforms to SeparatorCell
|
||||
public init<T>(row: TableRow<T>) where T: SeparatorCell {
|
||||
self.row = row
|
||||
setSeparatorHandler = row.set
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,156 @@
|
|||
//
|
||||
// Copyright (c) 2017 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
public final class SpinnerView: UIView, Animatable, LoadingIndicator {
|
||||
|
||||
private var animating: Bool {
|
||||
imageView?.layer.animation(forKey: CABasicAnimation.rotationKeyPath) != nil
|
||||
}
|
||||
|
||||
private var startTime = CFTimeInterval(0)
|
||||
private var stopTime = CFTimeInterval(0)
|
||||
|
||||
private weak var imageView: UIImageView?
|
||||
|
||||
private let hidesWhenStopped: Bool
|
||||
private let animationDuration: CFTimeInterval
|
||||
private let animationRepeatCount: Float
|
||||
private let clockwiseAnimation: Bool
|
||||
|
||||
public init(image: UIImage,
|
||||
hidesWhenStopped: Bool = true,
|
||||
animationDuration: CFTimeInterval = 1,
|
||||
animationRepeatCount: Float = Float.infinity,
|
||||
clockwiseAnimation: Bool = true) {
|
||||
|
||||
self.hidesWhenStopped = hidesWhenStopped
|
||||
self.animationDuration = animationDuration
|
||||
self.animationRepeatCount = animationRepeatCount
|
||||
self.clockwiseAnimation = clockwiseAnimation
|
||||
|
||||
super.init(frame: CGRect(origin: .zero, size: image.size))
|
||||
|
||||
let imageView = UIImageView(image: image)
|
||||
imageView.frame = bounds
|
||||
imageView.autoresizingMask = [
|
||||
.flexibleLeftMargin,
|
||||
.flexibleRightMargin,
|
||||
.flexibleTopMargin,
|
||||
.flexibleBottomMargin
|
||||
]
|
||||
|
||||
addSubview(imageView)
|
||||
|
||||
self.imageView = imageView
|
||||
|
||||
NotificationCenter.default.addObserver(self,
|
||||
selector: #selector(SpinnerView.restartAnimationIfNeeded),
|
||||
name: UIApplication.willEnterForegroundNotification,
|
||||
object: nil)
|
||||
}
|
||||
|
||||
required public init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
|
||||
override public func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
|
||||
if window != nil {
|
||||
restartAnimationIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
override public func didMoveToSuperview() {
|
||||
super.didMoveToSuperview()
|
||||
|
||||
if superview != nil {
|
||||
restartAnimationIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Animatable
|
||||
|
||||
@objc public func startAnimating() {
|
||||
guard !animating else {
|
||||
return
|
||||
}
|
||||
|
||||
if hidesWhenStopped {
|
||||
imageView?.isHidden = false
|
||||
}
|
||||
|
||||
addAnimation()
|
||||
}
|
||||
|
||||
@objc public func stopAnimating() {
|
||||
guard animating else {
|
||||
return
|
||||
}
|
||||
|
||||
if hidesWhenStopped {
|
||||
imageView?.isHidden = true
|
||||
}
|
||||
|
||||
removeAnimation()
|
||||
}
|
||||
|
||||
// MARK: - private stuff
|
||||
|
||||
private func addAnimation() {
|
||||
guard let imageView = imageView else {
|
||||
return
|
||||
}
|
||||
|
||||
let anim = CABasicAnimation.zRotationAnimationWith(duration: animationDuration,
|
||||
repeatCount: animationRepeatCount,
|
||||
clockwise: clockwiseAnimation)
|
||||
anim.timeOffset = stopTime - startTime
|
||||
|
||||
imageView.layer.add(anim, forKey: CABasicAnimation.rotationKeyPath)
|
||||
|
||||
startTime = imageView.layer.convertTime(CACurrentMediaTime(), from: nil)
|
||||
}
|
||||
|
||||
private func removeAnimation() {
|
||||
guard let imageView = imageView else {
|
||||
return
|
||||
}
|
||||
|
||||
imageView.layer.removeAnimation(forKey: CABasicAnimation.rotationKeyPath)
|
||||
|
||||
stopTime = imageView.layer.convertTime(CACurrentMediaTime(), from: nil)
|
||||
}
|
||||
|
||||
@objc private func restartAnimationIfNeeded() {
|
||||
if animating {
|
||||
removeAnimation()
|
||||
addAnimation()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
//
|
||||
// Copyright (c) 2018 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import UIKit.UITableView
|
||||
|
||||
/// The main purpose of this class is to fix empty space on top of the screen
|
||||
/// when view controller view is UITableView.
|
||||
open class TableViewWrapperView: ScrollViewHolderView, TableViewHolder {
|
||||
|
||||
/// Contained table view.
|
||||
public let tableView: UITableView
|
||||
|
||||
/// Initializer with tableViewStyle parameter.
|
||||
///
|
||||
/// - Parameter tableViewStyle: UITableViewStyle to pass in UITableView init.
|
||||
public init(tableViewStyle: UITableView.Style) {
|
||||
self.tableView = UITableView(frame: .zero, style: tableViewStyle)
|
||||
self.tableView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
|
||||
super.init(frame: .zero)
|
||||
|
||||
addSubview(tableView)
|
||||
}
|
||||
|
||||
required public init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
//
|
||||
// Copyright (c) 2018 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import RxSwift
|
||||
import RxCocoa
|
||||
|
||||
/// Class that maps data model field to String and vise-versa.
|
||||
public final class DataModelFieldBinding<T> {
|
||||
|
||||
public typealias GetFieldClosure = (T) -> String?
|
||||
public typealias MergeFieldClosure = (T, String?) -> T
|
||||
|
||||
private let modelRelay: BehaviorRelay<T>
|
||||
private let modelDriver: Driver<T>
|
||||
private let getFieldClosure: DataModelFieldBinding<T>.GetFieldClosure
|
||||
private let mergeFieldClosure: DataModelFieldBinding<T>.MergeFieldClosure
|
||||
|
||||
/// Memberwise initializer.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - modelRelay: BehaviourRelay that contains data model.
|
||||
/// - modelDriver: Driver that emits new data models.
|
||||
/// - getFieldClosure: Closure for getting field string reprerentation from data model.
|
||||
/// - mergeFieldClosure: Closure for merging new field value into data model.
|
||||
public init(modelRelay: BehaviorRelay<T>,
|
||||
modelDriver: Driver<T>,
|
||||
getFieldClosure: @escaping GetFieldClosure,
|
||||
mergeFieldClosure: @escaping MergeFieldClosure) {
|
||||
|
||||
self.modelRelay = modelRelay
|
||||
self.modelDriver = modelDriver
|
||||
self.getFieldClosure = getFieldClosure
|
||||
self.mergeFieldClosure = mergeFieldClosure
|
||||
}
|
||||
|
||||
/// Method that merges new field values with data model.
|
||||
///
|
||||
/// - Parameter textDriver: Driver that emits new text values.
|
||||
/// - Returns: Disposable object that can be used to unsubscribe the observer from the behaviour relay.
|
||||
public func mergeStringToModel(from textDriver: Driver<String?>) -> Disposable {
|
||||
textDriver.map { [modelRelay, mergeFieldClosure] in
|
||||
mergeFieldClosure(modelRelay.value, $0)
|
||||
}
|
||||
.drive(modelRelay)
|
||||
}
|
||||
|
||||
/// A Driver that will emit current field value.
|
||||
public var fieldDriver: Driver<String?> {
|
||||
modelDriver.map(getFieldClosure)
|
||||
}
|
||||
}
|
||||
|
||||
public extension DataModelFieldBinding {
|
||||
|
||||
/// Convenience initializer without modelDriver, which will be obtained from modelRelay.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - modelRelay: BehaviourRelay that contains data model.
|
||||
/// - getFieldClosure: Closure for getting field string reprerentation from data model.
|
||||
/// - mergeFieldClosure: Closure for merging new field value into data model.
|
||||
convenience init(modelRelay: BehaviorRelay<T>,
|
||||
getFieldClosure: @escaping GetFieldClosure,
|
||||
mergeFieldClosure: @escaping MergeFieldClosure) {
|
||||
|
||||
self.init(modelRelay: modelRelay,
|
||||
modelDriver: modelRelay.asDriver(),
|
||||
getFieldClosure: getFieldClosure,
|
||||
mergeFieldClosure: mergeFieldClosure)
|
||||
}
|
||||
}
|
||||
|
||||
public extension DataModelFieldBinding where T == String? {
|
||||
|
||||
/// Convenience initializer for data model of string.
|
||||
///
|
||||
/// - Parameter modelRelay: BehaviourRelay that contains data model.
|
||||
convenience init(modelRelay: BehaviorRelay<T>) {
|
||||
self.init(modelRelay: modelRelay,
|
||||
modelDriver: modelRelay.asDriver(),
|
||||
getFieldClosure: { $0 },
|
||||
mergeFieldClosure: { $1 })
|
||||
}
|
||||
}
|
||||
|
||||
public extension BehaviorRelay {
|
||||
|
||||
/// Creates DataModelFieldBinding configured with given closures and behaviour relay itself.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - getFieldClosure: Closure for getting field string reprerentation from data model.
|
||||
/// - mergeFieldClosure: Closure for merging new field value into data model.
|
||||
/// - Returns: DataModelFieldBinding instance.
|
||||
func fieldBinding(getFieldClosure: @escaping DataModelFieldBinding<Element>.GetFieldClosure,
|
||||
mergeFieldClosure: @escaping DataModelFieldBinding<Element>.MergeFieldClosure)
|
||||
-> DataModelFieldBinding<Element> {
|
||||
|
||||
DataModelFieldBinding(modelRelay: self,
|
||||
getFieldClosure: getFieldClosure,
|
||||
mergeFieldClosure: mergeFieldClosure)
|
||||
}
|
||||
}
|
||||
|
||||
public extension BehaviorRelay where Element == String? {
|
||||
|
||||
/// Creates DataModelFieldBinding configured with behaviour relay itself.
|
||||
///
|
||||
/// - Returns: DataModelFieldBinding instance.
|
||||
func fieldBinding() -> DataModelFieldBinding<Element> {
|
||||
DataModelFieldBinding(modelRelay: self)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
//
|
||||
// Copyright (c) 2018 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import RxSwift
|
||||
import RxCocoa
|
||||
|
||||
/// Class that used for binding text field with upper level view model.
|
||||
open class TextFieldViewModel < ViewEvents: TextFieldViewEvents,
|
||||
ViewModelEvents: TextFieldViewModelEvents> {
|
||||
|
||||
/// Events that can be emitted by view model.
|
||||
public let viewModelEvents: ViewModelEvents
|
||||
|
||||
private let viewEventsRelay = BehaviorRelay<ViewEvents?>(value: nil)
|
||||
|
||||
private(set) public var disposeBag = DisposeBag()
|
||||
|
||||
/// Initializer with view model events.
|
||||
///
|
||||
/// - Parameter viewModelEvents: Events that can be emitted by view model.
|
||||
public init(viewModelEvents: ViewModelEvents) {
|
||||
self.viewModelEvents = viewModelEvents
|
||||
}
|
||||
|
||||
/// View events driver that will emit view events structure
|
||||
/// when view will bind itself to the view model.
|
||||
public var viewEventsDriver: Driver<ViewEvents> {
|
||||
viewEventsRelay
|
||||
.asDriver()
|
||||
.flatMap { viewEvents -> Driver<ViewEvents> in
|
||||
guard let viewEvents = viewEvents else {
|
||||
return .empty()
|
||||
}
|
||||
|
||||
return .just(viewEvents)
|
||||
}
|
||||
}
|
||||
|
||||
/// Method that performs binding view events to view model.
|
||||
///
|
||||
/// - Parameter viewEvents: View events structure.
|
||||
public func bind(viewEvents: ViewEvents) {
|
||||
viewEventsRelay.accept(viewEvents)
|
||||
}
|
||||
|
||||
/// Unbinds view from view model.
|
||||
public func unbindView() {
|
||||
disposeBag = DisposeBag()
|
||||
}
|
||||
}
|
||||
|
||||
public extension TextFieldViewModel {
|
||||
|
||||
typealias MapViewEventClosure = (ViewEvents) -> Disposable
|
||||
|
||||
/// Convenient method for binding to the current view events structure.
|
||||
///
|
||||
/// - Parameter closure: Closure that takes a view events parameter and returns Disposable.
|
||||
/// - Returns: Disposable object that can be used to unsubscribe the observer from the binding.
|
||||
func mapViewEvents(_ closure: @escaping MapViewEventClosure) -> Disposable {
|
||||
mapViewEvents { [closure($0)] }
|
||||
}
|
||||
|
||||
typealias MapViewEventsClosure = (ViewEvents) -> [Disposable]
|
||||
|
||||
/// Convenient method for binding to the current view events structure.
|
||||
///
|
||||
/// - Parameter closure: Closure that takes a view events parameter and returns [Disposable].
|
||||
/// - Returns: Disposable object that can be used to unsubscribe the observer from the binding.
|
||||
func mapViewEvents(_ closure: @escaping MapViewEventsClosure) -> Disposable {
|
||||
viewEventsDriver
|
||||
.map { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
closure($0).forEach { $0.disposed(by: strongSelf.disposeBag) }
|
||||
}
|
||||
.drive()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
//
|
||||
// Copyright (c) 2018 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import UIKit.UIFont
|
||||
import UIKit.UIColor
|
||||
|
||||
/// Base set of attributes to configure appearance of text.
|
||||
open class BaseTextAttributes {
|
||||
|
||||
/// Text font.
|
||||
public let font: UIFont
|
||||
/// Text color.
|
||||
public let color: UIColor
|
||||
/// Text alignment.
|
||||
public let alignment: NSTextAlignment
|
||||
|
||||
/// Memberwise initializer.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - font: Text font.
|
||||
/// - color: Text color.
|
||||
/// - alignment: Text alignment.
|
||||
public init(font: UIFont, color: UIColor, alignment: NSTextAlignment = .natural) {
|
||||
self.font = font
|
||||
self.color = color
|
||||
self.alignment = alignment
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
//
|
||||
// Copyright (c) 2017 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
/// Class used to instantiate custom view in storyboards
|
||||
open class XibView: UIView {
|
||||
|
||||
/// Nib name used to instantiate inner view
|
||||
/// - NOTE: Be very carefully when you're intending to change this line
|
||||
open var innerViewNibName: String {
|
||||
typeName(of: type(of: self))
|
||||
}
|
||||
|
||||
public convenience init() {
|
||||
self.init(frame: .zero)
|
||||
}
|
||||
|
||||
public override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
setupView()
|
||||
}
|
||||
|
||||
public required init?(coder aDecoder: NSCoder) {
|
||||
super.init(coder: aDecoder)
|
||||
setupView()
|
||||
}
|
||||
|
||||
private func setupView() {
|
||||
let view = UIView.loadFromNib(named: innerViewNibName, owner: self) as UIView
|
||||
|
||||
// Make frame size match the size of the content view in the xib
|
||||
frame = CGRect(origin: frame.origin, size: view.frame.size)
|
||||
|
||||
view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
|
||||
addSubview(view)
|
||||
|
||||
configure()
|
||||
}
|
||||
|
||||
/// Provide initial configuration. Called once
|
||||
open func configure() {
|
||||
}
|
||||
}
|
||||
|
|
@ -24,11 +24,8 @@ import Foundation
|
|||
|
||||
/// A type representing an possible errors that can be thrown during working with cursor object
|
||||
///
|
||||
/// - busy: cursor is currently processing another request
|
||||
/// - exhausted: cursor did load all available results
|
||||
public enum CursorError: Error {
|
||||
|
||||
case busy
|
||||
case exhausted
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
//
|
||||
// Copyright (c) 2018 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
/// Enum that contains states for general data loading.
|
||||
///
|
||||
/// - initial: Initial state. Before something will happen.
|
||||
/// - loading: Loading state. When data loading is started.
|
||||
/// - result: Result state from a specific data source with result.
|
||||
/// - error: Error state with a specific error.
|
||||
/// - empty: Empty state. When data was requested and empty result was received.
|
||||
public enum GeneralDataLoadingState<DS: DataSource> {
|
||||
|
||||
case initial
|
||||
case loading
|
||||
case result(newResult: DS.ResultType, from: DS)
|
||||
case error(error: Error)
|
||||
case empty
|
||||
}
|
||||
|
||||
extension GeneralDataLoadingState: DataLoadingState {
|
||||
|
||||
public typealias DataSourceType = DS
|
||||
|
||||
public static var initialState: GeneralDataLoadingState<DS> {
|
||||
.initial
|
||||
}
|
||||
|
||||
public static var emptyState: GeneralDataLoadingState<DS> {
|
||||
.empty
|
||||
}
|
||||
|
||||
public static func initialLoadingState(after: GeneralDataLoadingState<DS>) -> GeneralDataLoadingState<DS> {
|
||||
.loading
|
||||
}
|
||||
|
||||
public static func resultState(result: DS.ResultType,
|
||||
from: DS,
|
||||
after: GeneralDataLoadingState<DS>) -> GeneralDataLoadingState<DS> {
|
||||
|
||||
.result(newResult: result, from: from)
|
||||
}
|
||||
|
||||
public static func errorState(error: Error,
|
||||
after: GeneralDataLoadingState<DS>) -> GeneralDataLoadingState<DS> {
|
||||
|
||||
.error(error: error)
|
||||
}
|
||||
|
||||
public var isInitialState: Bool {
|
||||
switch self {
|
||||
case .initial:
|
||||
return true
|
||||
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
public var result: DS.ResultType? {
|
||||
switch self {
|
||||
case .result(let newResult, _):
|
||||
return newResult
|
||||
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public var error: Error? {
|
||||
switch self {
|
||||
case .error(let error):
|
||||
return error
|
||||
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
//
|
||||
// Copyright (c) 2018 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
/// Enum that contains states for paginated data loading.
|
||||
///
|
||||
/// - initial: Initial state. Before something will happen.
|
||||
/// - initialLoading: Initial loading state. When data was requested initially.
|
||||
/// - loadingMore: Loading more state. When additional data was requested.
|
||||
/// - results: Result state from a specific data source after a given state.
|
||||
/// - error: Error state with a specific error after a given state.
|
||||
/// - empty: Empty state. When data was initially requested and empty result was received.
|
||||
/// - exhausted: Exhausted state. When no more results can be received.
|
||||
public indirect enum PaginationDataLoadingState<DS: DataSource> {
|
||||
|
||||
case initial
|
||||
case initialLoading(after: PaginationDataLoadingState)
|
||||
case loadingMore(after: PaginationDataLoadingState)
|
||||
case results(newItems: DS.ResultType, from: DS, after: PaginationDataLoadingState)
|
||||
case error(error: Error, after: PaginationDataLoadingState)
|
||||
case empty
|
||||
case exhausted
|
||||
}
|
||||
|
||||
extension PaginationDataLoadingState: DataLoadingState {
|
||||
|
||||
public typealias DataSourceType = DS
|
||||
|
||||
public static var initialState: PaginationDataLoadingState<DS> {
|
||||
.initial
|
||||
}
|
||||
|
||||
public static var emptyState: PaginationDataLoadingState<DS> {
|
||||
.empty
|
||||
}
|
||||
|
||||
public static func initialLoadingState(after: PaginationDataLoadingState<DS>) -> PaginationDataLoadingState<DS> {
|
||||
.initialLoading(after: after)
|
||||
}
|
||||
|
||||
public static func resultState(result: DS.ResultType,
|
||||
from: DataSourceType,
|
||||
after: PaginationDataLoadingState<DS>) -> PaginationDataLoadingState<DS> {
|
||||
|
||||
.results(newItems: result, from: from, after: after)
|
||||
}
|
||||
|
||||
public static func errorState(error: Error,
|
||||
after: PaginationDataLoadingState<DS>) -> PaginationDataLoadingState<DS> {
|
||||
|
||||
.error(error: error, after: after)
|
||||
}
|
||||
|
||||
public var isInitialState: Bool {
|
||||
switch self {
|
||||
case .initial:
|
||||
return true
|
||||
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
public var result: DS.ResultType? {
|
||||
switch self {
|
||||
case .results(let newItems, _, _):
|
||||
return newItems
|
||||
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public var error: Error? {
|
||||
switch self {
|
||||
case .error(let error, _):
|
||||
return error
|
||||
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -22,11 +22,14 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
/// Enum which represents common errors in LeadKit framework
|
||||
/// Enum that represents common errors in LeadKit framework
|
||||
///
|
||||
/// - failedToCastValue: attempt to cast was failed
|
||||
/// - failedToDecode: attempt to decoding was failed
|
||||
/// - failedToEncodeQueryItems: attempt to encoding to query items was failed
|
||||
public enum LeadKitError: Error {
|
||||
|
||||
case failedToCastValue(expectedType: Any.Type, givenType: Any.Type)
|
||||
|
||||
case failedToDecode(reason: String)
|
||||
case failedToEncodeQueryItems
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue