Compare commits
469 Commits
feature/re
...
master
| 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 |
|
|
@ -1,3 +1,3 @@
|
||||||
[submodule "build-scripts"]
|
[submodule "build-scripts"]
|
||||||
path = build-scripts
|
path = build-scripts
|
||||||
url = https://github.com/TouchInstinct/BuildScripts.git
|
url = https://git.svc.touchin.ru/TouchInstinct/BuildScripts.git
|
||||||
|
|
|
||||||
287
CHANGELOG.md
287
CHANGELOG.md
|
|
@ -1,5 +1,292 @@
|
||||||
# Changelog
|
# 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
|
### 1.20.0
|
||||||
|
|
||||||
- **Add**: OpenAPI security schemes support for EndpointRequest's.
|
- **Add**: OpenAPI security schemes support for EndpointRequest's.
|
||||||
|
|
|
||||||
|
|
@ -89,9 +89,10 @@ GEM
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
x86_64-darwin-20
|
x86_64-darwin-20
|
||||||
|
x86_64-darwin-21
|
||||||
|
|
||||||
DEPENDENCIES
|
DEPENDENCIES
|
||||||
cocoapods (~> 1.11)
|
cocoapods (~> 1.11)
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
2.3.10
|
2.3.26
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
Pod::Spec.new do |s|
|
Pod::Spec.new do |s|
|
||||||
s.name = "LeadKit"
|
s.name = "LeadKit"
|
||||||
s.version = "1.20.0"
|
s.version = "1.35.0"
|
||||||
s.summary = "iOS framework with a bunch of tools for rapid development"
|
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.license = "Apache License, Version 2.0"
|
||||||
s.author = "Touch Instinct"
|
s.author = "Touch Instinct"
|
||||||
s.source = { :git => "https://github.com/TouchInstinct/LeadKit.git", :tag => s.version }
|
s.source = { :git => "https://git.svc.touchin.ru/TouchInstinct/LeadKit.git", :tag => s.version }
|
||||||
s.platform = :ios, '10.0'
|
s.platform = :ios, '10.0'
|
||||||
s.swift_versions = ['5.1']
|
s.swift_versions = ['5.1']
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
122
Package.resolved
122
Package.resolved
|
|
@ -1,79 +1,95 @@
|
||||||
{
|
{
|
||||||
"object": {
|
"pins" : [
|
||||||
"pins": [
|
|
||||||
{
|
{
|
||||||
"package": "Alamofire",
|
"identity" : "alamofire",
|
||||||
"repositoryURL": "https://github.com/Alamofire/Alamofire.git",
|
"kind" : "remoteSourceControl",
|
||||||
"state": {
|
"location" : "https://github.com/Alamofire/Alamofire.git",
|
||||||
"branch": null,
|
"state" : {
|
||||||
"revision": "f96b619bcb2383b43d898402283924b80e2c4bae",
|
"revision" : "bc268c28fb170f494de9e9927c371b8342979ece",
|
||||||
"version": "5.4.3"
|
"version" : "5.7.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"package": "Cache",
|
"identity" : "antlr4",
|
||||||
"repositoryURL": "https://github.com/hyperoslo/Cache.git",
|
"kind" : "remoteSourceControl",
|
||||||
"state": {
|
"location" : "https://github.com/antlr/antlr4",
|
||||||
"branch": null,
|
"state" : {
|
||||||
"revision": "c7f4d633049c3bd649a353bad36f6c17e9df085f",
|
"revision" : "44d87bc1d130c88aa452894aa5f7e2f710f68253",
|
||||||
"version": "6.0.0"
|
"version" : "4.10.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"package": "Cursors",
|
"identity" : "cache",
|
||||||
"repositoryURL": "https://github.com/petropavel13/Cursors",
|
"kind" : "remoteSourceControl",
|
||||||
"state": {
|
"location" : "https://github.com/hyperoslo/Cache.git",
|
||||||
"branch": null,
|
"state" : {
|
||||||
"revision": "a1561869135e72832eff3b1e729075c56c2eebf6",
|
"revision" : "c7f4d633049c3bd649a353bad36f6c17e9df085f",
|
||||||
"version": "0.5.1"
|
"version" : "6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"package": "KeychainAccess",
|
"identity" : "cursors",
|
||||||
"repositoryURL": "https://github.com/kishikawakatsumi/KeychainAccess.git",
|
"kind" : "remoteSourceControl",
|
||||||
"state": {
|
"location" : "https://github.com/petropavel13/Cursors",
|
||||||
"branch": null,
|
"state" : {
|
||||||
"revision": "84e546727d66f1adc5439debad16270d0fdd04e7",
|
"revision" : "52f27b82cb1cbbc2b5fd09514c48b9c75e3b0300",
|
||||||
"version": "4.2.2"
|
"version" : "0.6.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"package": "Moya",
|
"identity" : "keychainaccess",
|
||||||
"repositoryURL": "https://github.com/Moya/Moya.git",
|
"kind" : "remoteSourceControl",
|
||||||
"state": {
|
"location" : "https://github.com/kishikawakatsumi/KeychainAccess.git",
|
||||||
"branch": null,
|
"state" : {
|
||||||
"revision": "9b906860e3c3c09032879465c471e6375829593f",
|
"revision" : "84e546727d66f1adc5439debad16270d0fdd04e7",
|
||||||
"version": "15.0.0"
|
"version" : "4.2.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"package": "ReactiveSwift",
|
"identity" : "moya",
|
||||||
"repositoryURL": "https://github.com/ReactiveCocoa/ReactiveSwift.git",
|
"kind" : "remoteSourceControl",
|
||||||
"state": {
|
"location" : "https://github.com/Moya/Moya.git",
|
||||||
"branch": null,
|
"state" : {
|
||||||
"revision": "c43bae3dac73fdd3cb906bd5a1914686ca71ed3c",
|
"revision" : "c263811c1f3dbf002be9bd83107f7cdc38992b26",
|
||||||
"version": "6.7.0"
|
"version" : "15.0.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"package": "RxSwift",
|
"identity" : "panmodal",
|
||||||
"repositoryURL": "https://github.com/ReactiveX/RxSwift.git",
|
"kind" : "remoteSourceControl",
|
||||||
"state": {
|
"location" : "https://git.svc.touchin.ru/TouchInstinct/PanModal",
|
||||||
"branch": null,
|
"state" : {
|
||||||
"revision": "b4307ba0b6425c0ba4178e138799946c3da594f8",
|
"revision" : "ced7c1703f90746df0224b6e0d33c146d9ae4284",
|
||||||
"version": "6.5.0"
|
"version" : "1.3.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"package": "TableKit",
|
"identity" : "reactiveswift",
|
||||||
"repositoryURL": "https://github.com/maxsokolov/TableKit.git",
|
"kind" : "remoteSourceControl",
|
||||||
"state": {
|
"location" : "https://github.com/ReactiveCocoa/ReactiveSwift.git",
|
||||||
"branch": null,
|
"state" : {
|
||||||
"revision": "8bf4840d9d0475a92352f02f368f88b74eced447",
|
"revision" : "c43bae3dac73fdd3cb906bd5a1914686ca71ed3c",
|
||||||
"version": "2.11.0"
|
"version" : "6.7.0"
|
||||||
}
|
}
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"version": 1
|
{
|
||||||
|
"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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
140
Package.swift
140
Package.swift
|
|
@ -1,25 +1,39 @@
|
||||||
// swift-tools-version:5.1
|
// swift-tools-version:5.7
|
||||||
|
|
||||||
|
#if canImport(PackageDescription)
|
||||||
|
|
||||||
import PackageDescription
|
import PackageDescription
|
||||||
|
|
||||||
let package = Package(
|
let package = Package(
|
||||||
name: "LeadKit",
|
name: "LeadKit",
|
||||||
platforms: [
|
platforms: [
|
||||||
.iOS(.v11)
|
.iOS(.v12)
|
||||||
],
|
],
|
||||||
products: [
|
products: [
|
||||||
|
|
||||||
|
// MARK: - Application
|
||||||
|
|
||||||
|
.library(name: "TIApplication", targets: ["TIApplication"]),
|
||||||
|
|
||||||
// MARK: - UIKit
|
// MARK: - UIKit
|
||||||
|
|
||||||
.library(name: "TIUIKitCore", targets: ["TIUIKitCore"]),
|
.library(name: "TIUIKitCore", targets: ["TIUIKitCore"]),
|
||||||
.library(name: "TIUIElements", targets: ["TIUIElements"]),
|
.library(name: "TIUIElements", targets: ["TIUIElements"]),
|
||||||
|
.library(name: "TIWebView", targets: ["TIWebView"]),
|
||||||
|
.library(name: "TIBottomSheet", targets: ["TIBottomSheet"]),
|
||||||
|
|
||||||
// MARK: - SwiftUI
|
// MARK: - SwiftUI
|
||||||
|
|
||||||
.library(name: "TISwiftUICore", targets: ["TISwiftUICore"]),
|
.library(name: "TISwiftUICore", targets: ["TISwiftUICore"]),
|
||||||
|
|
||||||
// MARK: - Utils
|
// MARK: - Utils
|
||||||
.library(name: "TISwiftUtils", targets: ["TISwiftUtils"]),
|
.library(name: "TISwiftUtils", targets: ["TISwiftUtils"]),
|
||||||
.library(name: "TIFoundationUtils", targets: ["TIFoundationUtils"]),
|
.library(name: "TIFoundationUtils", targets: ["TIFoundationUtils"]),
|
||||||
|
.library(name: "TICoreGraphicsUtils", targets: ["TICoreGraphicsUtils"]),
|
||||||
.library(name: "TIKeychainUtils", targets: ["TIKeychainUtils"]),
|
.library(name: "TIKeychainUtils", targets: ["TIKeychainUtils"]),
|
||||||
.library(name: "TITableKitUtils", targets: ["TITableKitUtils"]),
|
.library(name: "TITableKitUtils", targets: ["TITableKitUtils"]),
|
||||||
|
.library(name: "TIDeeplink", targets: ["TIDeeplink"]),
|
||||||
|
.library(name: "TIDeveloperUtils", targets: ["TIDeveloperUtils"]),
|
||||||
|
|
||||||
// MARK: - Networking
|
// MARK: - Networking
|
||||||
|
|
||||||
|
|
@ -33,52 +47,136 @@ let package = Package(
|
||||||
.library(name: "TIAppleMapUtils", targets: ["TIAppleMapUtils"]),
|
.library(name: "TIAppleMapUtils", targets: ["TIAppleMapUtils"]),
|
||||||
|
|
||||||
// MARK: - Elements
|
// MARK: - Elements
|
||||||
|
|
||||||
.library(name: "OTPSwiftView", targets: ["OTPSwiftView"]),
|
.library(name: "OTPSwiftView", targets: ["OTPSwiftView"]),
|
||||||
.library(name: "TITransitions", targets: ["TITransitions"]),
|
.library(name: "TITransitions", targets: ["TITransitions"]),
|
||||||
.library(name: "TIPagination", targets: ["TIPagination"]),
|
.library(name: "TIPagination", targets: ["TIPagination"]),
|
||||||
.library(name: "TIAuth", targets: ["TIAuth"]),
|
.library(name: "TIAuth", targets: ["TIAuth"]),
|
||||||
|
.library(name: "TIEcommerce", targets: ["TIEcommerce"]),
|
||||||
|
.library(name: "TITextProcessing", targets: ["TITextProcessing"])
|
||||||
],
|
],
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.package(url: "https://github.com/maxsokolov/TableKit.git", .upToNextMajor(from: "2.11.0")),
|
.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/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/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/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/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/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: [
|
targets: [
|
||||||
|
|
||||||
|
// MARK: - Application architecture
|
||||||
|
|
||||||
|
.target(name: "TIApplication",
|
||||||
|
dependencies: ["TILogging", "TIFoundationUtils", "KeychainAccess"],
|
||||||
|
path: "TIApplication/Sources",
|
||||||
|
plugins: [.plugin(name: "TISwiftLintPlugin")]),
|
||||||
|
|
||||||
// MARK: - UIKit
|
// MARK: - UIKit
|
||||||
.target(name: "TIUIKitCore", path: "TIUIKitCore/Sources"),
|
|
||||||
.target(name: "TIUIElements", dependencies: ["TIUIKitCore", "TISwiftUtils"], path: "TIUIElements/Sources"),
|
.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
|
// MARK: - SwiftUI
|
||||||
|
|
||||||
.target(name: "TISwiftUICore", path: "TISwiftUICore/Sources"),
|
.target(name: "TISwiftUICore",
|
||||||
|
dependencies: ["TIUIKitCore", "TISwiftUtils"],
|
||||||
|
path: "TISwiftUICore/Sources"),
|
||||||
|
|
||||||
// MARK: - Utils
|
// MARK: - Utils
|
||||||
.target(name: "TISwiftUtils", path: "TISwiftUtils/Sources"),
|
|
||||||
.target(name: "TIFoundationUtils", dependencies: ["TISwiftUtils"], path: "TIFoundationUtils"),
|
.target(name: "TISwiftUtils",
|
||||||
.target(name: "TIKeychainUtils", dependencies: ["TIFoundationUtils", "KeychainAccess"], path: "TIKeychainUtils/Sources"),
|
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: "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
|
// MARK: - Networking
|
||||||
|
|
||||||
.target(name: "TINetworking", dependencies: ["TISwiftUtils", "Alamofire"], path: "TINetworking/Sources"),
|
.target(name: "TINetworking",
|
||||||
.target(name: "TIMoyaNetworking", dependencies: ["TINetworking", "TIFoundationUtils", "Moya"], path: "TIMoyaNetworking"),
|
dependencies: ["TIFoundationUtils", "Alamofire", "TILogging"],
|
||||||
.target(name: "TINetworkingCache", dependencies: ["TIFoundationUtils", "TINetworking", "Cache"], path: "TINetworkingCache/Sources"),
|
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
|
// MARK: - Maps
|
||||||
|
|
||||||
.target(name: "TIMapUtils", dependencies: [], path: "TIMapUtils/Sources"),
|
.target(name: "TIMapUtils",
|
||||||
.target(name: "TIAppleMapUtils", dependencies: ["TIMapUtils"], path: "TIAppleMapUtils/Sources"),
|
dependencies: ["TILogging", "TICoreGraphicsUtils"],
|
||||||
|
path: "TIMapUtils/Sources",
|
||||||
|
plugins: [.plugin(name: "TISwiftLintPlugin")]),
|
||||||
|
|
||||||
|
.target(name: "TIAppleMapUtils",
|
||||||
|
dependencies: ["TIMapUtils"],
|
||||||
|
path: "TIAppleMapUtils/Sources",
|
||||||
|
plugins: [.plugin(name: "TISwiftLintPlugin")]),
|
||||||
|
|
||||||
// MARK: - Elements
|
// MARK: - Elements
|
||||||
|
|
||||||
.target(name: "OTPSwiftView", dependencies: ["TIUIElements"], path: "OTPSwiftView/Sources"),
|
.target(name: "OTPSwiftView", dependencies: ["TIUIElements"], path: "OTPSwiftView/Sources"),
|
||||||
.target(name: "TITransitions", path: "TITransitions/Sources"),
|
.target(name: "TITransitions", path: "TITransitions/Sources"),
|
||||||
.target(name: "TIPagination", dependencies: ["Cursors", "TISwiftUtils"], path: "TIPagination/Sources"),
|
.target(name: "TIPagination", dependencies: ["Cursors", "TISwiftUtils"], path: "TIPagination/Sources"),
|
||||||
.target(name: "TIAuth", dependencies: ["TIFoundationUtils"], path: "TIAuth/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
|
// MARK: - Tests
|
||||||
|
|
||||||
|
|
@ -86,5 +184,15 @@ let package = Package(
|
||||||
name: "TITimerTests",
|
name: "TITimerTests",
|
||||||
dependencies: ["TIFoundationUtils"],
|
dependencies: ["TIFoundationUtils"],
|
||||||
path: "Tests/TITimerTests"),
|
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)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
110
README.md
110
README.md
|
|
@ -1,23 +1,105 @@
|
||||||
# LeadKit
|
# LeadKit
|
||||||
|
|
||||||
LeadKit is the 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.
|
||||||
|
|
||||||
## Additional
|
This repository contains the following frameworks:
|
||||||
|
|
||||||
This repository contains the following additional 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.
|
- [TIUIKitCore](TIUIKitCore) - core ui elements and protocols from LeadKit.
|
||||||
- [TITransitions](TITransitions) - set of custom transitions to present controller.
|
- [TISwiftUICore](TISwiftUICore) Core UI elements: protocols, views and helpers.
|
||||||
- [TIUIElements](TIUIElements) - bunch of of useful protocols and views.
|
- [TIUIElements](TIUIElements) - bunch of of useful protocols and views.
|
||||||
- [OTPSwiftView](OTPSwiftView) - a fully customizable OTP view.
|
- [OTPSwiftView](OTPSwiftView) - a fully customizable OTP view.
|
||||||
- [TISwiftUtils](TISwiftUtils) - a bunch of useful helpers for development.
|
- [TITableKitUtils](TITableKitUtils) - set of helpers for TableKit classes.
|
||||||
- [TITableKitUtils](TITableKitUtils) - Set of helpers for TableKit classes.
|
- [TIKeychainUtils](TIKeychainUtils) - set of helpers for Keychain classes.
|
||||||
- [TIFoundationUtils](TIFoundationUtils) - Set of helpers for Foundation framework classes.
|
|
||||||
- [TIKeychainUtils](TIKeychainUtils) - Set of helpers for Keychain classes.
|
|
||||||
- [TIPagination](TIPagination) - realisation of paginating items from a data source.
|
- [TIPagination](TIPagination) - realisation of paginating items from a data source.
|
||||||
- [TINetworking](TINetworking) - Swagger-frendly networking layer helpers.
|
- [TINetworking](TINetworking) - Swagger-frendly networking layer helpers.
|
||||||
- [TIMoyaNetworking](TIMoyaNetworking) - Moya + Swagger network service.
|
- [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
|
||||||
|
|
||||||
Useful docs:
|
## 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.
|
- [Semantic Commit Messages](docs/semantic-commit-messages.md) - commit message codestyle.
|
||||||
- [Snippets](docs/snippets.md) - useful commands and scripts for development.
|
- [Snippets](docs/snippets.md) - useful commands and scripts for development.
|
||||||
|
|
||||||
|
|
@ -28,7 +110,7 @@ Useful docs:
|
||||||
./setup
|
./setup
|
||||||
```
|
```
|
||||||
|
|
||||||
- If legacy [Source](https://github.com/TouchInstinct/LeadKit/tree/master/Sources) folder needed, [build dependencies for LeadKit.xcodeproj](https://github.com/TouchInstinct/LeadKit/blob/master/docs/snippets.md#build-dependencies-for-LeadKit.xcodeproj).
|
- 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).
|
- Make sure the commit message codestyle is followed. More about [Semantic Commit Messages](docs/semantic-commit-messages.md).
|
||||||
|
|
||||||
|
|
@ -38,16 +120,20 @@ Useful docs:
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.package(url: "https://github.com/TouchInstinct/LeadKit.git", from: "x.y.z"),
|
.package(url: "https://git.svc.touchin.ru/TouchInstinct/LeadKit.git", from: "x.y.z"),
|
||||||
],
|
],
|
||||||
```
|
```
|
||||||
|
|
||||||
### Cocoapods
|
### Cocoapods
|
||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
source 'https://github.com/TouchInstinct/Podspecs.git'
|
source 'https://git.svc.touchin.ru/TouchInstinct/Podspecs.git'
|
||||||
|
|
||||||
pod 'TISwiftUtils', 'x.y.z'
|
pod 'TISwiftUtils', 'x.y.z'
|
||||||
pod 'TIFoundationUtils', '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.
|
||||||
|
|
|
||||||
|
|
@ -31,8 +31,8 @@ import Alamofire
|
||||||
/// - mapping: Errors that occurs during mapping json into model.
|
/// - mapping: Errors that occurs during mapping json into model.
|
||||||
public enum RequestError: Error {
|
public enum RequestError: Error {
|
||||||
|
|
||||||
case noConnection
|
case noConnection(url: String?)
|
||||||
case network(error: Error, response: Data?)
|
case network(error: Error, response: Data?, url: String?)
|
||||||
case invalidResponse(error: AFError, response: Data?)
|
case invalidResponse(error: AFError, response: Data?, url: String?)
|
||||||
case mapping(error: Error, response: Data)
|
case mapping(error: Error, response: Data, url: String?)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -80,9 +80,9 @@ public extension ObservableType where Element == DataRequest {
|
||||||
///
|
///
|
||||||
/// - Parameter statusCodes: set of status codes to validate
|
/// - Parameter statusCodes: set of status codes to validate
|
||||||
/// - Returns: Observable on self
|
/// - Returns: Observable on self
|
||||||
func validate(statusCodes: Set<Int>) -> Observable<Element> {
|
func validate(statusCodes: Set<Int>, url: String? = nil) -> Observable<Element> {
|
||||||
map { $0.validate(statusCode: statusCodes) }
|
map { $0.validate(statusCode: statusCodes) }
|
||||||
.catchAsRequestError()
|
.catchAsRequestError(url: url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -93,7 +93,9 @@ private extension ObservableType where Element == ServerResponse {
|
||||||
do {
|
do {
|
||||||
return try transform($0)
|
return try transform($0)
|
||||||
} catch {
|
} catch {
|
||||||
throw RequestError.mapping(error: error, response: $0.1)
|
throw RequestError.mapping(error: error,
|
||||||
|
response: $0.1,
|
||||||
|
url: $0.0.url?.absoluteString)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -103,10 +105,14 @@ private extension ObservableType where Element == ServerResponse {
|
||||||
do {
|
do {
|
||||||
return try transform((response, result))
|
return try transform((response, result))
|
||||||
.catch {
|
.catch {
|
||||||
throw RequestError.mapping(error: $0, response: result)
|
throw RequestError.mapping(error: $0,
|
||||||
|
response: result,
|
||||||
|
url: response.url?.absoluteString)
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
throw RequestError.mapping(error: error, response: result)
|
throw RequestError.mapping(error: error,
|
||||||
|
response: result,
|
||||||
|
url: response.url?.absoluteString)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -114,7 +120,8 @@ private extension ObservableType where Element == ServerResponse {
|
||||||
|
|
||||||
private extension ObservableType {
|
private extension ObservableType {
|
||||||
|
|
||||||
func catchAsRequestError(with request: DataRequest? = nil) -> Observable<Element> {
|
func catchAsRequestError(with request: DataRequest? = nil,
|
||||||
|
url: String? = nil) -> Observable<Element> {
|
||||||
self.catch { error in
|
self.catch { error in
|
||||||
let resultError: RequestError
|
let resultError: RequestError
|
||||||
let response = request?.data
|
let response = request?.data
|
||||||
|
|
@ -126,10 +133,10 @@ private extension ObservableType {
|
||||||
case let urlError as URLError:
|
case let urlError as URLError:
|
||||||
switch urlError.code {
|
switch urlError.code {
|
||||||
case .notConnectedToInternet:
|
case .notConnectedToInternet:
|
||||||
resultError = .noConnection
|
resultError = .noConnection(url: url)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
resultError = .network(error: urlError, response: response)
|
resultError = .network(error: urlError, response: response, url: url)
|
||||||
}
|
}
|
||||||
|
|
||||||
case let afError as AFError:
|
case let afError as AFError:
|
||||||
|
|
@ -137,21 +144,21 @@ private extension ObservableType {
|
||||||
case let .sessionTaskFailed(error):
|
case let .sessionTaskFailed(error):
|
||||||
switch error {
|
switch error {
|
||||||
case let urlError as URLError where urlError.code == .notConnectedToInternet:
|
case let urlError as URLError where urlError.code == .notConnectedToInternet:
|
||||||
resultError = .noConnection
|
resultError = .noConnection(url: url)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
resultError = .network(error: error, response: response)
|
resultError = .network(error: error, response: response, url: url)
|
||||||
}
|
}
|
||||||
|
|
||||||
case .responseSerializationFailed, .responseValidationFailed:
|
case .responseSerializationFailed, .responseValidationFailed:
|
||||||
resultError = .invalidResponse(error: afError, response: response)
|
resultError = .invalidResponse(error: afError, response: response, url: url)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
resultError = .network(error: afError, response: response)
|
resultError = .network(error: afError, response: response, url: url)
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
resultError = .network(error: error, response: response)
|
resultError = .network(error: error, response: response, url: url)
|
||||||
}
|
}
|
||||||
|
|
||||||
throw resultError
|
throw resultError
|
||||||
|
|
|
||||||
|
|
@ -117,7 +117,8 @@ public extension Reactive where Base: SessionManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
return requestObservable
|
return requestObservable
|
||||||
.validate(statusCodes: self.base.acceptableStatusCodes.union(additionalValidStatusCodes))
|
.validate(statusCodes: self.base.acceptableStatusCodes.union(additionalValidStatusCodes),
|
||||||
|
url: url.absoluteString)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -191,7 +192,8 @@ public extension Reactive where Base: SessionManager {
|
||||||
|
|
||||||
return self.upload(data, urlRequest: urlRequest)
|
return self.upload(data, urlRequest: urlRequest)
|
||||||
.map { $0 as DataRequest }
|
.map { $0 as DataRequest }
|
||||||
.validate(statusCodes: self.base.acceptableStatusCodes.union(additionalValidStatusCodes))
|
.validate(statusCodes: self.base.acceptableStatusCodes.union(additionalValidStatusCodes),
|
||||||
|
url: try? requestParameters.url.asURL().absoluteString)
|
||||||
.flatMap {
|
.flatMap {
|
||||||
$0.rx.apiResponse(mappingQueue: self.base.mappingQueue, decoder: decoder)
|
$0.rx.apiResponse(mappingQueue: self.base.mappingQueue, decoder: decoder)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ public extension Error {
|
||||||
/// - Returns: optional target object
|
/// - Returns: optional target object
|
||||||
/// - Throws: an error during decoding
|
/// - Throws: an error during decoding
|
||||||
func handleMappingError<T: Decodable>(with decoder: JSONDecoder = JSONDecoder()) throws -> T? {
|
func handleMappingError<T: Decodable>(with decoder: JSONDecoder = JSONDecoder()) throws -> T? {
|
||||||
guard let self = requestError, case .mapping(_, let response) = self else {
|
guard let self = requestError, case .mapping(_, let response, _) = self else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,11 @@ import TIMapUtils
|
||||||
import MapKit
|
import MapKit
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
open class AppleClusterPlacemarkManager<Model>: BasePlacemarkManager<MKAnnotationView, [ApplePlacemarkManager<Model>], MKMapRect>, MKMapViewDelegate {
|
open class AppleClusterPlacemarkManager<Model>: BaseClusterPlacemarkManager<MKAnnotationView,
|
||||||
|
ApplePlacemarkManager<Model>,
|
||||||
|
MKMapRect>,
|
||||||
|
MKMapViewDelegate {
|
||||||
|
|
||||||
public weak var mapViewDelegate: MKMapViewDelegate?
|
public weak var mapViewDelegate: MKMapViewDelegate?
|
||||||
|
|
||||||
private let mapDelegateSelectors = NSObject.instanceMethodSelectors(of: MKMapViewDelegate.self)
|
private let mapDelegateSelectors = NSObject.instanceMethodSelectors(of: MKMapViewDelegate.self)
|
||||||
|
|
@ -36,7 +40,8 @@ open class AppleClusterPlacemarkManager<Model>: BasePlacemarkManager<MKAnnotatio
|
||||||
|
|
||||||
self.mapViewDelegate = mapViewDelegate
|
self.mapViewDelegate = mapViewDelegate
|
||||||
|
|
||||||
super.init(dataModel: placemarkManagers,
|
super.init(placemarkPosition: .from(coordinates: placemarkManagers.map(\.placemarkPosition)),
|
||||||
|
dataModel: placemarkManagers,
|
||||||
iconFactory: iconFactory?.asAnyMarkerIconFactory { $0.map { $0.dataModel } },
|
iconFactory: iconFactory?.asAnyMarkerIconFactory { $0.map { $0.dataModel } },
|
||||||
tapHandler: tapHandler)
|
tapHandler: tapHandler)
|
||||||
}
|
}
|
||||||
|
|
@ -58,7 +63,7 @@ open class AppleClusterPlacemarkManager<Model>: BasePlacemarkManager<MKAnnotatio
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
placemark.image = iconFactory?.markerIcon(for: placemarkManagers)
|
placemark.image = iconFactory?.markerIcon(for: placemarkManagers, state: .default)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - MKMapViewDelegate
|
// MARK: - MKMapViewDelegate
|
||||||
|
|
@ -79,6 +84,7 @@ open class AppleClusterPlacemarkManager<Model>: BasePlacemarkManager<MKAnnotatio
|
||||||
configure(placemark: defaultAnnotationView)
|
configure(placemark: defaultAnnotationView)
|
||||||
|
|
||||||
return defaultAnnotationView
|
return defaultAnnotationView
|
||||||
|
|
||||||
case let placemarkManager as ApplePlacemarkManager<Model>:
|
case let placemarkManager as ApplePlacemarkManager<Model>:
|
||||||
let defaultAnnotationView = placemarkManager.iconFactory != nil
|
let defaultAnnotationView = placemarkManager.iconFactory != nil
|
||||||
? MKAnnotationView(annotation: annotation,
|
? MKAnnotationView(annotation: annotation,
|
||||||
|
|
@ -89,6 +95,7 @@ open class AppleClusterPlacemarkManager<Model>: BasePlacemarkManager<MKAnnotatio
|
||||||
placemarkManager.configure(placemark: defaultAnnotationView)
|
placemarkManager.configure(placemark: defaultAnnotationView)
|
||||||
|
|
||||||
return defaultAnnotationView
|
return defaultAnnotationView
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -107,8 +114,29 @@ open class AppleClusterPlacemarkManager<Model>: BasePlacemarkManager<MKAnnotatio
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = tapHandler?(placemarkManagers, .from(coordinates: placemarkManagers.map { $0.coordinate }))
|
_ = tapHandler?(placemarkManagers, .from(coordinates: placemarkManagers.map { $0.coordinate }))
|
||||||
|
|
||||||
case let placemarkManager as ApplePlacemarkManager<Model>:
|
case let placemarkManager as ApplePlacemarkManager<Model>:
|
||||||
_ = placemarkManager.tapHandler?(placemarkManager.dataModel, placemarkManager.coordinate)
|
let isTapHandled = placemarkManager.tapHandler?(placemarkManager.dataModel, placemarkManager.coordinate) ?? false
|
||||||
|
|
||||||
|
if isTapHandled {
|
||||||
|
placemarkManager.state = .selected
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open func mapView(_ mapView: MKMapView, didDeselect view: MKAnnotationView) {
|
||||||
|
guard !(mapViewDelegate?.responds(to: #selector(mapView(_:didDeselect:))) ?? false) else {
|
||||||
|
mapViewDelegate?.mapView?(mapView, didDeselect: view)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch view.annotation {
|
||||||
|
case let placemarkManager as ApplePlacemarkManager<Model>:
|
||||||
|
placemarkManager.state = .default
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,8 @@ import MapKit
|
||||||
open class AppleMapManager<DataModel>: BaseMapManager<MKMapView,
|
open class AppleMapManager<DataModel>: BaseMapManager<MKMapView,
|
||||||
ApplePlacemarkManager<DataModel>,
|
ApplePlacemarkManager<DataModel>,
|
||||||
AppleClusterPlacemarkManager<DataModel>,
|
AppleClusterPlacemarkManager<DataModel>,
|
||||||
MKCameraUpdate> {
|
MKCameraUpdate,
|
||||||
|
AppleMapUISettings> {
|
||||||
|
|
||||||
public typealias ClusteringIdentifier = String
|
public typealias ClusteringIdentifier = String
|
||||||
|
|
||||||
|
|
@ -44,7 +45,8 @@ open class AppleMapManager<DataModel>: BaseMapManager<MKMapView,
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return ApplePlacemarkManager(dataModel: $0,
|
return ApplePlacemarkManager(map: map,
|
||||||
|
dataModel: $0,
|
||||||
position: position,
|
position: position,
|
||||||
clusteringIdentifier: clusteringIdentifierGetter($0),
|
clusteringIdentifier: clusteringIdentifierGetter($0),
|
||||||
iconFactory: iconFactory?.asAnyMarkerIconFactory(),
|
iconFactory: iconFactory?.asAnyMarkerIconFactory(),
|
||||||
|
|
@ -65,27 +67,18 @@ open class AppleMapManager<DataModel>: BaseMapManager<MKMapView,
|
||||||
}
|
}
|
||||||
|
|
||||||
public convenience init(map: MKMapView,
|
public convenience init(map: MKMapView,
|
||||||
positionGetter: @escaping PositionGetter,
|
iconFactory: DefaultMarkerIconFactory<DataModel>? = nil,
|
||||||
clusteringIdentifierGetter: @escaping (DataModel) -> ClusteringIdentifier,
|
clusterIconFactory: DefaultClusterMarkerIconFactory<DataModel>? = nil,
|
||||||
mapViewDelegate: MKMapViewDelegate? = nil,
|
mapViewDelegate: MKMapViewDelegate? = nil,
|
||||||
selectPlacemarkHandler: @escaping SelectPlacemarkHandler) {
|
selectPlacemarkHandler: @escaping SelectPlacemarkHandler)
|
||||||
|
where DataModel: MapLocatable, DataModel.Position == CLLocationCoordinate2D,
|
||||||
self.init(map: map,
|
DataModel: Clusterable, DataModel.ClusterIdentifier == ClusteringIdentifier {
|
||||||
positionGetter: positionGetter,
|
|
||||||
clusteringIdentifierGetter: clusteringIdentifierGetter,
|
|
||||||
iconFactory: nil as DefaultMarkerIconFactory<DataModel>?,
|
|
||||||
clusterIconFactory: nil as DefaultClusterMarkerIconFactory<DataModel>?,
|
|
||||||
mapViewDelegate: mapViewDelegate,
|
|
||||||
selectPlacemarkHandler: selectPlacemarkHandler)
|
|
||||||
}
|
|
||||||
|
|
||||||
public convenience init(map: MKMapView,
|
|
||||||
mapViewDelegate: MKMapViewDelegate? = nil,
|
|
||||||
selectPlacemarkHandler: @escaping SelectPlacemarkHandler) where DataModel: MapLocatable & Clusterable, DataModel.ClusterIdentifier == ClusteringIdentifier, DataModel.Position == CLLocationCoordinate2D {
|
|
||||||
|
|
||||||
self.init(map: map,
|
self.init(map: map,
|
||||||
positionGetter: { $0.position },
|
positionGetter: { $0.position },
|
||||||
clusteringIdentifierGetter: { $0.clusterIdentifier },
|
clusteringIdentifierGetter: { $0.clusterIdentifier },
|
||||||
|
iconFactory: iconFactory,
|
||||||
|
clusterIconFactory: clusterIconFactory,
|
||||||
mapViewDelegate: mapViewDelegate,
|
mapViewDelegate: mapViewDelegate,
|
||||||
selectPlacemarkHandler: selectPlacemarkHandler)
|
selectPlacemarkHandler: selectPlacemarkHandler)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
//
|
||||||
|
// 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 TIMapUtils
|
||||||
|
import MapKit
|
||||||
|
|
||||||
|
open class AppleMapUISettings: BaseMapUISettings<MKMapView> {
|
||||||
|
open class Defaults: BaseMapUISettings<MKMapView>.Defaults {
|
||||||
|
public static var showCompassButton: Bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var showCompassButton = false
|
||||||
|
|
||||||
|
public init(showUserLocation: Bool = Defaults.showUserLocation,
|
||||||
|
isZoomEnabled: Bool = Defaults.isZoomEnabled,
|
||||||
|
isTiltEnabled: Bool = Defaults.isTiltEnabled,
|
||||||
|
isRotationEnabled: Bool = Defaults.isRotationEnabled,
|
||||||
|
showCompassButton: Bool = Defaults.showCompassButton) {
|
||||||
|
|
||||||
|
self.showCompassButton = showCompassButton
|
||||||
|
|
||||||
|
super.init(showUserLocation: showUserLocation,
|
||||||
|
isZoomEnabled: isZoomEnabled,
|
||||||
|
isTiltEnabled: isTiltEnabled,
|
||||||
|
isRotationEnabled: isRotationEnabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
override open func apply(to mapView: MKMapView) {
|
||||||
|
super.apply(to: mapView)
|
||||||
|
|
||||||
|
mapView.showsUserLocation = showUserLocation
|
||||||
|
mapView.isZoomEnabled = isZoomEnabled
|
||||||
|
mapView.isPitchEnabled = isTiltEnabled
|
||||||
|
mapView.isRotateEnabled = isRotationEnabled
|
||||||
|
mapView.showsCompass = showCompassButton
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -23,23 +23,58 @@
|
||||||
import TIMapUtils
|
import TIMapUtils
|
||||||
import MapKit
|
import MapKit
|
||||||
|
|
||||||
open class ApplePlacemarkManager<Model>: BasePlacemarkManager<MKAnnotationView, Model, CLLocationCoordinate2D>, MKAnnotation {
|
open class ApplePlacemarkManager<Model>: BaseItemPlacemarkManager<MKAnnotationView, Model, CLLocationCoordinate2D>,
|
||||||
|
MKAnnotation {
|
||||||
|
|
||||||
// MARK: - MKAnnotation
|
// MARK: - MKAnnotation
|
||||||
|
|
||||||
public let coordinate: CLLocationCoordinate2D
|
/// A map where all placemarks are placed
|
||||||
|
public let map: MKMapView
|
||||||
|
|
||||||
|
/// Identifier required for correct cluster placement
|
||||||
public var clusteringIdentifier: String?
|
public var clusteringIdentifier: String?
|
||||||
|
|
||||||
public init(dataModel: Model,
|
/// Point (coordinates) itself of the current placemark manager
|
||||||
|
public var coordinate: CLLocationCoordinate2D {
|
||||||
|
placemarkPosition
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The current state of a manager's placemark
|
||||||
|
override public var state: MarkerState {
|
||||||
|
didSet {
|
||||||
|
guard let placemark = placemark else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Although the icon is being updated, it is necessary to manually deselect
|
||||||
|
the annotation of the current placemark if it is currently selected.
|
||||||
|
*/
|
||||||
|
if let annotation = placemark.annotation {
|
||||||
|
switch state {
|
||||||
|
case .default:
|
||||||
|
map.deselectAnnotation(annotation, animated: true)
|
||||||
|
case .selected:
|
||||||
|
map.selectAnnotation(annotation, animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
placemark.image = iconFactory?.markerIcon(for: dataModel, state: state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(map: MKMapView,
|
||||||
|
dataModel: Model,
|
||||||
position: CLLocationCoordinate2D,
|
position: CLLocationCoordinate2D,
|
||||||
clusteringIdentifier: String?,
|
clusteringIdentifier: String?,
|
||||||
iconFactory: AnyMarkerIconFactory<DataModel>?,
|
iconFactory: AnyMarkerIconFactory<DataModel>?,
|
||||||
tapHandler: TapHandlerClosure?) {
|
tapHandler: TapHandlerClosure?) {
|
||||||
|
|
||||||
self.coordinate = position
|
self.map = map
|
||||||
self.clusteringIdentifier = clusteringIdentifier
|
self.clusteringIdentifier = clusteringIdentifier
|
||||||
|
|
||||||
super.init(dataModel: dataModel,
|
super.init(placemarkPosition: position,
|
||||||
|
dataModel: dataModel,
|
||||||
iconFactory: iconFactory,
|
iconFactory: iconFactory,
|
||||||
tapHandler: tapHandler)
|
tapHandler: tapHandler)
|
||||||
}
|
}
|
||||||
|
|
@ -47,7 +82,10 @@ open class ApplePlacemarkManager<Model>: BasePlacemarkManager<MKAnnotationView,
|
||||||
// MARK: - PlacemarkManager
|
// MARK: - PlacemarkManager
|
||||||
|
|
||||||
override open func configure(placemark: MKAnnotationView) {
|
override open func configure(placemark: MKAnnotationView) {
|
||||||
placemark.image = iconFactory?.markerIcon(for: dataModel)
|
super.configure(placemark: placemark)
|
||||||
placemark.clusteringIdentifier = clusteringIdentifier
|
|
||||||
|
// Setting required values of the current manager and placemark respectively
|
||||||
|
self.placemark?.clusteringIdentifier = clusteringIdentifier
|
||||||
|
self.state = .default
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,23 @@
|
||||||
Pod::Spec.new do |s|
|
Pod::Spec.new do |s|
|
||||||
s.name = 'TIAppleMapUtils'
|
s.name = 'TIAppleMapUtils'
|
||||||
s.version = '1.20.0'
|
s.version = '1.56.0'
|
||||||
s.summary = 'Set of helpers for map objects clustering and interacting using Apple MapKit.'
|
s.summary = 'Set of helpers for map objects clustering and interacting using Apple MapKit.'
|
||||||
s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
|
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
|
||||||
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
||||||
s.author = { 'petropavel13' => 'ivan.smolin@touchin.ru' }
|
s.author = { 'petropavel13' => 'ivan.smolin@touchin.ru' }
|
||||||
s.source = { :git => 'https://github.com/TouchInstinct/LeadKit.git', :tag => s.version.to_s }
|
s.source = { :git => 'https://git.svc.touchin.ru/TouchInstinct/LeadKit.git', :tag => s.version.to_s }
|
||||||
|
|
||||||
s.ios.deployment_target = '11.0'
|
s.ios.deployment_target = '12.0'
|
||||||
s.swift_versions = ['5.3']
|
s.swift_versions = ['5.7']
|
||||||
|
|
||||||
s.source_files = s.name + '/Sources/**/*'
|
sources = 'Sources/**/*'
|
||||||
|
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
|
||||||
|
|
||||||
s.dependency 'TIMapUtils', s.version.to_s
|
s.dependency 'TIMapUtils', s.version.to_s
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
//
|
||||||
|
// 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 Foundation
|
||||||
|
|
||||||
|
public struct BundleIdentifier {
|
||||||
|
public let appPrefix: String
|
||||||
|
public let appIdentifier: String
|
||||||
|
public let fullIdentifier: String
|
||||||
|
public let defaultAppGroupIdenfier: String
|
||||||
|
|
||||||
|
static let bundleIdentifierSeparator = "."
|
||||||
|
|
||||||
|
public init(appPrefix: String, appIdentifier: String) {
|
||||||
|
self.appPrefix = appPrefix
|
||||||
|
self.appIdentifier = appIdentifier
|
||||||
|
self.fullIdentifier = appPrefix + Self.bundleIdentifierSeparator + appIdentifier
|
||||||
|
self.defaultAppGroupIdenfier = "group." + fullIdentifier
|
||||||
|
}
|
||||||
|
|
||||||
|
public init?(bundle: Bundle = .main) {
|
||||||
|
guard let fullIdenfifier = bundle.bundleIdentifier,
|
||||||
|
var components = bundle.bundleIdentifier?
|
||||||
|
.components(separatedBy: Self.bundleIdentifierSeparator),
|
||||||
|
let lastComponent = components.popLast() else {
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
self.fullIdentifier = fullIdenfifier
|
||||||
|
self.appIdentifier = lastComponent
|
||||||
|
self.appPrefix = components.joined(separator: Self.bundleIdentifierSeparator)
|
||||||
|
self.defaultAppGroupIdenfier = "group." + fullIdentifier
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,116 @@
|
||||||
|
//
|
||||||
|
// 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 Foundation
|
||||||
|
import TIFoundationUtils
|
||||||
|
import KeychainAccess
|
||||||
|
import TILogging
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
public struct CoreDependencies {
|
||||||
|
public var dateFormattersResusePool = DateFormattersReusePool()
|
||||||
|
public var iso8601DateFormattersReusePool = ISO8601DateFormattersReusePool()
|
||||||
|
public var jsonCodingConfigurator: JsonCodingConfigurator
|
||||||
|
public var jsonKeyValueDecoder: JSONKeyValueDecoder
|
||||||
|
public var jsonKeyValueEncoder: JSONKeyValueEncoder
|
||||||
|
|
||||||
|
public var device: UIDevice = .current
|
||||||
|
|
||||||
|
public var bundle: Bundle = .main
|
||||||
|
public var fileManager: FileManager = .default
|
||||||
|
|
||||||
|
public var logger: DefaultOSLogErrorLogger
|
||||||
|
|
||||||
|
public var keychain: Keychain
|
||||||
|
public var defaults: UserDefaults
|
||||||
|
|
||||||
|
public var appGroupDefaults: UserDefaults?
|
||||||
|
public var appGroupKeychain: Keychain?
|
||||||
|
|
||||||
|
public var appGroupCacheDirectory: URL?
|
||||||
|
|
||||||
|
public var networkCallbackQueue: DispatchQueue
|
||||||
|
|
||||||
|
public init(bundleIdentifier: BundleIdentifier,
|
||||||
|
customAppGroupIdentifier: String? = nil) {
|
||||||
|
|
||||||
|
jsonCodingConfigurator = JsonCodingConfigurator(dateFormattersReusePool: dateFormattersResusePool,
|
||||||
|
iso8601DateFormattersReusePool: iso8601DateFormattersReusePool)
|
||||||
|
|
||||||
|
jsonKeyValueDecoder = JSONKeyValueDecoder(jsonDecoder: jsonCodingConfigurator.jsonDecoder)
|
||||||
|
jsonKeyValueEncoder = JSONKeyValueEncoder(jsonEncoder: jsonCodingConfigurator.jsonEncoder)
|
||||||
|
|
||||||
|
logger = DefaultOSLogErrorLogger(subsystem: bundleIdentifier.fullIdentifier, category: "general")
|
||||||
|
|
||||||
|
keychain = Keychain(service: bundleIdentifier.fullIdentifier).accessibility(.whenUnlockedThisDeviceOnly)
|
||||||
|
defaults = .standard
|
||||||
|
|
||||||
|
let appGroupIdentifier = customAppGroupIdentifier ?? bundleIdentifier.defaultAppGroupIdenfier
|
||||||
|
|
||||||
|
if let containerURL = fileManager.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier) {
|
||||||
|
var appGroupCacheURL: URL
|
||||||
|
|
||||||
|
if #available(iOS 16.0, *) {
|
||||||
|
appGroupCacheURL = containerURL.appending(path: "Caches", directoryHint: .isDirectory)
|
||||||
|
} else {
|
||||||
|
appGroupCacheURL = containerURL.appendingPathComponent("Caches", isDirectory: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
var resourceValues = URLResourceValues()
|
||||||
|
resourceValues.isExcludedFromBackup = true
|
||||||
|
|
||||||
|
do {
|
||||||
|
try fileManager.createDirectory(at: appGroupCacheURL,
|
||||||
|
withIntermediateDirectories: true)
|
||||||
|
|
||||||
|
try appGroupCacheURL.setResourceValues(resourceValues)
|
||||||
|
|
||||||
|
appGroupCacheDirectory = appGroupCacheURL
|
||||||
|
} catch {
|
||||||
|
logger.log(error: error, file: #file, line: #line)
|
||||||
|
}
|
||||||
|
|
||||||
|
appGroupDefaults = UserDefaults(suiteName: appGroupIdentifier)
|
||||||
|
appGroupKeychain = Keychain(service: bundleIdentifier.appPrefix,
|
||||||
|
accessGroup: appGroupIdentifier)
|
||||||
|
.accessibility(.whenUnlockedThisDeviceOnly)
|
||||||
|
} else {
|
||||||
|
appGroupCacheDirectory = nil
|
||||||
|
appGroupDefaults = nil
|
||||||
|
appGroupKeychain = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
networkCallbackQueue = DispatchQueue(label: bundleIdentifier.fullIdentifier + ".network-callback-queue",
|
||||||
|
attributes: .concurrent)
|
||||||
|
}
|
||||||
|
|
||||||
|
public init?(bundle: Bundle = .main,
|
||||||
|
customAppGroupIdentifier: String? = nil) {
|
||||||
|
|
||||||
|
guard let bundleIdentifier = BundleIdentifier(bundle: bundle) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
self.init(bundleIdentifier: bundleIdentifier,
|
||||||
|
customAppGroupIdentifier: customAppGroupIdentifier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
//
|
||||||
|
|
||||||
|
public protocol TargetDependencies {
|
||||||
|
static func assemble() -> Self
|
||||||
|
static func assembleForPreview() -> Self
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
Pod::Spec.new do |s|
|
||||||
|
s.name = 'TIApplication'
|
||||||
|
s.version = '1.56.0'
|
||||||
|
s.summary = 'Application architecture.'
|
||||||
|
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
|
||||||
|
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
||||||
|
s.author = { 'petropavel13' => 'ivan.smolin@touchin.ru' }
|
||||||
|
s.source = { :git => 'https://git.svc.touchin.ru/TouchInstinct/LeadKit.git', :tag => s.version.to_s }
|
||||||
|
|
||||||
|
s.ios.deployment_target = '12.0'
|
||||||
|
s.swift_versions = ['5.7']
|
||||||
|
|
||||||
|
sources = 'Sources/**/*'
|
||||||
|
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
|
||||||
|
|
||||||
|
s.dependency 'TIFoundationUtils', s.version.to_s
|
||||||
|
s.dependency 'TILogging', s.version.to_s
|
||||||
|
s.dependency 'KeychainAccess', "~> 4.2"
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
//
|
||||||
|
// Copyright (c) 2022 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 LocalAuthentication
|
||||||
|
|
||||||
|
public protocol BiometryService {
|
||||||
|
var isBiometryAuthAvailable: Bool { get }
|
||||||
|
var biometryType: LABiometryType { get }
|
||||||
|
}
|
||||||
|
|
||||||
|
extension LAContext: BiometryService {
|
||||||
|
public var isBiometryAuthAvailable: Bool {
|
||||||
|
canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -20,8 +20,8 @@
|
||||||
// THE SOFTWARE.
|
// THE SOFTWARE.
|
||||||
//
|
//
|
||||||
|
|
||||||
public enum RecoverableErrorHandlerResult<ErrorType: Error> {
|
import Foundation
|
||||||
case skipError
|
|
||||||
case recoverRequest
|
public protocol BiometrySettingsStorage {
|
||||||
case forwardError(ErrorType)
|
var isBiometryAuthEnabled: Bool { get set }
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
//
|
||||||
|
// Copyright (c) 2022 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 DefaultBiometrySettingsStorage: BiometrySettingsStorage {
|
||||||
|
public enum StorageKeys {
|
||||||
|
static var isBiometryAuthEnabledStorageKey: String {
|
||||||
|
"isBiometryAuthEnabled"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var defaultsStorage: UserDefaults
|
||||||
|
|
||||||
|
// MARK: - BiometrySettingsService
|
||||||
|
|
||||||
|
public var isBiometryAuthEnabled: Bool {
|
||||||
|
get {
|
||||||
|
defaultsStorage.bool(forKey: StorageKeys.isBiometryAuthEnabledStorageKey)
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
defaultsStorage.set(newValue, forKey: StorageKeys.isBiometryAuthEnabledStorageKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(defaultsStorage: UserDefaults = .standard) {
|
||||||
|
self.defaultsStorage = defaultsStorage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -21,18 +21,15 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import TIUIKitCore
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
protocol CodeConfirmPresenter {
|
protocol CodeConfirmPresenter: LifecyclePresenter {
|
||||||
// MARK: - User actions handling
|
// MARK: - User actions handling
|
||||||
|
|
||||||
func inputChanged(newInput: String?)
|
func inputChanged(newInput: String?)
|
||||||
func refreshCode()
|
func refreshCode()
|
||||||
|
|
||||||
// MARK: - View lifecycle handling
|
|
||||||
|
|
||||||
func viewDidPresented()
|
|
||||||
|
|
||||||
// MARK: - Autofill
|
// MARK: - Autofill
|
||||||
|
|
||||||
func autofill(code: String, with codeId: String?)
|
func autofill(code: String, with codeId: String?)
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,6 @@
|
||||||
// THE SOFTWARE.
|
// THE SOFTWARE.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
public protocol CodeConfirmStateStorage: AnyObject {
|
public protocol CodeConfirmStateStorage: AnyObject {
|
||||||
var currentUserInput: String? { get set }
|
var currentUserInput: String? { get set }
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,8 @@ open class DefaultCodeConfirmPresenter<ConfirmResponse: CodeConfirmResponse,
|
||||||
private let codeRefreshTimer = TITimer(mode: .activeAndBackground)
|
private let codeRefreshTimer = TITimer(mode: .activeAndBackground)
|
||||||
private let codeLifetimeTimer = TITimer(mode: .activeAndBackground)
|
private let codeLifetimeTimer = TITimer(mode: .activeAndBackground)
|
||||||
|
|
||||||
|
private var executingTask: Cancellable?
|
||||||
|
|
||||||
public var output: Output
|
public var output: Output
|
||||||
public var requests: Requests
|
public var requests: Requests
|
||||||
public weak var stateStorage: CodeConfirmStateStorage?
|
public weak var stateStorage: CodeConfirmStateStorage?
|
||||||
|
|
@ -190,7 +192,7 @@ open class DefaultCodeConfirmPresenter<ConfirmResponse: CodeConfirmResponse,
|
||||||
if let code = newInput, code.count >= config.codeLength {
|
if let code = newInput, code.count >= config.codeLength {
|
||||||
stateStorage?.isExecutingRequest = true
|
stateStorage?.isExecutingRequest = true
|
||||||
|
|
||||||
Task {
|
executingTask = Task {
|
||||||
await confirm(code: code)
|
await confirm(code: code)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -200,7 +202,7 @@ open class DefaultCodeConfirmPresenter<ConfirmResponse: CodeConfirmResponse,
|
||||||
stateStorage?.canRequestNewCode = false
|
stateStorage?.canRequestNewCode = false
|
||||||
stateStorage?.canRefreshCodeAfter = nil
|
stateStorage?.canRefreshCodeAfter = nil
|
||||||
|
|
||||||
Task {
|
executingTask = Task {
|
||||||
await refreshCode()
|
await refreshCode()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -213,6 +215,10 @@ open class DefaultCodeConfirmPresenter<ConfirmResponse: CodeConfirmResponse,
|
||||||
for: currentCodeResponse)
|
for: currentCodeResponse)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
open func viewWillDestroy() {
|
||||||
|
executingTask?.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Autofill
|
// MARK: - Autofill
|
||||||
|
|
||||||
open func autofill(code: String, with codeId: String? = nil) {
|
open func autofill(code: String, with codeId: String? = nil) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
//
|
||||||
|
// Copyright (c) 2022 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 CommonCrypto
|
||||||
|
|
||||||
|
open class AESCipher: Cipher {
|
||||||
|
public struct CryptError: Error {
|
||||||
|
public let ccCryptorStatus: Int32
|
||||||
|
public let key: Data
|
||||||
|
public let iv: Data
|
||||||
|
}
|
||||||
|
|
||||||
|
public var iv: Data
|
||||||
|
public var key: Data
|
||||||
|
|
||||||
|
public init(iv: Data, key: Data) {
|
||||||
|
self.iv = iv
|
||||||
|
self.key = key
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Cipher
|
||||||
|
|
||||||
|
public func encrypt(data: Data) -> Result<Data, CipherError> {
|
||||||
|
crypt(data: data, operation: CCOperation(kCCEncrypt))
|
||||||
|
}
|
||||||
|
|
||||||
|
public func decrypt(data: Data) -> Result<Data, CipherError> {
|
||||||
|
crypt(data: data, operation: CCOperation(kCCDecrypt))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func crypt(data: Data, operation: CCOperation) -> Result<Data, CipherError> {
|
||||||
|
let cryptDataLength = data.count + kCCBlockSizeAES128
|
||||||
|
var cryptData = Data(count: cryptDataLength)
|
||||||
|
|
||||||
|
var bytesLength = Int.zero
|
||||||
|
|
||||||
|
let status = cryptData.withUnsafeMutableBytes { cryptBytes in
|
||||||
|
data.withUnsafeBytes { dataBytes in
|
||||||
|
iv.withUnsafeBytes { ivBytes in
|
||||||
|
key.withUnsafeBytes { keyBytes in
|
||||||
|
CCCrypt(operation,
|
||||||
|
CCAlgorithm(kCCAlgorithmAES),
|
||||||
|
CCOptions(kCCOptionPKCS7Padding),
|
||||||
|
keyBytes.baseAddress,
|
||||||
|
key.count,
|
||||||
|
ivBytes.baseAddress,
|
||||||
|
dataBytes.baseAddress,
|
||||||
|
data.count,
|
||||||
|
cryptBytes.baseAddress,
|
||||||
|
cryptDataLength,
|
||||||
|
&bytesLength)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
guard status == kCCSuccess else {
|
||||||
|
let error = CryptError(ccCryptorStatus: status,
|
||||||
|
key: key,
|
||||||
|
iv: iv)
|
||||||
|
|
||||||
|
if operation == kCCEncrypt {
|
||||||
|
return .failure(.failedToEncrypt(data: data, error: error))
|
||||||
|
} else {
|
||||||
|
return .failure(.failedToDecrypt(encryptedData: data, error: error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cryptData.removeSubrange(bytesLength..<cryptData.count)
|
||||||
|
return .success(cryptData)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
//
|
||||||
|
// Copyright (c) 2022 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
|
||||||
|
|
||||||
|
public protocol Cipher {
|
||||||
|
func encrypt(data: Data) -> Result<Data, CipherError>
|
||||||
|
func decrypt(data: Data) -> Result<Data, CipherError>
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
//
|
||||||
|
// Copyright (c) 2022 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
|
||||||
|
|
||||||
|
public enum CipherError: Error {
|
||||||
|
case failedToEncrypt(data: Data, error: Error)
|
||||||
|
case failedToDecrypt(encryptedData: Data, error: Error)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
//
|
||||||
|
// Copyright (c) 2022 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 CommonCrypto
|
||||||
|
|
||||||
|
open class DefaultPBKDF2PasswordDerivator: PasswordDerivator {
|
||||||
|
public struct CryptError: Error {
|
||||||
|
public let derivationStatus: Int32
|
||||||
|
public let password: String
|
||||||
|
public let salt: Data
|
||||||
|
|
||||||
|
func asCipherError() -> CipherError {
|
||||||
|
var mutablePassword = password
|
||||||
|
return .failedToEncrypt(data: mutablePassword.withUTF8 { Data($0) },
|
||||||
|
error: self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func derive(password: String, salt: Data) -> Result<Data, CipherError> {
|
||||||
|
var failureResult: Result<Data, CipherError>?
|
||||||
|
|
||||||
|
let derivedKeyBytes = Array<UInt8>(unsafeUninitializedCapacity: CryptoConstants.keyLength) { derivedKeyBuffer, initializedCount in
|
||||||
|
guard let derivedKeyStartAddress = derivedKeyBuffer.baseAddress else {
|
||||||
|
failureResult = .failure(CryptError(derivationStatus: CCStatus(kCCMemoryFailure),
|
||||||
|
password: password,
|
||||||
|
salt: salt)
|
||||||
|
.asCipherError())
|
||||||
|
|
||||||
|
initializedCount = .zero
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let deriviationStatus = salt.withContiguousStorageIfAvailable { saltBytes in
|
||||||
|
CCKeyDerivationPBKDF(
|
||||||
|
CCPBKDFAlgorithm(kCCPBKDF2),
|
||||||
|
password,
|
||||||
|
password.count,
|
||||||
|
saltBytes.baseAddress,
|
||||||
|
salt.count,
|
||||||
|
CCPBKDFAlgorithm(kCCPRFHmacAlgSHA512),
|
||||||
|
UInt32(CryptoConstants.pbkdf2NumberOfIterations),
|
||||||
|
derivedKeyStartAddress,
|
||||||
|
CryptoConstants.keyLength)
|
||||||
|
} ?? CCStatus(kCCParamError)
|
||||||
|
|
||||||
|
guard deriviationStatus == kCCSuccess else {
|
||||||
|
initializedCount = .zero
|
||||||
|
failureResult = .failure(CryptError(derivationStatus: deriviationStatus,
|
||||||
|
password: password,
|
||||||
|
salt: salt)
|
||||||
|
.asCipherError())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return failureResult ?? .success(Data(derivedKeyBytes))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -20,19 +20,22 @@
|
||||||
// THE SOFTWARE.
|
// THE SOFTWARE.
|
||||||
//
|
//
|
||||||
|
|
||||||
import TISwiftUtils
|
import Foundation
|
||||||
|
|
||||||
@available(iOS 13.0.0, *)
|
open class DefaultSaltPreprocessor: SaltPreprocessor {
|
||||||
public struct AnyAsyncEventHandler<EventType, ResultType>: AsyncEventHandler {
|
public typealias DeviceIdProviderClosure = () -> String?
|
||||||
private let processClosure: AsyncClosure<EventType, ResultType>
|
|
||||||
|
|
||||||
public init<Handler: AsyncEventHandler>(handler: Handler)
|
private let deviceIdProvider: DeviceIdProviderClosure
|
||||||
where Handler.EventType == EventType, Handler.ResultType == ResultType {
|
|
||||||
|
|
||||||
self.processClosure = handler.handle
|
public init(deviceIdProvider: @escaping DeviceIdProviderClosure) {
|
||||||
|
self.deviceIdProvider = deviceIdProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
public func handle(_ event: EventType) async -> ResultType {
|
// MARK: - SaltPreprocessor
|
||||||
await processClosure(event)
|
|
||||||
|
public func preprocess(salt: Data) -> Data {
|
||||||
|
deviceIdProvider().map {
|
||||||
|
salt + Data($0.utf8)
|
||||||
|
} ?? salt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,98 @@
|
||||||
|
//
|
||||||
|
// Copyright (c) 2022 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 Security
|
||||||
|
|
||||||
|
open class DefaultTokenCipher: TokenCipher {
|
||||||
|
public var saltPreprocessor: SaltPreprocessor
|
||||||
|
public var passwordDerivator: PasswordDerivator
|
||||||
|
|
||||||
|
public init(saltPreprocessor: SaltPreprocessor, passwordDerivator: PasswordDerivator) {
|
||||||
|
self.saltPreprocessor = saltPreprocessor
|
||||||
|
self.passwordDerivator = passwordDerivator
|
||||||
|
}
|
||||||
|
|
||||||
|
open func createCipher(iv: Data, key: Data) -> Cipher {
|
||||||
|
AESCipher(iv: iv, key: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
open func generateIV() -> Data {
|
||||||
|
generateRandomData(count: CryptoConstants.ivLength)
|
||||||
|
}
|
||||||
|
|
||||||
|
open func generateSalt() -> Data {
|
||||||
|
generateRandomData(count: CryptoConstants.saltLength)
|
||||||
|
}
|
||||||
|
|
||||||
|
open func generateRandomData(count: Int) -> Data {
|
||||||
|
let randomBytes = Array<UInt8>(unsafeUninitializedCapacity: count) { buffer, initializedCount in
|
||||||
|
guard let startAddress = buffer.baseAddress,
|
||||||
|
SecRandomCopyBytes(kSecRandomDefault,
|
||||||
|
count,
|
||||||
|
startAddress) == errSecSuccess else {
|
||||||
|
|
||||||
|
initializedCount = .zero
|
||||||
|
return
|
||||||
|
}
|
||||||
|
initializedCount = count
|
||||||
|
}
|
||||||
|
|
||||||
|
return Data(randomBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - TokenCipher
|
||||||
|
|
||||||
|
open func derive(password: String, using salt: Data) -> Result<Data, CipherError> {
|
||||||
|
passwordDerivator.derive(password: password,
|
||||||
|
salt: saltPreprocessor.preprocess(salt: salt))
|
||||||
|
}
|
||||||
|
|
||||||
|
open func encrypt(token: Data, using password: String) -> Result<StringEncryptionResult, CipherError> {
|
||||||
|
let iv = generateIV()
|
||||||
|
let salt = generateSalt()
|
||||||
|
|
||||||
|
return derive(password: password, using: salt)
|
||||||
|
.flatMap {
|
||||||
|
createCipher(iv: iv, key: $0)
|
||||||
|
.encrypt(data: token)
|
||||||
|
.map {
|
||||||
|
StringEncryptionResult(salt: salt, iv: iv, value: $0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open func decrypt(token: StringEncryptionResult, using key: Data) -> Result<Data, CipherError> {
|
||||||
|
createCipher(iv: token.iv, key: key)
|
||||||
|
.decrypt(data: token.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
open func decrypt(token: StringEncryptionResult, using password: String) -> Result<Data, CipherError> {
|
||||||
|
passwordDerivator.derive(password: password,
|
||||||
|
salt: saltPreprocessor.preprocess(salt: token.salt))
|
||||||
|
.flatMap {
|
||||||
|
createCipher(iv: token.iv,
|
||||||
|
key: $0)
|
||||||
|
.decrypt(data: token.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -20,8 +20,8 @@
|
||||||
// THE SOFTWARE.
|
// THE SOFTWARE.
|
||||||
//
|
//
|
||||||
|
|
||||||
import TINetworking
|
import Foundation
|
||||||
|
|
||||||
public protocol EndpointRequestPreprocessor {
|
public protocol PasswordDerivator {
|
||||||
func preprocess<B,S>(request: EndpointRequest<B,S>) throws -> EndpointRequest<B,S>
|
func derive(password: String, salt: Data) -> Result<Data, CipherError>
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
//
|
||||||
|
// Copyright (c) 2022 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
|
||||||
|
|
||||||
|
public protocol SaltPreprocessor {
|
||||||
|
func preprocess(salt: Data) -> Data
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
//
|
||||||
|
// Copyright (c) 2022 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
|
||||||
|
|
||||||
|
public protocol TokenCipher {
|
||||||
|
func derive(password: String, using salt: Data) -> Result<Data, CipherError>
|
||||||
|
|
||||||
|
func encrypt(token: Data, using password: String) -> Result<StringEncryptionResult, CipherError>
|
||||||
|
func decrypt(token: StringEncryptionResult, using key: Data) -> Result<Data, CipherError>
|
||||||
|
func decrypt(token: StringEncryptionResult, using password: String) -> Result<Data, CipherError>
|
||||||
|
}
|
||||||
|
|
@ -20,17 +20,16 @@
|
||||||
// THE SOFTWARE.
|
// THE SOFTWARE.
|
||||||
//
|
//
|
||||||
|
|
||||||
@available(iOS 13.0.0, *)
|
public struct EqualDigitsValidationRule: ValidationRule {
|
||||||
public protocol AsyncEventHandler {
|
private let minEqualDigits: UInt
|
||||||
associatedtype EventType
|
|
||||||
associatedtype ResultType
|
|
||||||
|
|
||||||
func handle(_ event: EventType) async -> ResultType
|
public init(minEqualDigits: UInt) {
|
||||||
}
|
self.minEqualDigits = minEqualDigits
|
||||||
|
}
|
||||||
|
|
||||||
@available(iOS 13.0.0, *)
|
// MARK: - ValidationRule
|
||||||
public extension AsyncEventHandler {
|
|
||||||
func asAnyAsyncEventHandler() -> AnyAsyncEventHandler<EventType, ResultType> {
|
public func validate(input: String) -> Bool {
|
||||||
.init(handler: self)
|
!input.containsSequenceOfEqualCharacters(minEqualCharacters: minEqualDigits)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
//
|
||||||
|
// Copyright (c) 2022 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.
|
||||||
|
//
|
||||||
|
|
||||||
|
public struct OrderedDigitsValidationRule: ValidationRule {
|
||||||
|
private let ascendingSequence: Bool
|
||||||
|
private let minLength: UInt
|
||||||
|
|
||||||
|
public init(ascendingSequence: Bool, minLength: UInt) {
|
||||||
|
self.ascendingSequence = ascendingSequence
|
||||||
|
self.minLength = minLength
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - ValidationRule
|
||||||
|
|
||||||
|
public func validate(input: String) -> Bool {
|
||||||
|
ascendingSequence
|
||||||
|
? !input.containsAscendingSequence(minLength: minLength)
|
||||||
|
: !input.containsDescendingSequence(minLength: minLength)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
//
|
||||||
|
// Copyright (c) 2022 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.
|
||||||
|
//
|
||||||
|
|
||||||
|
private extension Substring.SubSequence {
|
||||||
|
func recursivePairCheck(requiredMatches: UInt,
|
||||||
|
sequenceRequiredMatches: UInt,
|
||||||
|
checkClosure: (Character, Character) -> Bool) -> Bool {
|
||||||
|
|
||||||
|
guard sequenceRequiredMatches > 0 else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
guard !isEmpty else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
let tail = dropFirst()
|
||||||
|
|
||||||
|
guard let current = first, let next = tail.first else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
let matched = checkClosure(current, next)
|
||||||
|
|
||||||
|
let reducedMatches = sequenceRequiredMatches - (matched ? 1 : 0)
|
||||||
|
|
||||||
|
let currentSequenceMatch = matched
|
||||||
|
&& tail.recursivePairCheck(requiredMatches: requiredMatches,
|
||||||
|
sequenceRequiredMatches: reducedMatches,
|
||||||
|
checkClosure: checkClosure)
|
||||||
|
|
||||||
|
return currentSequenceMatch || tail.recursivePairCheck(requiredMatches: requiredMatches,
|
||||||
|
sequenceRequiredMatches: requiredMatches,
|
||||||
|
checkClosure: checkClosure)
|
||||||
|
}
|
||||||
|
|
||||||
|
func recursivePairCheck(requiredMatches: UInt, checkClosure: (Character, Character) -> Bool) -> Bool {
|
||||||
|
recursivePairCheck(requiredMatches: requiredMatches,
|
||||||
|
sequenceRequiredMatches: requiredMatches,
|
||||||
|
checkClosure: checkClosure)
|
||||||
|
}
|
||||||
|
|
||||||
|
func containsOrderedSequence(minLength: UInt, orderingClosure: ((Int, Int) -> Bool)) -> Bool {
|
||||||
|
recursivePairCheck(requiredMatches: minLength - 1) {
|
||||||
|
guard let current = $0.intValue, let next = $1.intValue else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return orderingClosure(current, next)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension Character {
|
||||||
|
var intValue: Int? {
|
||||||
|
return Int(String(self))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension String {
|
||||||
|
func containsSequenceOfEqualCharacters(minEqualCharacters: UInt) -> Bool {
|
||||||
|
Substring(self).recursivePairCheck(requiredMatches: minEqualCharacters - 1) { $0 == $1 }
|
||||||
|
}
|
||||||
|
|
||||||
|
func containsAscendingSequence(minLength: UInt) -> Bool {
|
||||||
|
Substring(self).containsOrderedSequence(minLength: minLength) { $0 + 1 == $1 }
|
||||||
|
}
|
||||||
|
|
||||||
|
func containsDescendingSequence(minLength: UInt) -> Bool {
|
||||||
|
Substring(self).containsOrderedSequence(minLength: minLength) { $0 - 1 == $1 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
//
|
||||||
|
// Copyright (c) 2022 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.
|
||||||
|
//
|
||||||
|
|
||||||
|
public protocol ValidationRule {
|
||||||
|
func validate(input: String) -> Bool
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
//
|
||||||
|
// Copyright (c) 2022 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 DefaultInputValidator<Violation: Hashable>: InputValidator {
|
||||||
|
public var rules: [Violation: ValidationRule]
|
||||||
|
|
||||||
|
public init(rules: [Violation: ValidationRule]) {
|
||||||
|
self.rules = rules
|
||||||
|
}
|
||||||
|
|
||||||
|
convenience init(violations: [Violation], rulesCreator: (Violation) -> ValidationRule) {
|
||||||
|
self.init(rules: .init(uniqueKeysWithValues: violations.map { ($0, rulesCreator($0)) }) )
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - InputValidator
|
||||||
|
|
||||||
|
open func validate(input: String) -> Set<Violation> {
|
||||||
|
Set(rules.filter { !$0.value.validate(input: input) }.keys)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
//
|
||||||
|
// Copyright (c) 2022 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.
|
||||||
|
//
|
||||||
|
|
||||||
|
public enum DefaultViolation: Hashable {
|
||||||
|
case orderedDigits(ascending: Bool, minLength: UInt)
|
||||||
|
case equalDigits(minEqualDigits: UInt)
|
||||||
|
|
||||||
|
public var defaultValidationRule: ValidationRule {
|
||||||
|
switch self {
|
||||||
|
case let .orderedDigits(ascending, minLength):
|
||||||
|
return OrderedDigitsValidationRule(ascendingSequence: ascending, minLength: minLength)
|
||||||
|
case let .equalDigits(minEqualDigits):
|
||||||
|
return EqualDigitsValidationRule(minEqualDigits: minEqualDigits)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -20,10 +20,10 @@
|
||||||
// THE SOFTWARE.
|
// THE SOFTWARE.
|
||||||
//
|
//
|
||||||
|
|
||||||
import TINetworking
|
public protocol InputValidator {
|
||||||
|
associatedtype Violation: Hashable
|
||||||
|
|
||||||
public protocol SecuritySchemePreprocessor {
|
var rules: [Violation: ValidationRule] { get set }
|
||||||
func preprocess<B,S>(request: EndpointRequest<B,S>, using security: SecurityScheme) throws -> EndpointRequest<B,S>
|
|
||||||
|
func validate(input: String) -> Set<Violation>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
//
|
||||||
|
// Copyright (c) 2022 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
|
||||||
|
|
||||||
|
enum CryptoConstants {
|
||||||
|
static var saltLength: Int {
|
||||||
|
32
|
||||||
|
}
|
||||||
|
|
||||||
|
static var ivLength: Int {
|
||||||
|
16
|
||||||
|
}
|
||||||
|
|
||||||
|
static var keyLength: Int {
|
||||||
|
32
|
||||||
|
}
|
||||||
|
|
||||||
|
static var pbkdf2NumberOfIterations: Int {
|
||||||
|
8192
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
//
|
||||||
|
// Copyright (c) 2022 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 KeychainAccess
|
||||||
|
import Foundation
|
||||||
|
import TIFoundationUtils
|
||||||
|
import LocalAuthentication
|
||||||
|
|
||||||
|
open class DefaultEncryptedTokenKeyStorage: SingleValueAuthKeychainStorage<Data> {
|
||||||
|
open class Defaults: SingleValueAuthKeychainStorage<StringEncryptionResult>.Defaults {
|
||||||
|
public static var encryptedTokenKeyStorageKey: StorageKey<Data> {
|
||||||
|
.init(rawValue: keychainServiceIdentifier + ".encryptedTokenKey")
|
||||||
|
}
|
||||||
|
|
||||||
|
public static var reusableLAContext: LAContext {
|
||||||
|
let context = LAContext()
|
||||||
|
context.touchIDAuthenticationAllowableReuseDuration = LATouchIDAuthenticationMaximumAllowableReuseDuration
|
||||||
|
return context
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(keychain: Keychain = Keychain(service: Defaults.keychainServiceIdentifier),
|
||||||
|
localAuthContext: LAContext = Defaults.reusableLAContext,
|
||||||
|
encryptedTokenKeyStorageKey: StorageKey<Data> = Defaults.encryptedTokenKeyStorageKey,
|
||||||
|
appFirstRunCheckStorage: BoolValueDefaultsStorage = DefaultResetAuthSettingsStorage()) {
|
||||||
|
|
||||||
|
let getValueClosure: GetValueClosure = { keychain, storageKey in
|
||||||
|
do {
|
||||||
|
guard let value = try keychain.getData(storageKey.rawValue) else {
|
||||||
|
return .failure(.valueNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
return .success(value)
|
||||||
|
} catch {
|
||||||
|
return .failure(.unableToExtractData(underlyingError: error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let storeValueClosure: StoreValueClosure = { keychain, value, storageKey in
|
||||||
|
do {
|
||||||
|
return .success(try keychain.set(value, key: storageKey.rawValue))
|
||||||
|
} catch {
|
||||||
|
return .failure(.unableToWriteData(underlyingError: error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
super.init(keychain: keychain.authenticationContext(localAuthContext),
|
||||||
|
storageKey: encryptedTokenKeyStorageKey,
|
||||||
|
getValueClosure: getValueClosure,
|
||||||
|
storeValueClosure: storeValueClosure,
|
||||||
|
appFirstRunCheckStorage: appFirstRunCheckStorage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
//
|
||||||
|
// Copyright (c) 2022 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 TIFoundationUtils
|
||||||
|
import KeychainAccess
|
||||||
|
|
||||||
|
open class DefaultEncryptedTokenStorage: SingleValueAuthKeychainStorage<StringEncryptionResult> {
|
||||||
|
open class Defaults: SingleValueAuthKeychainStorage<StringEncryptionResult>.Defaults {
|
||||||
|
public static var encryptedTokenStorageKey: StorageKey<StringEncryptionResult> {
|
||||||
|
.init(rawValue: keychainServiceIdentifier + ".encryptedToken")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(keychain: Keychain = Keychain(service: Defaults.keychainServiceIdentifier),
|
||||||
|
encryptedTokenStorageKey: StorageKey<StringEncryptionResult> = Defaults.encryptedTokenStorageKey,
|
||||||
|
appFirstRunCheckStorage: BoolValueDefaultsStorage = DefaultResetAuthSettingsStorage()) {
|
||||||
|
|
||||||
|
let getValueClosure: GetValueClosure = { keychain, storageKey in
|
||||||
|
do {
|
||||||
|
guard let value = try keychain.getData(storageKey.rawValue) else {
|
||||||
|
return .failure(.valueNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
return .success(try StringEncryptionResult(storableData: value))
|
||||||
|
} catch {
|
||||||
|
return .failure(.unableToDecode(underlyingError: error))
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return .failure(.unableToExtractData(underlyingError: error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let storeValueClosure: StoreValueClosure = { keychain, value, storageKey in
|
||||||
|
do {
|
||||||
|
return .success(try keychain.set(value.asStorableData(), key: storageKey.rawValue))
|
||||||
|
} catch {
|
||||||
|
return .failure(.unableToWriteData(underlyingError: error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
super.init(keychain: keychain,
|
||||||
|
storageKey: encryptedTokenStorageKey,
|
||||||
|
getValueClosure: getValueClosure,
|
||||||
|
storeValueClosure: storeValueClosure,
|
||||||
|
appFirstRunCheckStorage: appFirstRunCheckStorage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -20,20 +20,19 @@
|
||||||
// THE SOFTWARE.
|
// THE SOFTWARE.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Moya
|
|
||||||
import TISwiftUtils
|
|
||||||
import TIFoundationUtils
|
import TIFoundationUtils
|
||||||
|
import Foundation
|
||||||
|
|
||||||
public struct Cancellables {
|
open class DefaultResetAuthSettingsStorage: DefaultAppFirstRunCheckStorage {
|
||||||
public static func nonCancellable() -> Cancellable {
|
public enum Defaults {
|
||||||
NonCancellable()
|
public static var shouldResetAuthDataKey: StorageKey<Bool> {
|
||||||
|
.init(rawValue: "shouldResetAuthData")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func scoped(scopeCancellableClosure: ScopeCancellable.ScopeCancellableClosure) -> Cancellable {
|
public override init(defaults: UserDefaults = .standard,
|
||||||
ScopeCancellable(scopeCancellableClosure: scopeCancellableClosure)
|
storageKey: StorageKey<Bool> = Defaults.shouldResetAuthDataKey) {
|
||||||
}
|
|
||||||
|
|
||||||
public static func nonCancellableTask() -> CancellableTask {
|
super.init(defaults: defaults, storageKey: storageKey)
|
||||||
NonCancellable()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
//
|
||||||
|
// Copyright (c) 2022 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 TIFoundationUtils
|
||||||
|
import KeychainAccess
|
||||||
|
import Foundation
|
||||||
|
import TIKeychainUtils
|
||||||
|
|
||||||
|
open class SingleValueAuthKeychainStorage<ValueType>:
|
||||||
|
AppInstallLifetimeSingleValueStorage<BaseSingleValueKeychainStorage<ValueType>, BoolValueDefaultsStorage> {
|
||||||
|
|
||||||
|
public typealias GetValueClosure = BaseSingleValueKeychainStorage<ValueType>.GetValueClosure
|
||||||
|
public typealias StoreValueClosure = BaseSingleValueKeychainStorage<ValueType>.StoreValueClosure
|
||||||
|
|
||||||
|
open class Defaults {
|
||||||
|
public static var keychainServiceIdentifier: String {
|
||||||
|
Bundle.main.bundleIdentifier ?? "ru.touchin.TIAuth"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(keychain: Keychain = Keychain(service: Defaults.keychainServiceIdentifier),
|
||||||
|
storageKey: StorageKey<ValueType>,
|
||||||
|
getValueClosure: @escaping GetValueClosure,
|
||||||
|
storeValueClosure: @escaping StoreValueClosure,
|
||||||
|
appFirstRunCheckStorage: BoolValueDefaultsStorage = DefaultResetAuthSettingsStorage()) {
|
||||||
|
|
||||||
|
let keychainStorage = BaseSingleValueKeychainStorage(keychain: keychain,
|
||||||
|
storageKey: storageKey,
|
||||||
|
getValueClosure: getValueClosure,
|
||||||
|
storeValueClosure: storeValueClosure)
|
||||||
|
|
||||||
|
super.init(storage: keychainStorage, appFirstRunCheckStorage: appFirstRunCheckStorage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
//
|
||||||
|
// Copyright (c) 2022 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
|
||||||
|
|
||||||
|
public struct StringEncryptionResult {
|
||||||
|
public struct DataRangeMismatch: Error {
|
||||||
|
public let dataLength: Int
|
||||||
|
public let valueRangeLowerBound: Int
|
||||||
|
}
|
||||||
|
|
||||||
|
public let salt: Data
|
||||||
|
public let iv: Data
|
||||||
|
public let value: Data
|
||||||
|
|
||||||
|
private static var saltRange: Range<Int> {
|
||||||
|
.zero..<CryptoConstants.saltLength
|
||||||
|
}
|
||||||
|
|
||||||
|
private static var ivRange: Range<Int> {
|
||||||
|
saltRange.endIndex..<CryptoConstants.saltLength + CryptoConstants.ivLength
|
||||||
|
}
|
||||||
|
|
||||||
|
private static var valueRange: PartialRangeFrom<Int> {
|
||||||
|
ivRange.endIndex...
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(salt: Data, iv: Data, value: Data) {
|
||||||
|
self.salt = salt
|
||||||
|
self.iv = iv
|
||||||
|
self.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(storableData: Data) throws {
|
||||||
|
guard Self.valueRange.contains(storableData.endIndex) else {
|
||||||
|
throw DataRangeMismatch(dataLength: storableData.count,
|
||||||
|
valueRangeLowerBound: Self.valueRange.lowerBound)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.init(salt: storableData[Self.saltRange],
|
||||||
|
iv: storableData[Self.ivRange],
|
||||||
|
value: storableData[Self.valueRange])
|
||||||
|
}
|
||||||
|
|
||||||
|
public func asStorableData() -> Data {
|
||||||
|
salt + iv + value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,16 +1,25 @@
|
||||||
Pod::Spec.new do |s|
|
Pod::Spec.new do |s|
|
||||||
s.name = 'TIAuth'
|
s.name = 'TIAuth'
|
||||||
s.version = '1.20.0'
|
s.version = '1.56.0'
|
||||||
s.summary = 'Login, registration, confirmation and other related actions'
|
s.summary = 'Login, registration, confirmation and other related actions'
|
||||||
s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
|
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
|
||||||
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
||||||
s.author = { 'petropavel13' => 'ivan.smolin@touchin.ru' }
|
s.author = { 'petropavel13' => 'ivan.smolin@touchin.ru' }
|
||||||
s.source = { :git => 'https://github.com/TouchInstinct/LeadKit.git', :tag => s.version.to_s }
|
s.source = { :git => 'https://git.svc.touchin.ru/TouchInstinct/LeadKit.git', :tag => s.version.to_s }
|
||||||
|
|
||||||
s.ios.deployment_target = '13.0'
|
s.ios.deployment_target = '13.0'
|
||||||
s.swift_versions = ['5.3']
|
s.swift_versions = ['5.7']
|
||||||
|
|
||||||
s.source_files = s.name + '/Sources/**/*'
|
sources = 'Sources/**/*'
|
||||||
|
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
|
||||||
|
|
||||||
s.dependency 'TIFoundationUtils', s.version.to_s
|
s.dependency 'TIFoundationUtils', s.version.to_s
|
||||||
|
s.dependency 'TIKeychainUtils', s.version.to_s
|
||||||
|
s.dependency 'TIUIKitCore', s.version.to_s
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
ENV["DEVELOPMENT_INSTALL"] = "true"
|
||||||
|
|
||||||
|
source 'https://git.svc.touchin.ru/TouchInstinct/Podspecs.git'
|
||||||
|
|
||||||
|
target 'TIBottomSheet' do
|
||||||
|
platform :ios, 11
|
||||||
|
use_frameworks!
|
||||||
|
|
||||||
|
pod 'TIUIElements', :path => '../../../../TIUIElements/TIUIElements.podspec'
|
||||||
|
pod 'TIUIKitCore', :path => '../../../../TIUIKitCore/TIUIKitCore.podspec'
|
||||||
|
pod 'TISwiftUtils', :path => '../../../../TISwiftUtils/TISwiftUtils.podspec'
|
||||||
|
pod 'TIBottomSheet', :path => '../../../../TIBottomSheet/TIBottomSheet.podspec'
|
||||||
|
pod 'TILogging', :path => '../../../../TILogging/TILogging.podspec'
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
//
|
||||||
|
// 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 TIUIElements
|
||||||
|
import TIUIKitCore
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
extension BaseModalViewController {
|
||||||
|
|
||||||
|
open class BaseAppearance: UIView.BaseAppearance<UIView.DefaultWrappedLayout> {
|
||||||
|
|
||||||
|
public var presentationDetents: [ModalViewPresentationDetent]
|
||||||
|
public var dragViewState: DragView.State
|
||||||
|
public var headerViewState: ModalHeaderView.State
|
||||||
|
public var footerViewState: ModalFooterView<FooterContentView>.State
|
||||||
|
|
||||||
|
public init(layout: UIView.DefaultWrappedLayout = .defaultLayout,
|
||||||
|
background: UIViewBackground = UIViewColorBackground(color: .clear),
|
||||||
|
border: UIViewBorder = BaseUIViewBorder(),
|
||||||
|
shadow: UIViewShadow? = nil,
|
||||||
|
presentationDetents: [ModalViewPresentationDetent] = [.maxHeight],
|
||||||
|
dragViewState: DragView.State = .hidden,
|
||||||
|
headerViewState: ModalHeaderView.State = .hidden,
|
||||||
|
footerViewState: ModalFooterView<FooterContentView>.State = .hidden) {
|
||||||
|
|
||||||
|
self.presentationDetents = presentationDetents
|
||||||
|
self.dragViewState = dragViewState
|
||||||
|
self.headerViewState = headerViewState
|
||||||
|
self.footerViewState = footerViewState
|
||||||
|
|
||||||
|
super.init(layout: layout,
|
||||||
|
background: background,
|
||||||
|
border: border,
|
||||||
|
shadow: shadow)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public final class DefaultAppearance: BaseAppearance, ViewAppearance {
|
||||||
|
public static var defaultAppearance: Self {
|
||||||
|
Self()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,473 @@
|
||||||
|
//
|
||||||
|
// 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 TISwiftUtils
|
||||||
|
import TIUIElements
|
||||||
|
import TIUIKitCore
|
||||||
|
import UIKit
|
||||||
|
import PanModal
|
||||||
|
|
||||||
|
open class BaseModalViewController<ContentView: UIView,
|
||||||
|
FooterContentView: UIView>: BaseInitializableViewController, PanModalPresentable {
|
||||||
|
|
||||||
|
// MARK: - Public Properties
|
||||||
|
|
||||||
|
public let dragView = DragView()
|
||||||
|
public let headerView = ModalHeaderView()
|
||||||
|
private(set) public lazy var contentView = createContentView()
|
||||||
|
private(set) public lazy var footerView = createFooterView()
|
||||||
|
|
||||||
|
public private(set) lazy var dragViewBottomToHeaderViewTopConstraint: NSLayoutConstraint = {
|
||||||
|
dragView.bottomAnchor.constraint(equalTo: headerView.topAnchor)
|
||||||
|
}()
|
||||||
|
|
||||||
|
public private(set) lazy var dragViewBottomToContentViewTopConstraint: NSLayoutConstraint = {
|
||||||
|
dragView.bottomAnchor.constraint(equalTo: contentView.topAnchor)
|
||||||
|
}()
|
||||||
|
|
||||||
|
public private(set) lazy var dragViewConstraints: SubviewConstraints = {
|
||||||
|
let trailingConstraint = dragView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
|
||||||
|
|
||||||
|
let edgeConstraints = EdgeConstraints(leadingConstraint: dragView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||||
|
trailingConstraint: trailingConstraint,
|
||||||
|
topConstraint: dragView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
|
bottomConstraint: dragViewBottomToHeaderViewTopConstraint)
|
||||||
|
|
||||||
|
let centerXConstraint = dragView.centerXAnchor.constraint(equalTo: view.centerXAnchor)
|
||||||
|
let centerYConstraint = dragView.centerYAnchor.constraint(equalTo: view.centerYAnchor)
|
||||||
|
|
||||||
|
let centerConstraints = CenterConstraints(centerXConstraint: centerXConstraint,
|
||||||
|
centerYConstraint: centerYConstraint)
|
||||||
|
|
||||||
|
let sizeConstraints = SizeConstraints(widthConstraint: dragView.widthAnchor.constraint(equalToConstant: .zero),
|
||||||
|
heightConstraint: dragView.heightAnchor.constraint(equalToConstant: .zero))
|
||||||
|
|
||||||
|
return SubviewConstraints(edgeConstraints: edgeConstraints,
|
||||||
|
centerConstraints: centerConstraints,
|
||||||
|
sizeConstraints: sizeConstraints)
|
||||||
|
}()
|
||||||
|
|
||||||
|
public private(set) lazy var headerViewToSuperviewTopConstraint: NSLayoutConstraint = {
|
||||||
|
headerView.topAnchor.constraint(equalTo: view.topAnchor)
|
||||||
|
}()
|
||||||
|
|
||||||
|
public private(set) lazy var headerBottomToContentTopConstraint: NSLayoutConstraint = {
|
||||||
|
headerView.bottomAnchor.constraint(equalTo: contentView.topAnchor)
|
||||||
|
}()
|
||||||
|
|
||||||
|
public private(set) lazy var headerViewConstraints: SubviewConstraints = {
|
||||||
|
let leadingConstraint = headerView.leadingAnchor.constraint(equalTo: view.leadingAnchor)
|
||||||
|
let trailingConstraint = headerView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
|
||||||
|
|
||||||
|
let edgeConstraints = EdgeConstraints(leadingConstraint: leadingConstraint,
|
||||||
|
trailingConstraint: trailingConstraint,
|
||||||
|
topConstraint: dragViewBottomToHeaderViewTopConstraint,
|
||||||
|
bottomConstraint: headerBottomToContentTopConstraint)
|
||||||
|
|
||||||
|
let centerXConstraint = headerView.centerXAnchor.constraint(equalTo: view.centerXAnchor)
|
||||||
|
let centerYConstraint = headerView.centerYAnchor.constraint(equalTo: view.centerYAnchor)
|
||||||
|
|
||||||
|
let centerConstraints = CenterConstraints(centerXConstraint: centerXConstraint,
|
||||||
|
centerYConstraint: centerYConstraint)
|
||||||
|
|
||||||
|
let sizeConstraints = SizeConstraints(widthConstraint: headerView.widthAnchor.constraint(equalToConstant: .zero),
|
||||||
|
heightConstraint: headerView.heightAnchor.constraint(equalToConstant: .zero))
|
||||||
|
|
||||||
|
return SubviewConstraints(edgeConstraints: edgeConstraints,
|
||||||
|
centerConstraints: centerConstraints,
|
||||||
|
sizeConstraints: sizeConstraints)
|
||||||
|
}()
|
||||||
|
|
||||||
|
public private(set) lazy var contentViewTopToSuperviewConstraint: NSLayoutConstraint = {
|
||||||
|
contentView.topAnchor.constraint(equalTo: view.topAnchor)
|
||||||
|
}()
|
||||||
|
|
||||||
|
public private(set) lazy var contentViewBottomToSuperviewConstraint: NSLayoutConstraint = {
|
||||||
|
contentView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
|
||||||
|
}()
|
||||||
|
|
||||||
|
public private(set) lazy var contentViewConstraints: SubviewConstraints = {
|
||||||
|
let leadingConstraint = contentView.leadingAnchor.constraint(equalTo: view.leadingAnchor)
|
||||||
|
let trailingConstraint = contentView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
|
||||||
|
|
||||||
|
let edgeConstraints = EdgeConstraints(leadingConstraint: leadingConstraint,
|
||||||
|
trailingConstraint: trailingConstraint,
|
||||||
|
topConstraint: headerBottomToContentTopConstraint,
|
||||||
|
bottomConstraint: contentViewBottomToSuperviewConstraint)
|
||||||
|
|
||||||
|
let centerXConstraint = contentView.centerXAnchor.constraint(equalTo: view.centerXAnchor)
|
||||||
|
let centerYConstraint = contentView.centerYAnchor.constraint(equalTo: view.centerYAnchor)
|
||||||
|
|
||||||
|
let centerConstraints = CenterConstraints(centerXConstraint: centerXConstraint,
|
||||||
|
centerYConstraint: centerYConstraint)
|
||||||
|
|
||||||
|
let sizeConstraints = SizeConstraints(widthConstraint: contentView.widthAnchor.constraint(equalToConstant: .zero),
|
||||||
|
heightConstraint: contentView.heightAnchor.constraint(equalToConstant: .zero))
|
||||||
|
|
||||||
|
return SubviewConstraints(edgeConstraints: edgeConstraints,
|
||||||
|
centerConstraints: centerConstraints,
|
||||||
|
sizeConstraints: sizeConstraints)
|
||||||
|
}()
|
||||||
|
|
||||||
|
public private(set) lazy var footerViewConstraints: SubviewConstraints = {
|
||||||
|
let leadingConstraint = footerView.leadingAnchor.constraint(equalTo: view.leadingAnchor)
|
||||||
|
let trailingConstraint = footerView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
|
||||||
|
|
||||||
|
let edgeConstraints = EdgeConstraints(leadingConstraint: leadingConstraint,
|
||||||
|
trailingConstraint: trailingConstraint,
|
||||||
|
topConstraint: footerView.topAnchor.constraint(equalTo: contentView.bottomAnchor),
|
||||||
|
bottomConstraint: footerView.bottomAnchor.constraint(equalTo: view.bottomAnchor))
|
||||||
|
|
||||||
|
let centerXConstraint = footerView.centerXAnchor.constraint(equalTo: view.centerXAnchor)
|
||||||
|
let centerYConstraint = footerView.centerYAnchor.constraint(equalTo: view.centerYAnchor)
|
||||||
|
|
||||||
|
let centerConstraints = CenterConstraints(centerXConstraint: centerXConstraint,
|
||||||
|
centerYConstraint: centerYConstraint)
|
||||||
|
|
||||||
|
let sizeConstraints = SizeConstraints(widthConstraint: footerView.widthAnchor.constraint(equalToConstant: .zero),
|
||||||
|
heightConstraint: footerView.heightAnchor.constraint(equalToConstant: .zero))
|
||||||
|
|
||||||
|
return SubviewConstraints(edgeConstraints: edgeConstraints,
|
||||||
|
centerConstraints: centerConstraints,
|
||||||
|
sizeConstraints: sizeConstraints)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// MARK: - Modal View Controller Configuration
|
||||||
|
|
||||||
|
public var viewControllerAppearance: BaseAppearance = .init(background: UIViewColorBackground(color: .white))
|
||||||
|
|
||||||
|
open var panScrollable: UIScrollView? {
|
||||||
|
contentView as? UIScrollView
|
||||||
|
}
|
||||||
|
|
||||||
|
public var panScrollableInsets: UIEdgeInsets = .zero {
|
||||||
|
didSet {
|
||||||
|
panScrollable?.contentInset = panScrollableInsets
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var dimmedView = DimmedView()
|
||||||
|
|
||||||
|
public var showDragIndicator = true
|
||||||
|
|
||||||
|
open var headerViewHeight: CGFloat {
|
||||||
|
let dragViewHeight = getHeight(of: dragView)
|
||||||
|
let headerViewHeight = getHeight(of: headerView)
|
||||||
|
let dragViewVerticalInsets = getDragViewVerticalInsets()
|
||||||
|
let headerViewVerticalInsets = getHeaderViewVerticalInsets()
|
||||||
|
|
||||||
|
return dragViewHeight + headerViewHeight + dragViewVerticalInsets + headerViewVerticalInsets
|
||||||
|
}
|
||||||
|
|
||||||
|
open var longFormHeight: PanModalHeight {
|
||||||
|
let detents = getSortedDetents()
|
||||||
|
|
||||||
|
return detents.max()?.panModalHeight(headerHeight: headerViewHeight) ?? .maxHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
open var mediumFormHeight: PanModalHeight {
|
||||||
|
let detents = getSortedDetents()
|
||||||
|
|
||||||
|
if detents.count > 1 {
|
||||||
|
return detents[1].panModalHeight(headerHeight: headerViewHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
return detents.first?.panModalHeight(headerHeight: headerViewHeight) ?? .maxHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
open var shortFormHeight: PanModalHeight {
|
||||||
|
let detents = getSortedDetents()
|
||||||
|
|
||||||
|
return detents.min()?.panModalHeight(headerHeight: headerViewHeight) ?? .maxHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
private var keyboardDidShownObserver: NSObjectProtocol?
|
||||||
|
private var keyboardDidHiddenObserver: NSObjectProtocol?
|
||||||
|
|
||||||
|
// MARK: - Life Cycle
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
let notificationCenter = NotificationCenter.default
|
||||||
|
|
||||||
|
if let keyboardDidShownObserver {
|
||||||
|
notificationCenter.removeObserver(keyboardDidShownObserver)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let keyboardDidHiddenObserver {
|
||||||
|
notificationCenter.removeObserver(keyboardDidHiddenObserver)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - BaseInitializableViewController
|
||||||
|
|
||||||
|
open override func addViews() {
|
||||||
|
super.addViews()
|
||||||
|
|
||||||
|
view.addSubviews(dragView, headerView, contentView, footerView)
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func configureLayout() {
|
||||||
|
super.configureLayout()
|
||||||
|
|
||||||
|
for view in [dragView, headerView, contentView, footerView] {
|
||||||
|
view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
}
|
||||||
|
|
||||||
|
configureDragViewLayout()
|
||||||
|
configureHeaderViewLayout()
|
||||||
|
configureContentViewLayout()
|
||||||
|
configureFooterViewLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func bindViews() {
|
||||||
|
super.bindViews()
|
||||||
|
|
||||||
|
keyboardDidShownObserver = NotificationCenter.default
|
||||||
|
.addObserver(forName: UIResponder.keyboardDidShowNotification,
|
||||||
|
object: nil,
|
||||||
|
queue: .main) { [weak self] notification in
|
||||||
|
self?.configureLayoutForKeyboard(notification, isKeyboardHidden: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
keyboardDidHiddenObserver = NotificationCenter.default
|
||||||
|
.addObserver(forName: UIResponder.keyboardDidHideNotification,
|
||||||
|
object: nil,
|
||||||
|
queue: .main) { [weak self] notification in
|
||||||
|
self?.configureLayoutForKeyboard(notification, isKeyboardHidden: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func configureAppearance() {
|
||||||
|
super.configureAppearance()
|
||||||
|
|
||||||
|
view.configureUIView(appearance: viewControllerAppearance)
|
||||||
|
configureDragViewAppearance()
|
||||||
|
configureHeaderViewAppearance()
|
||||||
|
configureFooterViewAppearance()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Open Methods
|
||||||
|
|
||||||
|
open func createContentView() -> ContentView {
|
||||||
|
ContentView()
|
||||||
|
}
|
||||||
|
|
||||||
|
open func createFooterView() -> ModalFooterView<FooterContentView> {
|
||||||
|
ModalFooterView<FooterContentView>()
|
||||||
|
}
|
||||||
|
|
||||||
|
open func configureLayoutForKeyboard(_ notification: Notification, isKeyboardHidden: Bool) {
|
||||||
|
guard let keyboardHeight = getKeyboardHeight(notification) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if case let .presented(footerViewAppearance) = viewControllerAppearance.footerViewState {
|
||||||
|
let bottomInset = footerViewAppearance.layout.insets.add(\.bottom,
|
||||||
|
to: \.bottom,
|
||||||
|
of: .vertical(bottom: keyboardHeight))
|
||||||
|
|
||||||
|
footerViewConstraints.edgeConstraints.bottomConstraint.constant = bottomInset
|
||||||
|
} else if let panScrollable {
|
||||||
|
var insets = panScrollableInsets
|
||||||
|
|
||||||
|
if isKeyboardHidden {
|
||||||
|
panScrollable.contentInset = insets
|
||||||
|
} else {
|
||||||
|
insets.bottom += keyboardHeight
|
||||||
|
|
||||||
|
panScrollable.contentInset = insets
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Private Methods
|
||||||
|
|
||||||
|
private func configureDragViewLayout() {
|
||||||
|
guard case let .presented(dragViewAppearance) = viewControllerAppearance.dragViewState else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let bottomConstraint: NSLayoutConstraint
|
||||||
|
let bottomConstant: CGFloat
|
||||||
|
|
||||||
|
if case let .presented(headerViewAppearance) = viewControllerAppearance.headerViewState {
|
||||||
|
dragViewBottomToContentViewTopConstraint.isActive = false
|
||||||
|
bottomConstraint = dragViewBottomToHeaderViewTopConstraint
|
||||||
|
bottomConstant = dragViewAppearance.layout.insets.add(\.bottom,
|
||||||
|
to: \.top,
|
||||||
|
of: headerViewAppearance.layout.insets)
|
||||||
|
} else {
|
||||||
|
dragViewBottomToHeaderViewTopConstraint.isActive = false
|
||||||
|
bottomConstraint = dragViewBottomToContentViewTopConstraint
|
||||||
|
bottomConstant = dragViewAppearance.layout.insets.bottom
|
||||||
|
}
|
||||||
|
|
||||||
|
dragViewConstraints.edgeConstraints.bottomConstraint = bottomConstraint
|
||||||
|
dragViewConstraints.update(from: dragViewAppearance.layout)
|
||||||
|
|
||||||
|
bottomConstraint.setActiveConstantOrDeactivate(constant: bottomConstant)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func configureHeaderViewLayout() {
|
||||||
|
guard case let .presented(headerViewAppearance) = viewControllerAppearance.headerViewState else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let topConstraint: NSLayoutConstraint
|
||||||
|
let topConstant: CGFloat
|
||||||
|
|
||||||
|
if case let .presented(dragViewAppearance) = viewControllerAppearance.dragViewState {
|
||||||
|
dragViewBottomToContentViewTopConstraint.isActive = false
|
||||||
|
topConstraint = dragViewBottomToHeaderViewTopConstraint
|
||||||
|
|
||||||
|
topConstant = dragViewAppearance.layout.insets.add(\.bottom,
|
||||||
|
to: \.top,
|
||||||
|
of: headerViewAppearance.layout.insets)
|
||||||
|
} else {
|
||||||
|
dragViewBottomToHeaderViewTopConstraint.isActive = false
|
||||||
|
topConstraint = headerViewToSuperviewTopConstraint
|
||||||
|
|
||||||
|
let topInset = headerViewAppearance.layout.insets.top
|
||||||
|
topConstant = topInset.isFinite ? topInset : .zero
|
||||||
|
}
|
||||||
|
|
||||||
|
headerViewConstraints.edgeConstraints.topConstraint = topConstraint
|
||||||
|
headerViewConstraints.update(from: headerViewAppearance.layout)
|
||||||
|
|
||||||
|
topConstraint.setActiveConstantOrDeactivate(constant: topConstant)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func configureContentViewLayout() {
|
||||||
|
let topConstraint: NSLayoutConstraint
|
||||||
|
let topConstant: CGFloat
|
||||||
|
|
||||||
|
if case let .presented(headerViewAppearance) = viewControllerAppearance.headerViewState {
|
||||||
|
dragViewBottomToContentViewTopConstraint.isActive = false
|
||||||
|
contentViewTopToSuperviewConstraint.isActive = false
|
||||||
|
|
||||||
|
topConstraint = headerBottomToContentTopConstraint
|
||||||
|
topConstant = headerViewAppearance.layout.insets.bottom
|
||||||
|
} else if case let .presented(dragViewAppearance) = viewControllerAppearance.dragViewState {
|
||||||
|
contentViewTopToSuperviewConstraint.isActive = false
|
||||||
|
headerBottomToContentTopConstraint.isActive = false
|
||||||
|
|
||||||
|
topConstraint = dragViewBottomToContentViewTopConstraint
|
||||||
|
topConstant = dragViewAppearance.layout.insets.bottom
|
||||||
|
} else {
|
||||||
|
headerBottomToContentTopConstraint.isActive = false
|
||||||
|
dragViewBottomToContentViewTopConstraint.isActive = false
|
||||||
|
|
||||||
|
topConstraint = contentViewTopToSuperviewConstraint
|
||||||
|
topConstant = .zero
|
||||||
|
}
|
||||||
|
|
||||||
|
contentViewConstraints.edgeConstraints.topConstraint = topConstraint
|
||||||
|
let layout = UIView.DefaultWrappedLayout(insets: .horizontal(.zero)
|
||||||
|
.vertical(top: topConstant)
|
||||||
|
.replacingNan(with: .zero))
|
||||||
|
contentViewConstraints.update(from: layout)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func configureFooterViewLayout() {
|
||||||
|
guard case let .presented(footerViewAppearance) = viewControllerAppearance.footerViewState else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
contentViewConstraints.edgeConstraints.bottomConstraint.isActive = false
|
||||||
|
contentViewConstraints.edgeConstraints.bottomConstraint = footerViewConstraints.edgeConstraints.topConstraint
|
||||||
|
|
||||||
|
footerViewConstraints.update(from: footerViewAppearance.layout)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func configureDragViewAppearance() {
|
||||||
|
switch viewControllerAppearance.dragViewState {
|
||||||
|
case .hidden:
|
||||||
|
dragView.isHidden = true
|
||||||
|
|
||||||
|
case let .presented(appearance):
|
||||||
|
dragView.configure(appearance: appearance)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func configureHeaderViewAppearance() {
|
||||||
|
switch viewControllerAppearance.headerViewState {
|
||||||
|
case .hidden:
|
||||||
|
headerView.isHidden = true
|
||||||
|
|
||||||
|
case let .presented(appearance):
|
||||||
|
headerView.configure(appearance: appearance)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func configureFooterViewAppearance() {
|
||||||
|
switch viewControllerAppearance.footerViewState {
|
||||||
|
case .hidden:
|
||||||
|
footerView.isHidden = true
|
||||||
|
|
||||||
|
case let .presented(appearance):
|
||||||
|
footerView.configureBaseWrappedViewHolder(appearance: appearance)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getSortedDetents() -> [ModalViewPresentationDetent] {
|
||||||
|
viewControllerAppearance.presentationDetents.uniqued().sorted()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getHeight(of view: UIView) -> CGFloat {
|
||||||
|
guard !view.isHidden else {
|
||||||
|
return .zero
|
||||||
|
}
|
||||||
|
|
||||||
|
return getFittingSize(forView: view).height
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getDragViewVerticalInsets() -> CGFloat {
|
||||||
|
guard case let .presented(appearance) = viewControllerAppearance.dragViewState else {
|
||||||
|
return .zero
|
||||||
|
}
|
||||||
|
|
||||||
|
return appearance.layout.insets.vertical(onNan: .zero)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getHeaderViewVerticalInsets() -> CGFloat {
|
||||||
|
guard case let .presented(appearance) = viewControllerAppearance.headerViewState else {
|
||||||
|
return .zero
|
||||||
|
}
|
||||||
|
|
||||||
|
return appearance.layout.insets.vertical(onNan: .zero)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getFittingSize(forView view: UIView) -> CGSize {
|
||||||
|
let targetSize = CGSize(width: UIScreen.main.bounds.width,
|
||||||
|
height: UIView.layoutFittingCompressedSize.height)
|
||||||
|
|
||||||
|
return view.systemLayoutSizeFitting(targetSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getKeyboardHeight(_ notification: Notification) -> CGFloat? {
|
||||||
|
guard let userInfo = notification.userInfo else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect)?.height
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
//
|
||||||
|
// 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 UIKit
|
||||||
|
|
||||||
|
open class DefaultModalWrapperViewController<ContentController: UIViewController>: BaseModalViewController<UIView, UIView> {
|
||||||
|
|
||||||
|
private(set) public var contentViewController: ContentController
|
||||||
|
|
||||||
|
public init(contentViewController: ContentController) {
|
||||||
|
self.contentViewController = contentViewController
|
||||||
|
|
||||||
|
super.init(nibName: nil, bundle: nil)
|
||||||
|
|
||||||
|
addChild(contentViewController)
|
||||||
|
contentViewController.didMove(toParent: self)
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(*, unavailable)
|
||||||
|
required public init?(coder: NSCoder) {
|
||||||
|
contentViewController = ContentController()
|
||||||
|
|
||||||
|
super.init(coder: coder)
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func createContentView() -> UIView {
|
||||||
|
contentViewController.view
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension UIViewController {
|
||||||
|
func wrappedInBottomSheetController() -> DefaultModalWrapperViewController<UIViewController> {
|
||||||
|
DefaultModalWrapperViewController(contentViewController: self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
//
|
||||||
|
// 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 TIUIKitCore
|
||||||
|
|
||||||
|
extension UIViewShadow {
|
||||||
|
public static var defaultModalViewShadow: Self {
|
||||||
|
.init {
|
||||||
|
Color(.black)
|
||||||
|
Opacity(0.5)
|
||||||
|
Offset(0, -5)
|
||||||
|
Radius(10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
//
|
||||||
|
// Copyright (c) 2022 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 PanModal
|
||||||
|
|
||||||
|
public struct ModalViewPresentationDetent: Hashable {
|
||||||
|
|
||||||
|
// MARK: - Default Values
|
||||||
|
|
||||||
|
public static var headerOnly: Self {
|
||||||
|
Self(height: -.greatestFiniteMagnitude)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func height(_ height: CGFloat) -> Self {
|
||||||
|
Self(height: height)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static var maxHeight: Self {
|
||||||
|
Self(height: .greatestFiniteMagnitude)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Public Properties
|
||||||
|
|
||||||
|
public var height: CGFloat
|
||||||
|
|
||||||
|
// MARK: - Internal Methods
|
||||||
|
|
||||||
|
func panModalHeight(headerHeight: CGFloat = .zero) -> PanModalHeight {
|
||||||
|
if self == .headerOnly {
|
||||||
|
return .contentHeight(headerHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
if self == .maxHeight {
|
||||||
|
return .maxHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
return .contentHeight(height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Comparable
|
||||||
|
|
||||||
|
extension ModalViewPresentationDetent: Comparable {
|
||||||
|
public static func < (lhs: ModalViewPresentationDetent, rhs: ModalViewPresentationDetent) -> Bool {
|
||||||
|
lhs.height < rhs.height
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
//
|
||||||
|
// 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 TIUIElements
|
||||||
|
import TIUIKitCore
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
public final class DragView: BaseInitializableView, AppearanceConfigurable {
|
||||||
|
|
||||||
|
// MARK: - Nested Types
|
||||||
|
|
||||||
|
private enum Constants {
|
||||||
|
static var dragViewTopInset: CGFloat {
|
||||||
|
8
|
||||||
|
}
|
||||||
|
|
||||||
|
static var dragViewBorder: UIViewBorder {
|
||||||
|
UIViewRoundedBorder(cornerRadius: dragViewSize.height / 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
static var dragViewBackground: UIViewColorBackground {
|
||||||
|
UIViewColorBackground(color: .lightGray)
|
||||||
|
}
|
||||||
|
|
||||||
|
static var dragViewSize: CGSize {
|
||||||
|
CGSize(width: 52, height: 7)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum State {
|
||||||
|
case hidden
|
||||||
|
case presented(Appearance)
|
||||||
|
}
|
||||||
|
|
||||||
|
public final class Appearance: UIView.BaseWrappedAppearance<UIView.DefaultWrappedLayout>, WrappedViewAppearance {
|
||||||
|
|
||||||
|
public static var defaultAppearance: Self {
|
||||||
|
Self(layout: DefaultWrappedLayout(insets: .vertical(top: Constants.dragViewTopInset),
|
||||||
|
size: Constants.dragViewSize,
|
||||||
|
centerOffset: .centerHorizontal()),
|
||||||
|
background: Constants.dragViewBackground,
|
||||||
|
border: Constants.dragViewBorder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - AppearanceConfigurable
|
||||||
|
|
||||||
|
public func configure(appearance: Appearance) {
|
||||||
|
configureUIView(appearance: appearance)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
//
|
||||||
|
// 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 TIUIElements
|
||||||
|
import TIUIKitCore
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
open class ModalFooterView<ContentView: UIView>: ContainerView<ContentView> {
|
||||||
|
// MARK: - Nested Types
|
||||||
|
|
||||||
|
public enum State {
|
||||||
|
case hidden
|
||||||
|
case presented(Appearance)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension ModalFooterView {
|
||||||
|
|
||||||
|
final class Appearance: BaseWrappedViewHolderAppearance<DefaultWrappedAppearance, DefaultWrappedLayout>,
|
||||||
|
WrappedViewHolderAppearance {
|
||||||
|
public static var defaultAppearance: Self {
|
||||||
|
Self(layout: DefaultWrappedLayout(insets: .horizontal(.zero).vertical(bottom: .zero),
|
||||||
|
size: .fixedHeight(44)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,235 @@
|
||||||
|
//
|
||||||
|
// 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 TIUIElements
|
||||||
|
import TIUIKitCore
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
open class ModalHeaderView: BaseInitializableView, AppearanceConfigurable {
|
||||||
|
|
||||||
|
// MARK: - Nested Types
|
||||||
|
|
||||||
|
public enum State {
|
||||||
|
case hidden
|
||||||
|
case presented(Appearance)
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum ContentViewState {
|
||||||
|
case leadingButton(BaseButtonStyle)
|
||||||
|
case trailingButton(BaseButtonStyle)
|
||||||
|
case buttons(leading: BaseButtonStyle, trailing: BaseButtonStyle)
|
||||||
|
case custom(view: UIView, appearance: UIView.BaseWrappedAppearance<UIView.DefaultWrappedLayout>)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Public properties
|
||||||
|
|
||||||
|
public let leadingButton = StatefulButton()
|
||||||
|
public let trailingButton = StatefulButton()
|
||||||
|
|
||||||
|
public private(set) lazy var leftTrailingToRightLeadingConstraint: NSLayoutConstraint = {
|
||||||
|
leadingButton.trailingAnchor.constraint(equalTo: trailingButton.leadingAnchor)
|
||||||
|
}()
|
||||||
|
|
||||||
|
public private(set) lazy var leftTrailingToSuperviewTrailing: NSLayoutConstraint = {
|
||||||
|
leadingButton.trailingAnchor.constraint(equalTo: trailingAnchor)
|
||||||
|
}()
|
||||||
|
|
||||||
|
public private(set) lazy var leadingButtonConstraints: SubviewConstraints = {
|
||||||
|
let edgeConstraints = EdgeConstraints(leadingConstraint: leadingButton.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
|
trailingConstraint: leftTrailingToRightLeadingConstraint,
|
||||||
|
topConstraint: leadingButton.topAnchor.constraint(equalTo: topAnchor),
|
||||||
|
bottomConstraint: leadingButton.bottomAnchor.constraint(equalTo: bottomAnchor))
|
||||||
|
|
||||||
|
let centerXConstraint = leadingButton.centerXAnchor.constraint(equalTo: centerXAnchor)
|
||||||
|
let centerYConstraint = leadingButton.centerYAnchor.constraint(equalTo: centerYAnchor)
|
||||||
|
|
||||||
|
let centerConstraints = CenterConstraints(centerXConstraint: centerXConstraint,
|
||||||
|
centerYConstraint: centerYConstraint)
|
||||||
|
|
||||||
|
let sizeConstraints = SizeConstraints(widthConstraint: leadingButton.widthAnchor.constraint(equalToConstant: .zero),
|
||||||
|
heightConstraint: leadingButton.heightAnchor.constraint(equalToConstant: .zero))
|
||||||
|
|
||||||
|
return SubviewConstraints(edgeConstraints: edgeConstraints,
|
||||||
|
centerConstraints: centerConstraints,
|
||||||
|
sizeConstraints: sizeConstraints)
|
||||||
|
}()
|
||||||
|
|
||||||
|
public private(set) lazy var rightLeadingToSuperviewLeadingConstraint: NSLayoutConstraint = {
|
||||||
|
trailingButton.leadingAnchor.constraint(equalTo: leadingAnchor)
|
||||||
|
}()
|
||||||
|
|
||||||
|
public private(set) lazy var trailingButtonConstraints: SubviewConstraints = {
|
||||||
|
let trailingConstraint = trailingButton.trailingAnchor.constraint(equalTo: trailingAnchor)
|
||||||
|
|
||||||
|
let edgeConstraints = EdgeConstraints(leadingConstraint: leftTrailingToRightLeadingConstraint,
|
||||||
|
trailingConstraint: trailingConstraint,
|
||||||
|
topConstraint: trailingButton.topAnchor.constraint(equalTo: topAnchor),
|
||||||
|
bottomConstraint: trailingButton.bottomAnchor.constraint(equalTo: bottomAnchor))
|
||||||
|
|
||||||
|
let centerXConstraint = trailingButton.centerXAnchor.constraint(equalTo: centerXAnchor)
|
||||||
|
let centerYConstraint = trailingButton.centerYAnchor.constraint(equalTo: centerYAnchor)
|
||||||
|
|
||||||
|
let centerConstraints = CenterConstraints(centerXConstraint: centerXConstraint,
|
||||||
|
centerYConstraint: centerYConstraint)
|
||||||
|
|
||||||
|
let sizeConstraints = SizeConstraints(widthConstraint: trailingButton.widthAnchor.constraint(equalToConstant: .zero),
|
||||||
|
heightConstraint: trailingButton.heightAnchor.constraint(equalToConstant: .zero))
|
||||||
|
|
||||||
|
return SubviewConstraints(edgeConstraints: edgeConstraints,
|
||||||
|
centerConstraints: centerConstraints,
|
||||||
|
sizeConstraints: sizeConstraints)
|
||||||
|
}()
|
||||||
|
|
||||||
|
public var customViewConstraints: SubviewConstraints?
|
||||||
|
|
||||||
|
// MARK: - BaseInitializableView
|
||||||
|
|
||||||
|
open override func addViews() {
|
||||||
|
super.addViews()
|
||||||
|
|
||||||
|
addSubviews(leadingButton, trailingButton)
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func configureLayout() {
|
||||||
|
super.configureLayout()
|
||||||
|
|
||||||
|
for view in [leadingButton, trailingButton] {
|
||||||
|
view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - AppearanceConfigurable
|
||||||
|
|
||||||
|
open func configure(appearance: Appearance) {
|
||||||
|
configureUIView(appearance: appearance)
|
||||||
|
|
||||||
|
configureContentView(state: appearance.contentViewState)
|
||||||
|
}
|
||||||
|
|
||||||
|
open func configureContentView(state: ContentViewState) {
|
||||||
|
var leadingButtonStyle: BaseButtonStyle?
|
||||||
|
var trailingButtonStyle: BaseButtonStyle?
|
||||||
|
|
||||||
|
switch state {
|
||||||
|
case let .leadingButton(style):
|
||||||
|
leadingButtonStyle = style
|
||||||
|
leadingButtonConstraints.edgeConstraints.trailingConstraint = leftTrailingToSuperviewTrailing
|
||||||
|
|
||||||
|
case let .trailingButton(style):
|
||||||
|
trailingButtonStyle = style
|
||||||
|
trailingButtonConstraints.edgeConstraints.leadingConstraint = rightLeadingToSuperviewLeadingConstraint
|
||||||
|
|
||||||
|
case let .buttons(leading, trailing):
|
||||||
|
leadingButtonStyle = leading
|
||||||
|
trailingButtonStyle = trailing
|
||||||
|
|
||||||
|
case let .custom(view, appearance):
|
||||||
|
configureCustomView(view, withLayout: appearance.layout)
|
||||||
|
view.configureUIView(appearance: appearance)
|
||||||
|
}
|
||||||
|
|
||||||
|
configure(buttonStyle: leadingButtonStyle, forButton: leadingButton, constraints: leadingButtonConstraints)
|
||||||
|
configure(buttonStyle: trailingButtonStyle, forButton: trailingButton, constraints: trailingButtonConstraints)
|
||||||
|
|
||||||
|
if let leadingButtonStyle, let trailingButtonStyle {
|
||||||
|
leadingButtonConstraints.edgeConstraints.trailingConstraint = leftTrailingToRightLeadingConstraint
|
||||||
|
trailingButtonConstraints.edgeConstraints.leadingConstraint = leftTrailingToRightLeadingConstraint
|
||||||
|
|
||||||
|
let spacing = leadingButtonStyle.appearance.layout.insets.add(\.right,
|
||||||
|
to: \.left,
|
||||||
|
of: trailingButtonStyle.appearance.layout.insets)
|
||||||
|
|
||||||
|
leftTrailingToRightLeadingConstraint.setActiveConstantOrDeactivate(constant: spacing)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Private methods
|
||||||
|
|
||||||
|
private func configureCustomView(_ view: UIView, withLayout layout: WrappedViewLayout) {
|
||||||
|
addSubview(view)
|
||||||
|
|
||||||
|
view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
||||||
|
let edgeConstraints = EdgeConstraints(leadingConstraint: view.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
|
trailingConstraint: view.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||||
|
topConstraint: view.topAnchor.constraint(equalTo: topAnchor),
|
||||||
|
bottomConstraint: view.bottomAnchor.constraint(equalTo: bottomAnchor))
|
||||||
|
|
||||||
|
let centerConstraints = CenterConstraints(centerXConstraint: view.centerXAnchor.constraint(equalTo: centerXAnchor),
|
||||||
|
centerYConstraint: view.centerYAnchor.constraint(equalTo: centerYAnchor))
|
||||||
|
|
||||||
|
let sizeConstraints = SizeConstraints(widthConstraint: view.widthAnchor.constraint(equalToConstant: .zero),
|
||||||
|
heightConstraint: view.heightAnchor.constraint(equalToConstant: .zero))
|
||||||
|
|
||||||
|
let customViewConstraints = SubviewConstraints(edgeConstraints: edgeConstraints,
|
||||||
|
centerConstraints: centerConstraints,
|
||||||
|
sizeConstraints: sizeConstraints)
|
||||||
|
|
||||||
|
self.customViewConstraints = customViewConstraints
|
||||||
|
|
||||||
|
customViewConstraints.update(from: layout)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func configure(buttonStyle: BaseButtonStyle?,
|
||||||
|
forButton button: StatefulButton,
|
||||||
|
constraints: SubviewConstraints?) {
|
||||||
|
|
||||||
|
guard let buttonStyle else {
|
||||||
|
button.isHidden = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
button.isHidden = false
|
||||||
|
|
||||||
|
constraints?.update(from: buttonStyle.appearance.layout)
|
||||||
|
|
||||||
|
button.apply(style: buttonStyle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Appearance
|
||||||
|
|
||||||
|
public extension ModalHeaderView {
|
||||||
|
|
||||||
|
final class Appearance: BaseWrappedAppearance<DefaultWrappedLayout>, WrappedViewAppearance {
|
||||||
|
public static var defaultAppearance: Self {
|
||||||
|
Self(layout: DefaultWrappedLayout(insets: .horizontal(.zero).vertical(top: .zero),
|
||||||
|
size: .fixedHeight(44)))
|
||||||
|
}
|
||||||
|
|
||||||
|
public var contentViewState: ContentViewState
|
||||||
|
|
||||||
|
public init(layout: DefaultWrappedLayout = .defaultLayout,
|
||||||
|
background: UIViewBackground = UIViewColorBackground(color: .clear),
|
||||||
|
border: UIViewBorder = BaseUIViewBorder(),
|
||||||
|
shadow: UIViewShadow? = nil,
|
||||||
|
contentViewState: ContentViewState = .leadingButton(.init())) {
|
||||||
|
|
||||||
|
self.contentViewState = contentViewState
|
||||||
|
|
||||||
|
super.init(layout: layout,
|
||||||
|
background: background,
|
||||||
|
border: border,
|
||||||
|
shadow: shadow)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
//
|
||||||
|
// 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 UIKit
|
||||||
|
import PanModal
|
||||||
|
|
||||||
|
open class PassthroughDimmedView: DimmedView {
|
||||||
|
public weak var hitTestHandlerView: UIView?
|
||||||
|
|
||||||
|
public var isTransparent = false
|
||||||
|
|
||||||
|
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||||
|
guard let hitTestHandlerView else {
|
||||||
|
return super.hitTest(point, with: event)
|
||||||
|
}
|
||||||
|
|
||||||
|
return hitTestHandlerView.hitTest(point, with: event)
|
||||||
|
}
|
||||||
|
|
||||||
|
public override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
|
||||||
|
hitTestHandlerView == nil || super.point(inside: point, with: event)
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func onChange(dimState: DimmedView.DimState) {
|
||||||
|
super.onChange(dimState: dimState)
|
||||||
|
|
||||||
|
if isTransparent {
|
||||||
|
alpha = .zero
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
**/build/
|
||||||
|
**/nef/
|
||||||
|
LICENSE
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>en</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>launcher</string>
|
||||||
|
<key>CFBundleIconFile</key>
|
||||||
|
<string>AppIcon</string>
|
||||||
|
<key>CFBundleIconName</key>
|
||||||
|
<string>AppIcon</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>com.fortysevendeg.nef</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleSupportedPlatforms</key>
|
||||||
|
<array>
|
||||||
|
<string>MacOSX</string>
|
||||||
|
</array>
|
||||||
|
<key>LSApplicationCategoryType</key>
|
||||||
|
<string>public.app-category.developer-tools</string>
|
||||||
|
<key>LSMinimumSystemVersion</key>
|
||||||
|
<string>10.14</string>
|
||||||
|
<key>NSHumanReadableCopyright</key>
|
||||||
|
<string>Copyright © 2019 The nef Authors. All rights reserved.</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
## gitignore nef files
|
||||||
|
**/build/
|
||||||
|
**/nef/
|
||||||
|
LICENSE
|
||||||
|
|
||||||
|
## User data
|
||||||
|
**/xcuserdata/
|
||||||
|
podfile.lock
|
||||||
|
**.DS_Store
|
||||||
|
|
||||||
|
## Obj-C/Swift specific
|
||||||
|
*.hmap
|
||||||
|
*.ipa
|
||||||
|
*.dSYM.zip
|
||||||
|
*.dSYM
|
||||||
|
|
||||||
|
## CocoaPods
|
||||||
|
**Pods**
|
||||||
|
|
||||||
|
## Carthage
|
||||||
|
**Carthage**
|
||||||
|
|
||||||
|
## SPM
|
||||||
|
.build
|
||||||
|
.swiftpm
|
||||||
|
swiftpm
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
ENV["DEVELOPMENT_INSTALL"] = "true"
|
||||||
|
|
||||||
|
source 'https://git.svc.touchin.ru/TouchInstinct/Podspecs.git'
|
||||||
|
|
||||||
|
target 'TIBottomSheet' do
|
||||||
|
platform :ios, 12
|
||||||
|
use_frameworks!
|
||||||
|
|
||||||
|
pod 'TIUIElements', :path => '../../../../TIUIElements/TIUIElements.podspec'
|
||||||
|
pod 'TIUIKitCore', :path => '../../../../TIUIKitCore/TIUIKitCore.podspec'
|
||||||
|
pod 'TISwiftUtils', :path => '../../../../TISwiftUtils/TISwiftUtils.podspec'
|
||||||
|
pod 'TIBottomSheet', :path => '../../../../TIBottomSheet/TIBottomSheet.podspec'
|
||||||
|
pod 'TILogging', :path => '../../../../TILogging/TILogging.podspec'
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,110 @@
|
||||||
|
/*:
|
||||||
|
# TIBottomSheet
|
||||||
|
|
||||||
|
TIBottomSheet содержить базовую реализацию модального котроллера и немного видоизмененную библиотеку PanModal.
|
||||||
|
|
||||||
|
## Базовый контроллер
|
||||||
|
|
||||||
|
Для создания модального котроллера можно унаследоваться от `BaseModalViewController`. Данный клас принимает два generic типа: тип основного контента, тип контента футера.
|
||||||
|
*/
|
||||||
|
import TIBottomSheet
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class EmptyViewController: BaseModalViewController<UIView, UIView> { }
|
||||||
|
|
||||||
|
/*:
|
||||||
|
## Обертка вокруг существующего контроллера
|
||||||
|
|
||||||
|
Может быть такое, что из уже существующего контроллера нужно сделать модальное окно. С этим может помочь обертка `DefaultModalWrapperViewController`. Данный контроллер является наследником BaseModalViewController, что позволяет его настраивать так же, как и базовый модальный котроллер
|
||||||
|
*/
|
||||||
|
|
||||||
|
import TIUIKitCore
|
||||||
|
|
||||||
|
final class OldMassiveViewController: BaseInitializableViewController {
|
||||||
|
// some implementation
|
||||||
|
}
|
||||||
|
|
||||||
|
typealias ModalOldMassiveViewController = DefaultModalWrapperViewController<OldMassiveViewController>
|
||||||
|
|
||||||
|
class PresentingViewController: BaseInitializableViewController {
|
||||||
|
// some implementation
|
||||||
|
|
||||||
|
@objc private func onButtonTapped() {
|
||||||
|
presentPanModal(ModalOldMassiveViewController(contentViewController: OldMassiveViewController()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*:
|
||||||
|
## Контент модального контроллера
|
||||||
|
|
||||||
|
Модальный котроллер может содержать следующие элементы: `DragView`, `HeaderView`, `FooterView`. Каждый из них является опциональным и без дополнительных настроек не будет показываться.
|
||||||
|
|
||||||
|
DragView - небольшая view, за которую пользователь "держит" модальный контроллер
|
||||||
|
HeaderView - контейнер, содержащий в себе кнопки назад/закрыть или какие-то другие элементы управления
|
||||||
|
FooterView - view, располагающаяся внизу контроллера, поверх всего контента (модальный контроллер уже настроен так, чтобы при скролле в самый низ, футер не перекрывал последнюю ячейку)
|
||||||
|
|
||||||
|
Для настройки каждого у котроллера есть свойство `viewControllerAppearance`. Через него будет настраиваться весь контроллер. Однако стоит заметить, что котроллер не будет настраивать передаваимую вью, содержащую основной контент. Стандартно котроллер будет пытаться расположить контент так, чтобы он заполнил все пространство.
|
||||||
|
|
||||||
|
Вот пример настройки внешнего вида так, чтобы был видет dragView и headerView с левой кнопкой:
|
||||||
|
*/
|
||||||
|
|
||||||
|
import TIUIElements
|
||||||
|
|
||||||
|
let customViewController = BaseModalViewController<UIView, UIView>()
|
||||||
|
customViewController.viewControllerAppearance = BaseModalViewController.DefaultAppearance.make {
|
||||||
|
$0.dragViewState = .presented(.defaultAppearance)
|
||||||
|
$0.headerViewState = .presented(.make {
|
||||||
|
$0.layout.size = .fixedHeight(52)
|
||||||
|
$0.backgroundColor = .white
|
||||||
|
$0.contentViewState = .leadingButton(.init(titles: [.normal: "Close"],
|
||||||
|
appearance: .init(stateAppearances: [
|
||||||
|
.normal: .init(background: UIViewColorBackground(color: .blue))
|
||||||
|
])))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/*:
|
||||||
|
## "Якори" контроллера
|
||||||
|
|
||||||
|
Раньше для настройки высоты контроллера необходимо было пользоваться свойствами `longFormHeight`, `shortFormHeight`. В базовом контроллере можно лишь передать список точек на которых контроллер должен будет задержаться:
|
||||||
|
*/
|
||||||
|
|
||||||
|
let detentsViewController = BaseModalViewController<UIView, UIView>()
|
||||||
|
detentsViewController.viewControllerAppearance.presentationDetents = [.headerOnly, .height(300), .maxHeight]
|
||||||
|
|
||||||
|
/*:
|
||||||
|
- headerOnly будет сам пытаться вычеслить высоту хедера и dragView, показывая только их
|
||||||
|
- height(_) будет показывать контроллер на переданной высоте
|
||||||
|
- maxHeight - вся высота экрана (до safeArea)
|
||||||
|
|
||||||
|
В данный массив не рекомендуется передавать больше 3 значений, т.к. модальное окно все равно сможет занять только 3 положения на экране.
|
||||||
|
|
||||||
|
## DimmedView и PassthroughDimmedView
|
||||||
|
|
||||||
|
Для контроля `DimmedView` (затемняющей view) есть отдельное свойство `dimmedView`. Эти классы позволяют настраивать поведение при тапе в затемнённую область и кастомизировать затемнение под ваши нужды.
|
||||||
|
*/
|
||||||
|
|
||||||
|
let shadowViewController = BaseModalViewController<UIView, UIView>()
|
||||||
|
let dimmedView = PassthroughDimmedView()
|
||||||
|
dimmedView.hitTestHandlerView = shadowViewController.view
|
||||||
|
dimmedView.configureUIView(appearance: UIView.DefaultAppearance(shadow: UIViewShadow(radius: 8,
|
||||||
|
color: .black,
|
||||||
|
opacity: 0.3)))
|
||||||
|
|
||||||
|
shadowViewController.dimmedView = dimmedView
|
||||||
|
|
||||||
|
/*:
|
||||||
|
## Контроль закрытия
|
||||||
|
|
||||||
|
`PanModalPresentable` не умеет в настройку закрытия контроллера, делая это самостоятельно через `dismiss(animated:completion:)`. Теперь можно настроить закрытие самостоятельно через свойства: `onTapToDismiss` и `onDragToDismiss`.
|
||||||
|
|
||||||
|
# Взаимодействие с PanModal
|
||||||
|
|
||||||
|
Если нет необходимости или возможности использовать `BaseModalViewController`, вы все так же можете пользоваться протоколом `PanModalRepresentable`. Вот список изменений протокола:
|
||||||
|
|
||||||
|
- Открытие/закрытие модального окна теперь можно настроить с помощью свойств `onTapToDismiss` и `onDragToDismiss`
|
||||||
|
- Можно настроить промежуточное состояние модального окна с `mediumFormHeight`
|
||||||
|
- `DimmedView` открыт для наследования и может создаваться в `dimmedView` у
|
||||||
|
|
||||||
|
> Для `BaseModalViewController` все свойства из `PanModalPresentable` все также работают, т.е. вы можете их переопределять, добавлять и изменять по необходимости.
|
||||||
|
*/
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
public protocol NefPlaygroundLiveViewable {}
|
||||||
|
extension UIView: NefPlaygroundLiveViewable {}
|
||||||
|
extension UIViewController: NefPlaygroundLiveViewable {}
|
||||||
|
|
||||||
|
#if NOT_IN_PLAYGROUND
|
||||||
|
public enum Nef {
|
||||||
|
public enum Playground {
|
||||||
|
public static func liveView(_ view: NefPlaygroundLiveViewable) {}
|
||||||
|
public static func needsIndefiniteExecution(_ state: Bool) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#else
|
||||||
|
import PlaygroundSupport
|
||||||
|
|
||||||
|
public enum Nef {
|
||||||
|
public enum Playground {
|
||||||
|
public static func liveView(_ view: NefPlaygroundLiveViewable) {
|
||||||
|
PlaygroundPage.current.liveView = (view as! PlaygroundLiveViewable)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func needsIndefiniteExecution(_ state: Bool) {
|
||||||
|
PlaygroundPage.current.needsIndefiniteExecution = state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
import Foundation
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
public extension Nef {
|
||||||
|
|
||||||
|
static func run<T: XCTestCase>(testCase class: T.Type) {
|
||||||
|
startTestObserver()
|
||||||
|
T.defaultTestSuite.run()
|
||||||
|
}
|
||||||
|
|
||||||
|
static private func startTestObserver() {
|
||||||
|
_ = testObserverInstalled
|
||||||
|
}
|
||||||
|
|
||||||
|
static private var testObserverInstalled = { () -> NefTestFailObserver in
|
||||||
|
let testObserver = NefTestFailObserver()
|
||||||
|
XCTestObservationCenter.shared.addTestObserver(testObserver)
|
||||||
|
return testObserver
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: enrich the output for XCTest
|
||||||
|
fileprivate class NefTestFailObserver: NSObject, XCTestObservation {
|
||||||
|
|
||||||
|
private var numberOfFailedTests = 0
|
||||||
|
|
||||||
|
func testSuiteWillStart(_ testSuite: XCTestSuite) {
|
||||||
|
numberOfFailedTests = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSuiteDidFinish(_ testSuite: XCTestSuite) {
|
||||||
|
if numberOfFailedTests > 0 {
|
||||||
|
print("💢 Test Suite '\(testSuite.name)' finished with \(numberOfFailedTests) failed \(numberOfFailedTests > 1 ? "tests" : "test").")
|
||||||
|
} else {
|
||||||
|
print("🔅 Test Suite '\(testSuite.name)' finished successfully.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testCase(_ testCase: XCTestCase,
|
||||||
|
didFailWithDescription description: String,
|
||||||
|
inFile filePath: String?,
|
||||||
|
atLine lineNumber: Int) {
|
||||||
|
|
||||||
|
numberOfFailedTests += 1
|
||||||
|
print("❗️Test Fail '\(testCase.name)':\(UInt(lineNumber)): \(description.description)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<playground version='6.0' target-platform='ios' display-mode='raw' buildActiveScheme='true'/>
|
||||||
|
|
@ -0,0 +1,396 @@
|
||||||
|
// !$*UTF8*$!
|
||||||
|
{
|
||||||
|
archiveVersion = 1;
|
||||||
|
classes = {
|
||||||
|
};
|
||||||
|
objectVersion = 50;
|
||||||
|
objects = {
|
||||||
|
|
||||||
|
/* Begin PBXBuildFile section */
|
||||||
|
83C08983988F66570478C40D /* Pods_TIBottomSheet.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8EF3488B86B483233C2CC631 /* Pods_TIBottomSheet.framework */; };
|
||||||
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
|
/* Begin PBXFileReference section */
|
||||||
|
7B6955D74676A5427AC42234 /* Pods-TIBottomSheet.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TIBottomSheet.debug.xcconfig"; path = "Target Support Files/Pods-TIBottomSheet/Pods-TIBottomSheet.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
8BACBE8322576CAD00266845 /* TIBottomSheet.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = TIBottomSheet.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
8BACBE8622576CAD00266845 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
|
8EF3488B86B483233C2CC631 /* Pods_TIBottomSheet.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_TIBottomSheet.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
AA57D8210790AD14BCC54A7E /* Pods-TIBottomSheet.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TIBottomSheet.release.xcconfig"; path = "Target Support Files/Pods-TIBottomSheet/Pods-TIBottomSheet.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
|
8BACBE8022576CAD00266845 /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
83C08983988F66570478C40D /* Pods_TIBottomSheet.framework in Frameworks */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXGroup section */
|
||||||
|
11F06D2789C6CF40767861CF /* Frameworks */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
8EF3488B86B483233C2CC631 /* Pods_TIBottomSheet.framework */,
|
||||||
|
);
|
||||||
|
name = Frameworks;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
1F7782E3A7AD7291B7C09F56 /* Pods */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
7B6955D74676A5427AC42234 /* Pods-TIBottomSheet.debug.xcconfig */,
|
||||||
|
AA57D8210790AD14BCC54A7E /* Pods-TIBottomSheet.release.xcconfig */,
|
||||||
|
);
|
||||||
|
name = Pods;
|
||||||
|
path = Pods;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
8B39A26221D40F8700DE2643 = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
8BACBE8422576CAD00266845 /* TIBottomSheet */,
|
||||||
|
8B39A26C21D40F8700DE2643 /* Products */,
|
||||||
|
1F7782E3A7AD7291B7C09F56 /* Pods */,
|
||||||
|
11F06D2789C6CF40767861CF /* Frameworks */,
|
||||||
|
);
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
8B39A26C21D40F8700DE2643 /* Products */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
8BACBE8322576CAD00266845 /* TIBottomSheet.framework */,
|
||||||
|
);
|
||||||
|
name = Products;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
8BACBE8422576CAD00266845 /* TIBottomSheet */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
8BACBE8622576CAD00266845 /* Info.plist */,
|
||||||
|
);
|
||||||
|
path = TIBottomSheet;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
/* End PBXGroup section */
|
||||||
|
|
||||||
|
/* Begin PBXHeadersBuildPhase section */
|
||||||
|
8BACBE7E22576CAD00266845 /* Headers */ = {
|
||||||
|
isa = PBXHeadersBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXHeadersBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXNativeTarget section */
|
||||||
|
8BACBE8222576CAD00266845 /* TIBottomSheet */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = 8BACBE8A22576CAD00266845 /* Build configuration list for PBXNativeTarget "TIBottomSheet" */;
|
||||||
|
buildPhases = (
|
||||||
|
4E98D4C60DCD00EB801E579E /* [CP] Check Pods Manifest.lock */,
|
||||||
|
8BACBE7E22576CAD00266845 /* Headers */,
|
||||||
|
8BACBE7F22576CAD00266845 /* Sources */,
|
||||||
|
8BACBE8022576CAD00266845 /* Frameworks */,
|
||||||
|
8BACBE8122576CAD00266845 /* Resources */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
);
|
||||||
|
name = TIBottomSheet;
|
||||||
|
productName = TIBottomSheet2;
|
||||||
|
productReference = 8BACBE8322576CAD00266845 /* TIBottomSheet.framework */;
|
||||||
|
productType = "com.apple.product-type.framework";
|
||||||
|
};
|
||||||
|
/* End PBXNativeTarget section */
|
||||||
|
|
||||||
|
/* Begin PBXProject section */
|
||||||
|
8B39A26321D40F8700DE2643 /* Project object */ = {
|
||||||
|
isa = PBXProject;
|
||||||
|
attributes = {
|
||||||
|
LastSwiftUpdateCheck = 1010;
|
||||||
|
LastUpgradeCheck = 1200;
|
||||||
|
ORGANIZATIONNAME = "47 Degrees";
|
||||||
|
TargetAttributes = {
|
||||||
|
8BACBE8222576CAD00266845 = {
|
||||||
|
CreatedOnToolsVersion = 10.1;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
buildConfigurationList = 8B39A26621D40F8700DE2643 /* Build configuration list for PBXProject "TIBottomSheet" */;
|
||||||
|
compatibilityVersion = "Xcode 9.3";
|
||||||
|
developmentRegion = en;
|
||||||
|
hasScannedForEncodings = 0;
|
||||||
|
knownRegions = (
|
||||||
|
en,
|
||||||
|
Base,
|
||||||
|
);
|
||||||
|
mainGroup = 8B39A26221D40F8700DE2643;
|
||||||
|
productRefGroup = 8B39A26C21D40F8700DE2643 /* Products */;
|
||||||
|
projectDirPath = "";
|
||||||
|
projectRoot = "";
|
||||||
|
targets = (
|
||||||
|
8BACBE8222576CAD00266845 /* TIBottomSheet */,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
/* End PBXProject section */
|
||||||
|
|
||||||
|
/* Begin PBXResourcesBuildPhase section */
|
||||||
|
8BACBE8122576CAD00266845 /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXResourcesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXShellScriptBuildPhase section */
|
||||||
|
4E98D4C60DCD00EB801E579E /* [CP] Check Pods Manifest.lock */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||||
|
"${PODS_ROOT}/Manifest.lock",
|
||||||
|
);
|
||||||
|
name = "[CP] Check Pods Manifest.lock";
|
||||||
|
outputFileListPaths = (
|
||||||
|
);
|
||||||
|
outputPaths = (
|
||||||
|
"$(DERIVED_FILE_DIR)/Pods-TIBottomSheet-checkManifestLockResult.txt",
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||||
|
showEnvVarsInLog = 0;
|
||||||
|
};
|
||||||
|
/* End PBXShellScriptBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
|
8BACBE7F22576CAD00266845 /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXSourcesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin XCBuildConfiguration section */
|
||||||
|
8B39A27721D40F8800DE2643 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||||
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CLANG_ENABLE_OBJC_ARC = YES;
|
||||||
|
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||||
|
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||||
|
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_COMMA = YES;
|
||||||
|
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||||
|
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||||
|
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||||
|
CLANG_WARN_EMPTY_BODY = YES;
|
||||||
|
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||||
|
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||||
|
CLANG_WARN_INT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||||
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||||
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||||
|
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||||
|
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||||
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
|
CODE_SIGN_IDENTITY = "-";
|
||||||
|
COPY_PHASE_STRIP = NO;
|
||||||
|
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||||
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
|
ENABLE_TESTABILITY = YES;
|
||||||
|
ENABLE_TESTING_SEARCH_PATHS = YES;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
|
GCC_DYNAMIC_NO_PIC = NO;
|
||||||
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
|
GCC_OPTIMIZATION_LEVEL = 0;
|
||||||
|
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||||
|
"DEBUG=1",
|
||||||
|
"$(inherited)",
|
||||||
|
);
|
||||||
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||||
|
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||||
|
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||||
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||||
|
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||||
|
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||||
|
MTL_FAST_MATH = YES;
|
||||||
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||||
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
8B39A27821D40F8800DE2643 /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||||
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CLANG_ENABLE_OBJC_ARC = YES;
|
||||||
|
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||||
|
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||||
|
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_COMMA = YES;
|
||||||
|
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||||
|
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||||
|
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||||
|
CLANG_WARN_EMPTY_BODY = YES;
|
||||||
|
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||||
|
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||||
|
CLANG_WARN_INT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||||
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||||
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||||
|
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||||
|
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||||
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
|
CODE_SIGN_IDENTITY = "-";
|
||||||
|
COPY_PHASE_STRIP = NO;
|
||||||
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
|
ENABLE_NS_ASSERTIONS = NO;
|
||||||
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
|
ENABLE_TESTING_SEARCH_PATHS = YES;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||||
|
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||||
|
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||||
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||||
|
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||||
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
|
MTL_FAST_MATH = YES;
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
|
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
8BACBE8822576CAD00266845 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 7B6955D74676A5427AC42234 /* Pods-TIBottomSheet.debug.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
CODE_SIGN_IDENTITY = "";
|
||||||
|
CODE_SIGN_STYLE = Manual;
|
||||||
|
CURRENT_TIBottomSheet_VERSION = 1;
|
||||||
|
DEFINES_MODULE = YES;
|
||||||
|
DEVELOPMENT_TEAM = "";
|
||||||
|
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||||
|
DYLIB_CURRENT_VERSION = 1;
|
||||||
|
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||||
|
INFOPLIST_FILE = "$(SRCROOT)/TIBottomSheet/Info.plist";
|
||||||
|
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 12.1;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
"@loader_path/Frameworks",
|
||||||
|
);
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.47deg.ios.TIBottomSheet;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||||
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
SKIP_INSTALL = YES;
|
||||||
|
SWIFT_VERSION = 5;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
|
VERSION_INFO_PREFIX = "";
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
8BACBE8922576CAD00266845 /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = AA57D8210790AD14BCC54A7E /* Pods-TIBottomSheet.release.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
CODE_SIGN_IDENTITY = "";
|
||||||
|
CODE_SIGN_STYLE = Manual;
|
||||||
|
CURRENT_TIBottomSheet_VERSION = 1;
|
||||||
|
DEFINES_MODULE = YES;
|
||||||
|
DEVELOPMENT_TEAM = "";
|
||||||
|
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||||
|
DYLIB_CURRENT_VERSION = 1;
|
||||||
|
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||||
|
INFOPLIST_FILE = "$(SRCROOT)/TIBottomSheet/Info.plist";
|
||||||
|
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 12.1;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
"@loader_path/Frameworks",
|
||||||
|
);
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.47deg.ios.TIBottomSheet;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||||
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
SKIP_INSTALL = YES;
|
||||||
|
SWIFT_VERSION = 5;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
VALIDATE_PRODUCT = YES;
|
||||||
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
|
VERSION_INFO_PREFIX = "";
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
/* End XCBuildConfiguration section */
|
||||||
|
|
||||||
|
/* Begin XCConfigurationList section */
|
||||||
|
8B39A26621D40F8700DE2643 /* Build configuration list for PBXProject "TIBottomSheet" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
8B39A27721D40F8800DE2643 /* Debug */,
|
||||||
|
8B39A27821D40F8800DE2643 /* Release */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
8BACBE8A22576CAD00266845 /* Build configuration list for PBXNativeTarget "TIBottomSheet" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
8BACBE8822576CAD00266845 /* Debug */,
|
||||||
|
8BACBE8922576CAD00266845 /* Release */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
/* End XCConfigurationList section */
|
||||||
|
};
|
||||||
|
rootObject = 8B39A26321D40F8700DE2643 /* Project object */;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Workspace
|
||||||
|
version = "1.0">
|
||||||
|
<FileRef
|
||||||
|
location = "self:TIBottomSheet.xcodeproj">
|
||||||
|
</FileRef>
|
||||||
|
</Workspace>
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "1200"
|
||||||
|
version = "1.3">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "8BACBE8222576CAD00266845"
|
||||||
|
BuildableName = "TIBottomSheet.framework"
|
||||||
|
BlueprintName = "TIBottomSheet"
|
||||||
|
ReferencedContainer = "container:TIBottomSheet.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||||
|
<Testables>
|
||||||
|
</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 = "8BACBE8222576CAD00266845"
|
||||||
|
BuildableName = "TIBottomSheet.framework"
|
||||||
|
BlueprintName = "TIBottomSheet"
|
||||||
|
ReferencedContainer = "container:TIBottomSheet.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</MacroExpansion>
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES">
|
||||||
|
<MacroExpansion>
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "8BACBE8222576CAD00266845"
|
||||||
|
BuildableName = "TIBottomSheet.framework"
|
||||||
|
BlueprintName = "TIBottomSheet"
|
||||||
|
ReferencedContainer = "container:TIBottomSheet.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</MacroExpansion>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
||||||
11
TIBottomSheet/TIBottomSheet.app/Contents/MacOS/TIBottomSheet.xcworkspace/contents.xcworkspacedata
generated
Normal file
11
TIBottomSheet/TIBottomSheet.app/Contents/MacOS/TIBottomSheet.xcworkspace/contents.xcworkspacedata
generated
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Workspace
|
||||||
|
version = "1.0">
|
||||||
|
<FileRef location = "group:TIBottomSheet.playground"></FileRef>
|
||||||
|
<FileRef
|
||||||
|
location = "group:TIBottomSheet.xcodeproj">
|
||||||
|
</FileRef>
|
||||||
|
<FileRef
|
||||||
|
location = "group:Pods/Pods.xcodeproj">
|
||||||
|
</FileRef>
|
||||||
|
</Workspace>
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>$(PRODUCT_NAME)</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>FMWK</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>1.0</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>1.0</string>
|
||||||
|
<key>NSHumanReadableCopyright</key>
|
||||||
|
<string>Copyright © 2019. The nef authors.</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
workspace="TIBottomSheet.xcworkspace"
|
||||||
|
workspacePath=$(echo "$0" | rev | cut -f2- -d '/' | rev)
|
||||||
|
|
||||||
|
open "`pwd`/$workspacePath/$workspace"
|
||||||
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1 @@
|
||||||
|
TIBottomSheet.app/Contents/MacOS/TIBottomSheet.playground
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
Pod::Spec.new do |s|
|
||||||
|
s.name = 'TIBottomSheet'
|
||||||
|
s.version = '1.56.0'
|
||||||
|
s.summary = 'Base models for creating bottom sheet view controllers'
|
||||||
|
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
|
||||||
|
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
||||||
|
s.author = { 'castlele' => 'nikita.semenov@touchin.ru',
|
||||||
|
'petropavel13' => 'ivan.smolin@touchin.ru'}
|
||||||
|
s.source = { :git => 'https://git.svc.touchin.ru/TouchInstinct/LeadKit.git', :tag => s.version.to_s }
|
||||||
|
|
||||||
|
s.ios.deployment_target = '12.0'
|
||||||
|
s.swift_versions = ['5.7']
|
||||||
|
|
||||||
|
sources = 'Sources/**/*'
|
||||||
|
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
|
||||||
|
|
||||||
|
s.dependency 'TIUIElements', s.version.to_s
|
||||||
|
s.dependency 'TIUIKitCore', s.version.to_s
|
||||||
|
s.dependency 'TISwiftUtils', s.version.to_s
|
||||||
|
s.dependency 'PanModal', '~> 1.3.0'
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
ENV["DEVELOPMENT_INSTALL"] = "true"
|
||||||
|
|
||||||
|
target 'TICoreGraphicsUtils' do
|
||||||
|
platform :ios, 11.0
|
||||||
|
use_frameworks!
|
||||||
|
|
||||||
|
pod 'TICoreGraphicsUtils', :path => '../../../../TICoreGraphicsUtils/TICoreGraphicsUtils.podspec'
|
||||||
|
end
|
||||||
|
|
@ -22,11 +22,9 @@
|
||||||
|
|
||||||
import CoreGraphics.CGGeometry
|
import CoreGraphics.CGGeometry
|
||||||
|
|
||||||
public typealias CGContextSize = (width: Int, height: Int)
|
|
||||||
|
|
||||||
public extension CGSize {
|
public extension CGSize {
|
||||||
var ceiledContextSize: CGContextSize {
|
var ceiledContextSize: CGSize {
|
||||||
(width: Int(ceil(width)), height: Int(ceil(height)))
|
CGSize(width: ceil(width), height: ceil(height))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -23,19 +23,19 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
public struct BorderDrawingOperation: DrawingOperation {
|
public struct BorderDrawingOperation: DrawingOperation {
|
||||||
public var frameableContentSize: CGSize
|
public var frameableContentRect: CGRect
|
||||||
public var border: CGFloat
|
public var border: CGFloat
|
||||||
public var color: CGColor
|
public var color: CGColor
|
||||||
public var radius: CGFloat
|
public var radius: CGFloat
|
||||||
public var exteriorBorder: Bool
|
public var exteriorBorder: Bool
|
||||||
|
|
||||||
public init(frameableContentSize: CGSize,
|
public init(frameableContentRect: CGRect,
|
||||||
border: CGFloat,
|
border: CGFloat,
|
||||||
color: CGColor,
|
color: CGColor,
|
||||||
radius: CGFloat,
|
radius: CGFloat,
|
||||||
exteriorBorder: Bool) {
|
exteriorBorder: Bool) {
|
||||||
|
|
||||||
self.frameableContentSize = frameableContentSize
|
self.frameableContentRect = frameableContentRect
|
||||||
self.border = border
|
self.border = border
|
||||||
self.color = color
|
self.color = color
|
||||||
self.radius = radius
|
self.radius = radius
|
||||||
|
|
@ -47,10 +47,10 @@ public struct BorderDrawingOperation: DrawingOperation {
|
||||||
public func affectedArea(in context: CGContext?) -> CGRect {
|
public func affectedArea(in context: CGContext?) -> CGRect {
|
||||||
let margin = exteriorBorder ? border : 0
|
let margin = exteriorBorder ? border : 0
|
||||||
|
|
||||||
let width = frameableContentSize.width + margin * 2
|
let width = frameableContentRect.width + margin * 2
|
||||||
let height = frameableContentSize.height + margin * 2
|
let height = frameableContentRect.height + margin * 2
|
||||||
|
|
||||||
return CGRect(origin: .zero,
|
return CGRect(origin: frameableContentRect.origin,
|
||||||
size: CGSize(width: width, height: height))
|
size: CGSize(width: width, height: height))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -59,7 +59,8 @@ public struct BorderDrawingOperation: DrawingOperation {
|
||||||
|
|
||||||
let ctxSize = drawArea.size.ceiledContextSize
|
let ctxSize = drawArea.size.ceiledContextSize
|
||||||
|
|
||||||
let ctxRect = CGRect(origin: .zero, size: CGSize(width: ctxSize.width, height: ctxSize.height))
|
let ctxRect = CGRect(origin: frameableContentRect.origin,
|
||||||
|
size: CGSize(width: ctxSize.width, height: ctxSize.height))
|
||||||
|
|
||||||
let widthDiff = CGFloat(ctxSize.width) - drawArea.width // difference between context width and real width
|
let widthDiff = CGFloat(ctxSize.width) - drawArea.width // difference between context width and real width
|
||||||
let heightDiff = CGFloat(ctxSize.height) - drawArea.height // difference between context height and real height
|
let heightDiff = CGFloat(ctxSize.height) - drawArea.height // difference between context height and real height
|
||||||
|
|
@ -45,6 +45,6 @@ public struct CALayerDrawingOperation: DrawingOperation {
|
||||||
|
|
||||||
context.concatenate(offsetTransform)
|
context.concatenate(offsetTransform)
|
||||||
layer.render(in: context)
|
layer.render(in: context)
|
||||||
offsetTransform.concatenating(offsetTransform.inverted())
|
context.concatenate(offsetTransform.inverted())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -55,8 +55,10 @@ public struct SolidFillDrawingOperation: DrawingOperation {
|
||||||
switch shape {
|
switch shape {
|
||||||
case let .rect(rect):
|
case let .rect(rect):
|
||||||
return rect
|
return rect
|
||||||
|
|
||||||
case let .ellipse(rect):
|
case let .ellipse(rect):
|
||||||
return rect
|
return rect
|
||||||
|
|
||||||
case let .path(path):
|
case let .path(path):
|
||||||
return path.boundingBox
|
return path.boundingBox
|
||||||
}
|
}
|
||||||
|
|
@ -68,8 +70,10 @@ public struct SolidFillDrawingOperation: DrawingOperation {
|
||||||
switch shape {
|
switch shape {
|
||||||
case let .rect(rect):
|
case let .rect(rect):
|
||||||
context.fill(rect)
|
context.fill(rect)
|
||||||
|
|
||||||
case let .ellipse(rect):
|
case let .ellipse(rect):
|
||||||
context.fillEllipse(in: rect)
|
context.fillEllipse(in: rect)
|
||||||
|
|
||||||
case let .path(path):
|
case let .path(path):
|
||||||
context.addPath(path)
|
context.addPath(path)
|
||||||
context.fillPath()
|
context.fillPath()
|
||||||
|
|
@ -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 CoreGraphics
|
||||||
|
|
||||||
|
public struct TemplateDrawingOperation: OrientationAwareDrawingOperation {
|
||||||
|
|
||||||
|
public var image: CGImage
|
||||||
|
public var imageSize: CGSize
|
||||||
|
public var color: CGColor
|
||||||
|
public var flipHorizontallyDuringDrawing: Bool
|
||||||
|
|
||||||
|
public init(image: CGImage,
|
||||||
|
imageSize: CGSize,
|
||||||
|
color: CGColor,
|
||||||
|
flipHorizontallyDuringDrawing: Bool = true) {
|
||||||
|
|
||||||
|
self.image = image
|
||||||
|
self.imageSize = imageSize
|
||||||
|
self.color = color
|
||||||
|
self.flipHorizontallyDuringDrawing = flipHorizontallyDuringDrawing
|
||||||
|
}
|
||||||
|
|
||||||
|
public func affectedArea(in context: CGContext?) -> CGRect {
|
||||||
|
CGRect(origin: .zero, size: imageSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func apply(in context: CGContext) {
|
||||||
|
let imageRect = CGRect(origin: .zero, size: imageSize)
|
||||||
|
|
||||||
|
apply(in: context) {
|
||||||
|
$0.setFillColor(color)
|
||||||
|
$0.clip(to: imageRect, mask: image)
|
||||||
|
$0.fill(imageRect)
|
||||||
|
$0.setBlendMode(.multiply)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -26,48 +26,45 @@ import CoreText
|
||||||
|
|
||||||
public struct TextDrawingOperation: OrientationAwareDrawingOperation {
|
public struct TextDrawingOperation: OrientationAwareDrawingOperation {
|
||||||
public var text: String
|
public var text: String
|
||||||
public var font: CTFont
|
public var textAttributes: [NSAttributedString.Key: Any]
|
||||||
public var textColor: CGColor
|
|
||||||
public var flipHorizontallyDuringDrawing: Bool
|
public var flipHorizontallyDuringDrawing: Bool
|
||||||
public var desiredOffset: CGPoint
|
public var desiredOffset: CGPoint
|
||||||
|
|
||||||
private var line: CTLine {
|
public var attributedString: NSAttributedString {
|
||||||
let textAttributes: [NSAttributedString.Key : Any] = [
|
NSAttributedString(string: text, attributes: textAttributes)
|
||||||
.font: font,
|
}
|
||||||
.foregroundColor: textColor
|
|
||||||
]
|
|
||||||
|
|
||||||
let attributedString = NSAttributedString(string: text, attributes: textAttributes)
|
public var line: CTLine {
|
||||||
|
CTLineCreateWithAttributedString(attributedString)
|
||||||
|
}
|
||||||
|
|
||||||
return CTLineCreateWithAttributedString(attributedString)
|
public var desiredContextSize: CGSize {
|
||||||
|
let imageBounds = CTLineGetImageBounds(line, nil)
|
||||||
|
|
||||||
|
return CGSize(width: max(imageBounds.width, imageBounds.maxX) + desiredOffset.x,
|
||||||
|
height: max(imageBounds.height, imageBounds.maxY) + desiredOffset.y)
|
||||||
}
|
}
|
||||||
|
|
||||||
public init(text: String,
|
public init(text: String,
|
||||||
font: CTFont,
|
textAttributes: [NSAttributedString.Key: Any],
|
||||||
textColor: CGColor,
|
|
||||||
flipHorizontallyDuringDrawing: Bool = true,
|
flipHorizontallyDuringDrawing: Bool = true,
|
||||||
desiredOffset: CGPoint = .zero) {
|
desiredOffset: CGPoint = .zero) {
|
||||||
|
|
||||||
self.text = text
|
self.text = text
|
||||||
self.font = font
|
self.textAttributes = textAttributes
|
||||||
self.textColor = textColor
|
|
||||||
self.flipHorizontallyDuringDrawing = flipHorizontallyDuringDrawing
|
self.flipHorizontallyDuringDrawing = flipHorizontallyDuringDrawing
|
||||||
self.desiredOffset = desiredOffset
|
self.desiredOffset = desiredOffset
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - DrawingOperation
|
||||||
|
|
||||||
public func affectedArea(in context: CGContext? = nil) -> CGRect {
|
public func affectedArea(in context: CGContext? = nil) -> CGRect {
|
||||||
CGRect(origin: desiredOffset, size: CTLineGetImageBounds(line, context).size)
|
CTLineGetImageBounds(line, context).offset(by: desiredOffset)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func apply(in context: CGContext) {
|
public func apply(in context: CGContext) {
|
||||||
apply(in: context) {
|
apply(in: context) {
|
||||||
let originForDrawing = offsetForDrawing(CTLineGetImageBounds(line, context).origin)
|
$0.textPosition = offsetForDrawing(affectedArea(in: context).origin)
|
||||||
let desiredOffsetForDrawing = offsetForDrawing(desiredOffset)
|
|
||||||
let textPosition = CGPoint(x: desiredOffsetForDrawing.x - originForDrawing.x,
|
|
||||||
y: desiredOffsetForDrawing.y - originForDrawing.y)
|
|
||||||
|
|
||||||
$0.textPosition = textPosition
|
|
||||||
|
|
||||||
CTLineDraw(line, $0)
|
CTLineDraw(line, $0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
# gitignore nef files
|
||||||
|
**/build/
|
||||||
|
**/nef/
|
||||||
|
LICENSE
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>en</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>launcher</string>
|
||||||
|
<key>CFBundleIconFile</key>
|
||||||
|
<string>AppIcon</string>
|
||||||
|
<key>CFBundleIconName</key>
|
||||||
|
<string>AppIcon</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>com.fortysevendeg.nef</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleSupportedPlatforms</key>
|
||||||
|
<array>
|
||||||
|
<string>MacOSX</string>
|
||||||
|
</array>
|
||||||
|
<key>LSApplicationCategoryType</key>
|
||||||
|
<string>public.app-category.developer-tools</string>
|
||||||
|
<key>LSMinimumSystemVersion</key>
|
||||||
|
<string>10.14</string>
|
||||||
|
<key>NSHumanReadableCopyright</key>
|
||||||
|
<string>Copyright © 2019 The nef Authors. All rights reserved.</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
## gitignore nef files
|
||||||
|
**/build/
|
||||||
|
**/nef/
|
||||||
|
LICENSE
|
||||||
|
|
||||||
|
## User data
|
||||||
|
**/xcuserdata/
|
||||||
|
podfile.lock
|
||||||
|
**.DS_Store
|
||||||
|
|
||||||
|
## Obj-C/Swift specific
|
||||||
|
*.hmap
|
||||||
|
*.ipa
|
||||||
|
*.dSYM.zip
|
||||||
|
*.dSYM
|
||||||
|
|
||||||
|
## CocoaPods
|
||||||
|
**Pods**
|
||||||
|
|
||||||
|
## Carthage
|
||||||
|
**Carthage**
|
||||||
|
|
||||||
|
## SPM
|
||||||
|
.build
|
||||||
|
.swiftpm
|
||||||
|
swiftpm
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
ENV["DEVELOPMENT_INSTALL"] = "true"
|
||||||
|
|
||||||
|
target 'TICoreGraphicsUtils' do
|
||||||
|
platform :ios, 12.0
|
||||||
|
use_frameworks!
|
||||||
|
|
||||||
|
pod 'TICoreGraphicsUtils', :path => '../../../../TICoreGraphicsUtils/TICoreGraphicsUtils.podspec'
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,128 @@
|
||||||
|
/*:
|
||||||
|
# `DrawingOperation` - протокол для инкапсулирования низкоуровневых вызовов отрисовки CoreGraphicss.
|
||||||
|
|
||||||
|
Позволяет:
|
||||||
|
|
||||||
|
- сгруппировать низкоуровневые операции CoreGraphics в более высокоуровневые
|
||||||
|
- использовать композицию из высокоуровневых операций
|
||||||
|
- получить размер изображения необходимый для создания контекста
|
||||||
|
|
||||||
|
## Базовые операции
|
||||||
|
|
||||||
|
"Из коробки" доступны самые часто испольуемые операции.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*:
|
||||||
|
### `SolidFillDrawingOperation`
|
||||||
|
|
||||||
|
Операция для заполнения области рисования выбранным цветом и выбранной формой.
|
||||||
|
Например, можно нарисовать прямоугольник или круг на существующей области рисования
|
||||||
|
или просто создать изображение с однотонный фоном.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import TICoreGraphicsUtils
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
let solidFillSize = CGSize(width: 200, height: 200)
|
||||||
|
|
||||||
|
let renderer = UIGraphicsImageRenderer(size: solidFillSize)
|
||||||
|
let solidFillDrawingOperation = SolidFillDrawingOperation(color: UIColor.purple.cgColor,
|
||||||
|
rect: CGRect(origin: .zero,
|
||||||
|
size: solidFillSize))
|
||||||
|
|
||||||
|
let solidFillImage = renderer.image {
|
||||||
|
solidFillDrawingOperation.apply(in: $0.cgContext)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*:
|
||||||
|
### `BorderDrawingOperation`
|
||||||
|
|
||||||
|
Операция создаёт рамку определённой формы и размера.
|
||||||
|
*/
|
||||||
|
|
||||||
|
let borderDrawingOperation = BorderDrawingOperation(frameableContentRect: CGRect(origin: .zero,
|
||||||
|
size: solidFillSize),
|
||||||
|
border: 2,
|
||||||
|
color: UIColor.red.cgColor,
|
||||||
|
radius: 4,
|
||||||
|
exteriorBorder: false)
|
||||||
|
|
||||||
|
let borderImage = renderer.image {
|
||||||
|
borderDrawingOperation.apply(in: $0.cgContext)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*:
|
||||||
|
### `CALayerDrawingOperation`
|
||||||
|
|
||||||
|
Операция отрисовывает содержимое CALayer в изображение. Обычно используется для снапшота UIView.
|
||||||
|
*/
|
||||||
|
|
||||||
|
let button = UIButton(type: .custom)
|
||||||
|
button.setTitle("This is button", for: .normal)
|
||||||
|
button.setBackgroundImage(borderImage, for: .normal)
|
||||||
|
|
||||||
|
button.sizeToFit()
|
||||||
|
|
||||||
|
let layerDrawingOperation = CALayerDrawingOperation(layer: button.layer, offset: .zero)
|
||||||
|
|
||||||
|
let layerRenderer = UIGraphicsImageRenderer(size: button.bounds.size)
|
||||||
|
|
||||||
|
let buttonSnapshotImage = layerRenderer.image {
|
||||||
|
layerDrawingOperation.apply(in: $0.cgContext)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*:
|
||||||
|
### `TextDrawingOperation`
|
||||||
|
|
||||||
|
Операция отрисовывает текст с заданными атрибутами
|
||||||
|
*/
|
||||||
|
|
||||||
|
let textDrawingOperaton = TextDrawingOperation(text: "This is string",
|
||||||
|
textAttributes: [
|
||||||
|
.font: UIFont.boldSystemFont(ofSize: 15),
|
||||||
|
.foregroundColor: UIColor.white
|
||||||
|
])
|
||||||
|
|
||||||
|
let textRenderer = UIGraphicsImageRenderer(size: textDrawingOperaton.desiredContextSize)
|
||||||
|
|
||||||
|
let textImage = textRenderer.image {
|
||||||
|
textDrawingOperaton.apply(in: $0.cgContext)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*:
|
||||||
|
### `TemplateDrawingOperation`
|
||||||
|
|
||||||
|
Операция заменяет все цвета кроме прозрачного на переданный цвет.
|
||||||
|
Используется для приданиям иконкам определённого цвета (аналог tintColor).
|
||||||
|
*/
|
||||||
|
|
||||||
|
if let cgImage = textImage.cgImage {
|
||||||
|
let templateDrawingOperation = TemplateDrawingOperation(image: cgImage,
|
||||||
|
imageSize: textImage.size,
|
||||||
|
color: UIColor.red.cgColor)
|
||||||
|
|
||||||
|
let tintedImage = textRenderer.image {
|
||||||
|
templateDrawingOperation.apply(in: $0.cgContext)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*:
|
||||||
|
### `TransformDrawingOperation`
|
||||||
|
|
||||||
|
Операция производит изменение размера изображения учитывая пропорции
|
||||||
|
и другие настройки (по аналогии с contentMode у UIImage).
|
||||||
|
*/
|
||||||
|
|
||||||
|
if let cgImage = textImage.cgImage {
|
||||||
|
let newSize = CGSize(width: textImage.size.width / 1.5, height: textImage.size.height)
|
||||||
|
let resizeOperation = TransformDrawingOperation(image: cgImage,
|
||||||
|
imageSize: textImage.size,
|
||||||
|
maxNewSize: newSize,
|
||||||
|
resizeMode: .scaleAspectFill)
|
||||||
|
|
||||||
|
let resizeRenderer = UIGraphicsImageRenderer(size: newSize)
|
||||||
|
|
||||||
|
let resizedImage = resizeRenderer.image {
|
||||||
|
resizeOperation.apply(in: $0.cgContext)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
public protocol NefPlaygroundLiveViewable {}
|
||||||
|
extension UIView: NefPlaygroundLiveViewable {}
|
||||||
|
extension UIViewController: NefPlaygroundLiveViewable {}
|
||||||
|
|
||||||
|
#if NOT_IN_PLAYGROUND
|
||||||
|
public enum Nef {
|
||||||
|
public enum Playground {
|
||||||
|
public static func liveView(_ view: NefPlaygroundLiveViewable) {}
|
||||||
|
public static func needsIndefiniteExecution(_ state: Bool) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#else
|
||||||
|
import PlaygroundSupport
|
||||||
|
|
||||||
|
public enum Nef {
|
||||||
|
public enum Playground {
|
||||||
|
public static func liveView(_ view: NefPlaygroundLiveViewable) {
|
||||||
|
PlaygroundPage.current.liveView = (view as! PlaygroundLiveViewable)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func needsIndefiniteExecution(_ state: Bool) {
|
||||||
|
PlaygroundPage.current.needsIndefiniteExecution = state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<playground version='6.0' target-platform='ios' display-mode='raw' buildActiveScheme='true'/>
|
||||||
|
|
@ -0,0 +1,396 @@
|
||||||
|
// !$*UTF8*$!
|
||||||
|
{
|
||||||
|
archiveVersion = 1;
|
||||||
|
classes = {
|
||||||
|
};
|
||||||
|
objectVersion = 50;
|
||||||
|
objects = {
|
||||||
|
|
||||||
|
/* Begin PBXBuildFile section */
|
||||||
|
A319933CF180B210C469FFB1 /* Pods_TICoreGraphicsUtils.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AE2E24C8921A6FA2CDB65242 /* Pods_TICoreGraphicsUtils.framework */; };
|
||||||
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
|
/* Begin PBXFileReference section */
|
||||||
|
82AF4D8AFDF24586DD661012 /* Pods-TICoreGraphicsUtils.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TICoreGraphicsUtils.debug.xcconfig"; path = "Target Support Files/Pods-TICoreGraphicsUtils/Pods-TICoreGraphicsUtils.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
8BACBE8322576CAD00266845 /* TICoreGraphicsUtils.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = TICoreGraphicsUtils.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
8BACBE8622576CAD00266845 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
|
9238CAF7BE416E9ACF0C153E /* Pods-TICoreGraphicsUtils.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TICoreGraphicsUtils.release.xcconfig"; path = "Target Support Files/Pods-TICoreGraphicsUtils/Pods-TICoreGraphicsUtils.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
AE2E24C8921A6FA2CDB65242 /* Pods_TICoreGraphicsUtils.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_TICoreGraphicsUtils.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
|
8BACBE8022576CAD00266845 /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
A319933CF180B210C469FFB1 /* Pods_TICoreGraphicsUtils.framework in Frameworks */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXGroup section */
|
||||||
|
2238D5BA030C14EBEF939AC4 /* Pods */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
82AF4D8AFDF24586DD661012 /* Pods-TICoreGraphicsUtils.debug.xcconfig */,
|
||||||
|
9238CAF7BE416E9ACF0C153E /* Pods-TICoreGraphicsUtils.release.xcconfig */,
|
||||||
|
);
|
||||||
|
name = Pods;
|
||||||
|
path = Pods;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
8B39A26221D40F8700DE2643 = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
8BACBE8422576CAD00266845 /* TICoreGraphicsUtils */,
|
||||||
|
8B39A26C21D40F8700DE2643 /* Products */,
|
||||||
|
2238D5BA030C14EBEF939AC4 /* Pods */,
|
||||||
|
B51B4BA44249652EDC4FBF61 /* Frameworks */,
|
||||||
|
);
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
8B39A26C21D40F8700DE2643 /* Products */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
8BACBE8322576CAD00266845 /* TICoreGraphicsUtils.framework */,
|
||||||
|
);
|
||||||
|
name = Products;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
8BACBE8422576CAD00266845 /* TICoreGraphicsUtils */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
8BACBE8622576CAD00266845 /* Info.plist */,
|
||||||
|
);
|
||||||
|
path = TICoreGraphicsUtils;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
B51B4BA44249652EDC4FBF61 /* Frameworks */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
AE2E24C8921A6FA2CDB65242 /* Pods_TICoreGraphicsUtils.framework */,
|
||||||
|
);
|
||||||
|
name = Frameworks;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
/* End PBXGroup section */
|
||||||
|
|
||||||
|
/* Begin PBXHeadersBuildPhase section */
|
||||||
|
8BACBE7E22576CAD00266845 /* Headers */ = {
|
||||||
|
isa = PBXHeadersBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXHeadersBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXNativeTarget section */
|
||||||
|
8BACBE8222576CAD00266845 /* TICoreGraphicsUtils */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = 8BACBE8A22576CAD00266845 /* Build configuration list for PBXNativeTarget "TICoreGraphicsUtils" */;
|
||||||
|
buildPhases = (
|
||||||
|
3A6585E20D9DE6C1873231DC /* [CP] Check Pods Manifest.lock */,
|
||||||
|
8BACBE7E22576CAD00266845 /* Headers */,
|
||||||
|
8BACBE7F22576CAD00266845 /* Sources */,
|
||||||
|
8BACBE8022576CAD00266845 /* Frameworks */,
|
||||||
|
8BACBE8122576CAD00266845 /* Resources */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
);
|
||||||
|
name = TICoreGraphicsUtils;
|
||||||
|
productName = TICoreGraphicsUtils2;
|
||||||
|
productReference = 8BACBE8322576CAD00266845 /* TICoreGraphicsUtils.framework */;
|
||||||
|
productType = "com.apple.product-type.framework";
|
||||||
|
};
|
||||||
|
/* End PBXNativeTarget section */
|
||||||
|
|
||||||
|
/* Begin PBXProject section */
|
||||||
|
8B39A26321D40F8700DE2643 /* Project object */ = {
|
||||||
|
isa = PBXProject;
|
||||||
|
attributes = {
|
||||||
|
LastSwiftUpdateCheck = 1010;
|
||||||
|
LastUpgradeCheck = 1200;
|
||||||
|
ORGANIZATIONNAME = "47 Degrees";
|
||||||
|
TargetAttributes = {
|
||||||
|
8BACBE8222576CAD00266845 = {
|
||||||
|
CreatedOnToolsVersion = 10.1;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
buildConfigurationList = 8B39A26621D40F8700DE2643 /* Build configuration list for PBXProject "TICoreGraphicsUtils" */;
|
||||||
|
compatibilityVersion = "Xcode 9.3";
|
||||||
|
developmentRegion = en;
|
||||||
|
hasScannedForEncodings = 0;
|
||||||
|
knownRegions = (
|
||||||
|
en,
|
||||||
|
Base,
|
||||||
|
);
|
||||||
|
mainGroup = 8B39A26221D40F8700DE2643;
|
||||||
|
productRefGroup = 8B39A26C21D40F8700DE2643 /* Products */;
|
||||||
|
projectDirPath = "";
|
||||||
|
projectRoot = "";
|
||||||
|
targets = (
|
||||||
|
8BACBE8222576CAD00266845 /* TICoreGraphicsUtils */,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
/* End PBXProject section */
|
||||||
|
|
||||||
|
/* Begin PBXResourcesBuildPhase section */
|
||||||
|
8BACBE8122576CAD00266845 /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXResourcesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXShellScriptBuildPhase section */
|
||||||
|
3A6585E20D9DE6C1873231DC /* [CP] Check Pods Manifest.lock */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||||
|
"${PODS_ROOT}/Manifest.lock",
|
||||||
|
);
|
||||||
|
name = "[CP] Check Pods Manifest.lock";
|
||||||
|
outputFileListPaths = (
|
||||||
|
);
|
||||||
|
outputPaths = (
|
||||||
|
"$(DERIVED_FILE_DIR)/Pods-TICoreGraphicsUtils-checkManifestLockResult.txt",
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||||
|
showEnvVarsInLog = 0;
|
||||||
|
};
|
||||||
|
/* End PBXShellScriptBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
|
8BACBE7F22576CAD00266845 /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXSourcesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin XCBuildConfiguration section */
|
||||||
|
8B39A27721D40F8800DE2643 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||||
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CLANG_ENABLE_OBJC_ARC = YES;
|
||||||
|
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||||
|
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||||
|
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_COMMA = YES;
|
||||||
|
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||||
|
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||||
|
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||||
|
CLANG_WARN_EMPTY_BODY = YES;
|
||||||
|
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||||
|
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||||
|
CLANG_WARN_INT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||||
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||||
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||||
|
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||||
|
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||||
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
|
CODE_SIGN_IDENTITY = "-";
|
||||||
|
COPY_PHASE_STRIP = NO;
|
||||||
|
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||||
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
|
ENABLE_TESTABILITY = YES;
|
||||||
|
ENABLE_TESTING_SEARCH_PATHS = YES;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
|
GCC_DYNAMIC_NO_PIC = NO;
|
||||||
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
|
GCC_OPTIMIZATION_LEVEL = 0;
|
||||||
|
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||||
|
"DEBUG=1",
|
||||||
|
"$(inherited)",
|
||||||
|
);
|
||||||
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||||
|
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||||
|
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||||
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||||
|
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||||
|
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||||
|
MTL_FAST_MATH = YES;
|
||||||
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||||
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
8B39A27821D40F8800DE2643 /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||||
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CLANG_ENABLE_OBJC_ARC = YES;
|
||||||
|
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||||
|
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||||
|
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_COMMA = YES;
|
||||||
|
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||||
|
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||||
|
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||||
|
CLANG_WARN_EMPTY_BODY = YES;
|
||||||
|
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||||
|
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||||
|
CLANG_WARN_INT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||||
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||||
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||||
|
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||||
|
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||||
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
|
CODE_SIGN_IDENTITY = "-";
|
||||||
|
COPY_PHASE_STRIP = NO;
|
||||||
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
|
ENABLE_NS_ASSERTIONS = NO;
|
||||||
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
|
ENABLE_TESTING_SEARCH_PATHS = YES;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||||
|
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||||
|
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||||
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||||
|
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||||
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
|
MTL_FAST_MATH = YES;
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
|
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
8BACBE8822576CAD00266845 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 82AF4D8AFDF24586DD661012 /* Pods-TICoreGraphicsUtils.debug.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
CODE_SIGN_IDENTITY = "";
|
||||||
|
CODE_SIGN_STYLE = Manual;
|
||||||
|
CURRENT_TICoreGraphicsUtils_VERSION = 1;
|
||||||
|
DEFINES_MODULE = YES;
|
||||||
|
DEVELOPMENT_TEAM = "";
|
||||||
|
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||||
|
DYLIB_CURRENT_VERSION = 1;
|
||||||
|
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||||
|
INFOPLIST_FILE = "$(SRCROOT)/TICoreGraphicsUtils/Info.plist";
|
||||||
|
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 12.1;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
"@loader_path/Frameworks",
|
||||||
|
);
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.47deg.ios.TICoreGraphicsUtils;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||||
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
SKIP_INSTALL = YES;
|
||||||
|
SWIFT_VERSION = 5;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
|
VERSION_INFO_PREFIX = "";
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
8BACBE8922576CAD00266845 /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 9238CAF7BE416E9ACF0C153E /* Pods-TICoreGraphicsUtils.release.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
CODE_SIGN_IDENTITY = "";
|
||||||
|
CODE_SIGN_STYLE = Manual;
|
||||||
|
CURRENT_TICoreGraphicsUtils_VERSION = 1;
|
||||||
|
DEFINES_MODULE = YES;
|
||||||
|
DEVELOPMENT_TEAM = "";
|
||||||
|
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||||
|
DYLIB_CURRENT_VERSION = 1;
|
||||||
|
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||||
|
INFOPLIST_FILE = "$(SRCROOT)/TICoreGraphicsUtils/Info.plist";
|
||||||
|
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 12.1;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
"@loader_path/Frameworks",
|
||||||
|
);
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.47deg.ios.TICoreGraphicsUtils;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||||
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
SKIP_INSTALL = YES;
|
||||||
|
SWIFT_VERSION = 5;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
VALIDATE_PRODUCT = YES;
|
||||||
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
|
VERSION_INFO_PREFIX = "";
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
/* End XCBuildConfiguration section */
|
||||||
|
|
||||||
|
/* Begin XCConfigurationList section */
|
||||||
|
8B39A26621D40F8700DE2643 /* Build configuration list for PBXProject "TICoreGraphicsUtils" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
8B39A27721D40F8800DE2643 /* Debug */,
|
||||||
|
8B39A27821D40F8800DE2643 /* Release */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
8BACBE8A22576CAD00266845 /* Build configuration list for PBXNativeTarget "TICoreGraphicsUtils" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
8BACBE8822576CAD00266845 /* Debug */,
|
||||||
|
8BACBE8922576CAD00266845 /* Release */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
/* End XCConfigurationList section */
|
||||||
|
};
|
||||||
|
rootObject = 8B39A26321D40F8700DE2643 /* Project object */;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Workspace
|
||||||
|
version = "1.0">
|
||||||
|
<FileRef
|
||||||
|
location = "self:TICoreGraphicsUtils.xcodeproj">
|
||||||
|
</FileRef>
|
||||||
|
</Workspace>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue