Compare commits
290 Commits
release/20
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8212d7146c | ||
|
|
5c280b7922 | ||
|
|
1eed9ebb76 | ||
|
|
a210b8da9b | ||
|
|
44adc76dcb | ||
|
|
eb4f741f1a | ||
|
|
928c8c5b54 | ||
|
|
2dd3a7dca8 | ||
|
|
7bed914dca | ||
|
|
fe5bd4a9ac | ||
|
|
7eb1a4ff4b | ||
|
|
76b58ec88a | ||
|
|
4487f9b03b | ||
|
|
3a06f7b43e | ||
|
|
f3e97ecfd4 | ||
|
|
f66dc66818 | ||
|
|
bf2a3e7e5b | ||
|
|
54b50bffd3 | ||
|
|
3b2dc2d447 | ||
|
|
c737ca1344 | ||
|
|
d2e841c533 | ||
|
|
69d057c45a | ||
|
|
0510cded89 | ||
|
|
03a8a2d925 | ||
|
|
65bf81830e | ||
|
|
bb2fbe4bdd | ||
|
|
ce038458dc | ||
|
|
de33f8ffa1 | ||
|
|
4d4765ccec | ||
|
|
464e36606e | ||
|
|
8a7c07ecf2 | ||
|
|
740c3eeb31 | ||
|
|
00a11cd5dc | ||
|
|
03a1bd0920 | ||
|
|
4f6b848e72 | ||
|
|
5043ff35a0 | ||
|
|
806ac9821a | ||
|
|
47e63e0c8b | ||
|
|
2d7f8ae2ca | ||
|
|
e708b97581 | ||
|
|
1b5ca94988 | ||
|
|
d7756b66b0 | ||
|
|
89842ee443 | ||
|
|
61f9fb9946 | ||
|
|
484b578568 | ||
|
|
1c723c31de | ||
|
|
e79c1d7637 | ||
|
|
02a2da7f0a | ||
|
|
22c30de62f | ||
|
|
f6f3cd53fe | ||
|
|
378c46facd | ||
|
|
c2e10582f8 | ||
|
|
8670fa3193 | ||
|
|
9bbf325640 | ||
|
|
59e762f52e | ||
|
|
62c1aa6a8a | ||
|
|
df1846c295 | ||
|
|
533bcd2dea | ||
|
|
e211d4dc22 | ||
|
|
f9915dfacc | ||
|
|
68a72f6b2e | ||
|
|
2a497082a0 | ||
|
|
961a0546a8 | ||
|
|
86d49053a6 | ||
|
|
db8ba63892 | ||
|
|
878fd11c8a | ||
|
|
c40dc5e396 | ||
|
|
f94ddbf866 | ||
|
|
b70d2f8add | ||
|
|
398bf94811 | ||
|
|
7f9ee10c5e | ||
|
|
6354174857 | ||
|
|
3be3b11e39 | ||
|
|
de30c5d286 | ||
|
|
472323f303 | ||
|
|
3b306a299a | ||
|
|
de67fd7fb6 | ||
|
|
60cefc57a6 | ||
|
|
9185e185fd | ||
|
|
3d5c188898 | ||
|
|
458f303972 | ||
|
|
ef08aa043e | ||
|
|
698f7b5520 | ||
|
|
3270f21951 | ||
|
|
315eac1dac | ||
|
|
c3290f83ba | ||
|
|
c41cf3f18e | ||
|
|
56b4142c14 | ||
|
|
e7bcd32588 | ||
|
|
6c841c078a | ||
|
|
29b56bf63b | ||
|
|
e030b460ce | ||
|
|
25dce9e88c | ||
|
|
7b0f4e4328 | ||
|
|
8829bd7457 | ||
|
|
478f2b6083 | ||
|
|
94ace3d49a | ||
|
|
e3f22efb12 | ||
|
|
b71a4c5b25 | ||
|
|
3fd10bf199 | ||
|
|
aeabee2666 | ||
|
|
d8c83f9230 | ||
|
|
6b608b4a8a | ||
|
|
13f9776a4f | ||
|
|
ffd5242cde | ||
|
|
e89272a3cd | ||
|
|
33dc411743 | ||
|
|
558a3d7c55 | ||
|
|
17a94e2f04 | ||
|
|
4921d7d8db | ||
|
|
8c5a8776ab | ||
|
|
3867384307 | ||
|
|
efccb312e8 | ||
|
|
f437fbaea9 | ||
|
|
441525a610 | ||
|
|
07351c1a22 | ||
|
|
236a1969e9 | ||
|
|
f96b750302 | ||
|
|
715a911ee4 | ||
|
|
696b6b99d0 | ||
|
|
b41d2189bf | ||
|
|
8c1577bb7f | ||
|
|
21d1e957d0 | ||
|
|
c34a5c7661 | ||
|
|
d308bfc957 | ||
|
|
92b28d767b | ||
|
|
a691f8d713 | ||
|
|
f412fd70e1 | ||
|
|
50b0280ba4 | ||
|
|
dfbb53f545 | ||
|
|
1d2defeabb | ||
|
|
3d6d7c0cad | ||
|
|
2ce1b671bd | ||
|
|
e13e8ca5ab | ||
|
|
100ede48fa | ||
|
|
0d6fc3d15d | ||
|
|
5eca67fe0f | ||
|
|
1a48113b0d | ||
|
|
37c6dfd6b3 | ||
|
|
0938328ea2 | ||
|
|
5a3f04aa5f | ||
|
|
5e116e3c91 | ||
|
|
941312dd08 | ||
|
|
c2cb8781d6 | ||
|
|
ab5a5a1e55 | ||
|
|
3732f98d7c | ||
|
|
d69722c431 | ||
|
|
9606b914cc | ||
|
|
57bec1ecec | ||
|
|
998ad97372 | ||
|
|
d199389ff3 | ||
|
|
b24094ea5b | ||
|
|
ac23b0dc41 | ||
|
|
5a47b32789 | ||
|
|
f1087527d8 | ||
|
|
eb9ad769e5 | ||
|
|
fe66380d72 | ||
|
|
eab6fb5f3b | ||
|
|
fa650bafb5 | ||
|
|
aca65b13d3 | ||
|
|
4b06a5ba1f | ||
|
|
ccf520f49e | ||
|
|
2771808674 | ||
|
|
bf491e2341 | ||
|
|
3e702ec0f5 | ||
|
|
c794add84d | ||
|
|
344b2f18aa | ||
|
|
d0a6bdda3d | ||
|
|
68083819be | ||
|
|
fbecc82d20 | ||
|
|
8ec3f01e2c | ||
|
|
bdaa4b5ea7 | ||
|
|
66464cebc3 | ||
|
|
b754b476a6 | ||
|
|
8215542ee7 | ||
|
|
192509feef | ||
|
|
8a89b2b2a9 | ||
|
|
35ab4c94d2 | ||
|
|
5ebdbd24f1 | ||
|
|
3d86b8fcf7 | ||
|
|
62a74e8c34 | ||
|
|
796215f64d | ||
|
|
694f074d08 | ||
|
|
2675488ab3 | ||
|
|
4ad3f7c078 | ||
|
|
e1755a3b32 | ||
|
|
fa3c97cabe | ||
|
|
b8d6376e86 | ||
|
|
6db3a58782 | ||
|
|
226c23abae | ||
|
|
3689b16339 | ||
|
|
c47d21bdf1 | ||
|
|
f82ef2e51c | ||
|
|
efd98cc887 | ||
|
|
c7d2dcd655 | ||
|
|
2888eebb69 | ||
|
|
38ae12e91b | ||
|
|
0f4a97b49c | ||
|
|
7f32af62cc | ||
|
|
3b5b03a368 | ||
|
|
a845f70b55 | ||
|
|
16c20507ce | ||
|
|
018fe9a25e | ||
|
|
239af70710 | ||
|
|
36669b61cb | ||
|
|
65be46c767 | ||
|
|
273f45def0 | ||
|
|
bc72d81396 | ||
|
|
c77f0c6837 | ||
|
|
46d57733e4 | ||
|
|
f001f98d3a | ||
|
|
8e5e713c72 | ||
|
|
29466e4e33 | ||
|
|
5cbd472a2d | ||
|
|
53c4bd6c46 | ||
|
|
022355a49f | ||
|
|
c8496194f0 | ||
|
|
d2eae79f47 | ||
|
|
1a23c97a73 | ||
|
|
b5f61953df | ||
|
|
c8d817948b | ||
|
|
80edb943b2 | ||
|
|
05e2967131 | ||
|
|
c925323347 | ||
|
|
716550bf23 | ||
|
|
1b391272c5 | ||
|
|
fedf1760ec | ||
|
|
de60fdfaf9 | ||
|
|
73893d36d7 | ||
|
|
cb30a58db6 | ||
|
|
0abc903793 | ||
|
|
675ba3eaff | ||
|
|
c7d5cac1af | ||
|
|
4c28312191 | ||
|
|
d76d7c0974 | ||
|
|
be5a691365 | ||
|
|
7ec685136b | ||
|
|
021f4184a6 | ||
|
|
20d818b2c2 | ||
|
|
bd4c7780b0 | ||
|
|
cadded3217 | ||
|
|
c7a4bf72c3 | ||
|
|
9e9a1ab853 | ||
|
|
b1c59124a7 | ||
|
|
a4b550cf03 | ||
|
|
0ab9ad37a8 | ||
|
|
797d044913 | ||
|
|
79d5529991 | ||
|
|
981bdac292 | ||
|
|
8bf4a28b0f | ||
|
|
144efea342 | ||
|
|
4eba94f413 | ||
|
|
1b0197ed2c | ||
|
|
82fe0e13de | ||
|
|
8c5840fba3 | ||
|
|
8a12bf12c2 | ||
|
|
6e88ae0b62 | ||
|
|
7fec6f540e | ||
|
|
cae92360a2 | ||
|
|
b2ad684f00 | ||
|
|
90f9a0bcf5 | ||
|
|
04f228ea48 | ||
|
|
724130a827 | ||
|
|
10c55b12be | ||
|
|
fa52fa8c12 | ||
|
|
fcd4f4e0ec | ||
|
|
4cbf15a3a7 | ||
|
|
1d048f9389 | ||
|
|
aa773dc8f5 | ||
|
|
fdab57200d | ||
|
|
a84f502a0e | ||
|
|
3cdd3a95a0 | ||
|
|
9e712aa213 | ||
|
|
d8d4b37d1d | ||
|
|
568ab2e5e5 | ||
|
|
847daee32f | ||
|
|
dee11d7732 | ||
|
|
f3efb91fee | ||
|
|
7357a199d2 | ||
|
|
12bbe5d9a2 | ||
|
|
3092804f7c | ||
|
|
11a023d92a | ||
|
|
04e11ee1fb | ||
|
|
91eb4fa87a | ||
|
|
080f77581f | ||
|
|
525f0f8f0a | ||
|
|
66255d9dc5 | ||
|
|
eb78d507d6 | ||
|
|
3cdd74f097 | ||
|
|
3108dcdafc |
@ -153,6 +153,9 @@
|
||||
444FB7C32821B76B00DFE692 /* TitleLockupModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 444FB7C22821B76B00DFE692 /* TitleLockupModel.swift */; };
|
||||
4457904E27ECE989002B1E1E /* UIImageRenderingMode+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4457904D27ECE989002B1E1E /* UIImageRenderingMode+Extension.swift */; };
|
||||
4B002ACA2BD855EC009BC9C1 /* DateDropdownEntryFieldModel+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B002AC92BD855EC009BC9C1 /* DateDropdownEntryFieldModel+Extension.swift */; };
|
||||
4B3408A22C3873B0003BFABF /* CircularProgressBarModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B3408A12C3873B0003BFABF /* CircularProgressBarModel.swift */; };
|
||||
4B3408A42C3873E8003BFABF /* CircularProgressBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B3408A32C3873E8003BFABF /* CircularProgressBar.swift */; };
|
||||
4B53AF7B2C45BBBA00274685 /* GraphSizeProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B53AF7A2C45BBBA00274685 /* GraphSizeProtocol.swift */; };
|
||||
522679C123FE886900906CBA /* ListLeftVariableCheckboxAllTextAndLinks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 522679BF23FE886900906CBA /* ListLeftVariableCheckboxAllTextAndLinks.swift */; };
|
||||
522679C223FE886900906CBA /* ListLeftVariableCheckboxAllTextAndLinksModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 522679C023FE886900906CBA /* ListLeftVariableCheckboxAllTextAndLinksModel.swift */; };
|
||||
52267A0723FFE25000906CBA /* ListOneColumnFullWidthTextAllTextAndLinks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52267A0623FFE25000906CBA /* ListOneColumnFullWidthTextAllTextAndLinks.swift */; };
|
||||
@ -304,6 +307,9 @@
|
||||
AFA4932229E5EF2E001A9663 /* NotificationHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFA4932129E5EF2E001A9663 /* NotificationHandler.swift */; };
|
||||
AFA4933F29E874F0001A9663 /* MVMCoreUILoggingDelegateProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFA4933E29E874F0001A9663 /* MVMCoreUILoggingDelegateProtocol.swift */; };
|
||||
AFA4935729EE3DCC001A9663 /* AlertDelegateProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFA4935629EE3DCC001A9663 /* AlertDelegateProtocol.swift */; };
|
||||
AFB6336E2C65166E00791221 /* GoneableProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFB6336D2C65166E00791221 /* GoneableProtocol.swift */; };
|
||||
AFB633702C65175800791221 /* ActionUpdateVisibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFB6336F2C65175800791221 /* ActionUpdateVisibility.swift */; };
|
||||
AFB633722C653C0900791221 /* ActionUpdateVisibilityModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFB633712C653C0900791221 /* ActionUpdateVisibilityModel.swift */; };
|
||||
AFE4A1D627DFBB6F00C458D0 /* UINavigationController+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFE4A1D527DFBB6F00C458D0 /* UINavigationController+Extension.swift */; };
|
||||
B4CC8FBD29DF34680005D28B /* Badge.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4CC8FBC29DF34680005D28B /* Badge.swift */; };
|
||||
B4CC8FBF29DF34730005D28B /* BadgeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4CC8FBE29DF34730005D28B /* BadgeModel.swift */; };
|
||||
@ -578,13 +584,28 @@
|
||||
EA17584A2BC97EF100A5C0D9 /* BadgeIndicatorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA1758492BC97EF100A5C0D9 /* BadgeIndicatorModel.swift */; };
|
||||
EA17584C2BC9894800A5C0D9 /* ButtonIconModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA17584B2BC9894800A5C0D9 /* ButtonIconModel.swift */; };
|
||||
EA17584E2BC9895A00A5C0D9 /* ButtonIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA17584D2BC9895A00A5C0D9 /* ButtonIcon.swift */; };
|
||||
EA1B02DE2C41BFD200F0758B /* RuleVDSModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA1B02DD2C41BFD200F0758B /* RuleVDSModel.swift */; };
|
||||
EA1B02E02C470AFD00F0758B /* InputEntryField.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA1B02DF2C470AFD00F0758B /* InputEntryField.swift */; };
|
||||
EA41F4AC2787927100F5B377 /* DynamicRuleFormFieldEffectModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA41F4AB2787927100F5B377 /* DynamicRuleFormFieldEffectModel.swift */; };
|
||||
EA5124FD243601600051A3A4 /* BGImageHeadlineBodyButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA5124FC243601600051A3A4 /* BGImageHeadlineBodyButton.swift */; };
|
||||
EA5124FF2436018E0051A3A4 /* BGImageHeadlineBodyButtonModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA5124FE2436018E0051A3A4 /* BGImageHeadlineBodyButtonModel.swift */; };
|
||||
EA5DBDAB2C35B6C500290DF8 /* FormFieldModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA5DBDAA2C35B6C500290DF8 /* FormFieldModel.swift */; };
|
||||
EA6642912BCDA97300D81DC4 /* TileContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA6642902BCDA97300D81DC4 /* TileContainer.swift */; };
|
||||
EA6642932BCDA97D00D81DC4 /* TileContainerModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA6642922BCDA97D00D81DC4 /* TileContainerModel.swift */; };
|
||||
EA6E8B952B504A43000139B4 /* ButtonGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA6E8B942B504A43000139B4 /* ButtonGroup.swift */; };
|
||||
EA6E8B972B504A4D000139B4 /* ButtonGroupModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA6E8B962B504A4D000139B4 /* ButtonGroupModel.swift */; };
|
||||
EA7AE5472C73C01A00107C74 /* CheckboxesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA7AE5462C73C01A00107C74 /* CheckboxesModel.swift */; };
|
||||
EA7AE5492C7403DC00107C74 /* Checkboxes.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA7AE5482C7403DC00107C74 /* Checkboxes.swift */; };
|
||||
EA7AE54B2C74CACA00107C74 /* RadioButtons.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA7AE54A2C74CACA00107C74 /* RadioButtons.swift */; };
|
||||
EA7AE54D2C74CAD700107C74 /* RadioButtonsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA7AE54C2C74CAD700107C74 /* RadioButtonsModel.swift */; };
|
||||
EA7AE54F2C74EB3700107C74 /* CalendarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA7AE54E2C74EB3700107C74 /* CalendarView.swift */; };
|
||||
EA7AE5512C74EB4500107C74 /* CalendarViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA7AE5502C74EB4500107C74 /* CalendarViewModel.swift */; };
|
||||
EA7AE5532C74F1F600107C74 /* DatePickerEntryField.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA7AE5522C74F1F600107C74 /* DatePickerEntryField.swift */; };
|
||||
EA7AE5552C74F20600107C74 /* DatePickerEntryFieldModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA7AE5542C74F20600107C74 /* DatePickerEntryFieldModel.swift */; };
|
||||
EA7AE55C2C7D18A100107C74 /* BreadcrumbsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA7AE55B2C7D18A100107C74 /* BreadcrumbsModel.swift */; };
|
||||
EA7AE55E2C7D234500107C74 /* Breadcrumbs.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA7AE55D2C7D234500107C74 /* Breadcrumbs.swift */; };
|
||||
EA7AE5602C7E554700107C74 /* PaginationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA7AE55F2C7E554700107C74 /* PaginationModel.swift */; };
|
||||
EA7AE5622C7E555D00107C74 /* Pagination.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA7AE5612C7E555D00107C74 /* Pagination.swift */; };
|
||||
EA7D81602B2B6E6800D29F9E /* Icon.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA7D815F2B2B6E6800D29F9E /* Icon.swift */; };
|
||||
EA7D81622B2B6E7F00D29F9E /* IconModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA7D81612B2B6E7F00D29F9E /* IconModel.swift */; };
|
||||
EA7D81642B2BABCB00D29F9E /* TooltipModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA7D81632B2BABCB00D29F9E /* TooltipModel.swift */; };
|
||||
@ -770,6 +791,9 @@
|
||||
444FB7C22821B76B00DFE692 /* TitleLockupModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TitleLockupModel.swift; sourceTree = "<group>"; };
|
||||
4457904D27ECE989002B1E1E /* UIImageRenderingMode+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImageRenderingMode+Extension.swift"; sourceTree = "<group>"; };
|
||||
4B002AC92BD855EC009BC9C1 /* DateDropdownEntryFieldModel+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DateDropdownEntryFieldModel+Extension.swift"; sourceTree = "<group>"; };
|
||||
4B3408A12C3873B0003BFABF /* CircularProgressBarModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircularProgressBarModel.swift; sourceTree = "<group>"; };
|
||||
4B3408A32C3873E8003BFABF /* CircularProgressBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircularProgressBar.swift; sourceTree = "<group>"; };
|
||||
4B53AF7A2C45BBBA00274685 /* GraphSizeProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphSizeProtocol.swift; sourceTree = "<group>"; };
|
||||
522679BF23FE886900906CBA /* ListLeftVariableCheckboxAllTextAndLinks.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListLeftVariableCheckboxAllTextAndLinks.swift; sourceTree = "<group>"; };
|
||||
522679C023FE886900906CBA /* ListLeftVariableCheckboxAllTextAndLinksModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListLeftVariableCheckboxAllTextAndLinksModel.swift; sourceTree = "<group>"; };
|
||||
52267A0623FFE25000906CBA /* ListOneColumnFullWidthTextAllTextAndLinks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListOneColumnFullWidthTextAllTextAndLinks.swift; sourceTree = "<group>"; };
|
||||
@ -922,6 +946,9 @@
|
||||
AFA4932129E5EF2E001A9663 /* NotificationHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationHandler.swift; sourceTree = "<group>"; };
|
||||
AFA4933E29E874F0001A9663 /* MVMCoreUILoggingDelegateProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MVMCoreUILoggingDelegateProtocol.swift; sourceTree = "<group>"; };
|
||||
AFA4935629EE3DCC001A9663 /* AlertDelegateProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertDelegateProtocol.swift; sourceTree = "<group>"; };
|
||||
AFB6336D2C65166E00791221 /* GoneableProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoneableProtocol.swift; sourceTree = "<group>"; };
|
||||
AFB6336F2C65175800791221 /* ActionUpdateVisibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionUpdateVisibility.swift; sourceTree = "<group>"; };
|
||||
AFB633712C653C0900791221 /* ActionUpdateVisibilityModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionUpdateVisibilityModel.swift; sourceTree = "<group>"; };
|
||||
AFE4A1D527DFBB6F00C458D0 /* UINavigationController+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UINavigationController+Extension.swift"; sourceTree = "<group>"; };
|
||||
B4CC8FBC29DF34680005D28B /* Badge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Badge.swift; sourceTree = "<group>"; };
|
||||
B4CC8FBE29DF34730005D28B /* BadgeModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgeModel.swift; sourceTree = "<group>"; };
|
||||
@ -1198,13 +1225,28 @@
|
||||
EA1758492BC97EF100A5C0D9 /* BadgeIndicatorModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgeIndicatorModel.swift; sourceTree = "<group>"; };
|
||||
EA17584B2BC9894800A5C0D9 /* ButtonIconModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonIconModel.swift; sourceTree = "<group>"; };
|
||||
EA17584D2BC9895A00A5C0D9 /* ButtonIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonIcon.swift; sourceTree = "<group>"; };
|
||||
EA1B02DD2C41BFD200F0758B /* RuleVDSModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuleVDSModel.swift; sourceTree = "<group>"; };
|
||||
EA1B02DF2C470AFD00F0758B /* InputEntryField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputEntryField.swift; sourceTree = "<group>"; };
|
||||
EA41F4AB2787927100F5B377 /* DynamicRuleFormFieldEffectModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicRuleFormFieldEffectModel.swift; sourceTree = "<group>"; };
|
||||
EA5124FC243601600051A3A4 /* BGImageHeadlineBodyButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGImageHeadlineBodyButton.swift; sourceTree = "<group>"; };
|
||||
EA5124FE2436018E0051A3A4 /* BGImageHeadlineBodyButtonModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGImageHeadlineBodyButtonModel.swift; sourceTree = "<group>"; };
|
||||
EA5DBDAA2C35B6C500290DF8 /* FormFieldModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FormFieldModel.swift; sourceTree = "<group>"; };
|
||||
EA6642902BCDA97300D81DC4 /* TileContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TileContainer.swift; sourceTree = "<group>"; };
|
||||
EA6642922BCDA97D00D81DC4 /* TileContainerModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TileContainerModel.swift; sourceTree = "<group>"; };
|
||||
EA6E8B942B504A43000139B4 /* ButtonGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonGroup.swift; sourceTree = "<group>"; };
|
||||
EA6E8B962B504A4D000139B4 /* ButtonGroupModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonGroupModel.swift; sourceTree = "<group>"; };
|
||||
EA7AE5462C73C01A00107C74 /* CheckboxesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckboxesModel.swift; sourceTree = "<group>"; };
|
||||
EA7AE5482C7403DC00107C74 /* Checkboxes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Checkboxes.swift; sourceTree = "<group>"; };
|
||||
EA7AE54A2C74CACA00107C74 /* RadioButtons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadioButtons.swift; sourceTree = "<group>"; };
|
||||
EA7AE54C2C74CAD700107C74 /* RadioButtonsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadioButtonsModel.swift; sourceTree = "<group>"; };
|
||||
EA7AE54E2C74EB3700107C74 /* CalendarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarView.swift; sourceTree = "<group>"; };
|
||||
EA7AE5502C74EB4500107C74 /* CalendarViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarViewModel.swift; sourceTree = "<group>"; };
|
||||
EA7AE5522C74F1F600107C74 /* DatePickerEntryField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatePickerEntryField.swift; sourceTree = "<group>"; };
|
||||
EA7AE5542C74F20600107C74 /* DatePickerEntryFieldModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatePickerEntryFieldModel.swift; sourceTree = "<group>"; };
|
||||
EA7AE55B2C7D18A100107C74 /* BreadcrumbsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BreadcrumbsModel.swift; sourceTree = "<group>"; };
|
||||
EA7AE55D2C7D234500107C74 /* Breadcrumbs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Breadcrumbs.swift; sourceTree = "<group>"; };
|
||||
EA7AE55F2C7E554700107C74 /* PaginationModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaginationModel.swift; sourceTree = "<group>"; };
|
||||
EA7AE5612C7E555D00107C74 /* Pagination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Pagination.swift; sourceTree = "<group>"; };
|
||||
EA7D815F2B2B6E6800D29F9E /* Icon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Icon.swift; sourceTree = "<group>"; };
|
||||
EA7D81612B2B6E7F00D29F9E /* IconModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconModel.swift; sourceTree = "<group>"; };
|
||||
EA7D81632B2BABCB00D29F9E /* TooltipModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TooltipModel.swift; sourceTree = "<group>"; };
|
||||
@ -1275,6 +1317,7 @@
|
||||
27F6B08B26052AFF008529AA /* ParentMoleculeModelProtocol.swift */,
|
||||
27577DCC286CA959001EC47E /* MoleculeMaskingProtocol.swift */,
|
||||
58E7561C2BE04C320088BB5D /* MoleculeComparisonProtocol.swift */,
|
||||
AFB6336D2C65166E00791221 /* GoneableProtocol.swift */,
|
||||
);
|
||||
path = ModelProtocols;
|
||||
sourceTree = "<group>";
|
||||
@ -1294,6 +1337,7 @@
|
||||
011D95A0240453D0000E3791 /* RuleEqualsModel.swift */,
|
||||
0A849EFD246F1775009F277F /* RuleEqualsIgnoreCaseModel.swift */,
|
||||
FD9912FF28E21E4900542CC3 /* RuleNotEqualsModel.swift */,
|
||||
EA1B02DD2C41BFD200F0758B /* RuleVDSModel.swift */,
|
||||
);
|
||||
name = Rules;
|
||||
path = Rules/Rules;
|
||||
@ -1585,6 +1629,8 @@
|
||||
AF1C33722885D481006B1001 /* MVMCoreUIActionOpenPageHandler.swift */,
|
||||
AF60A7F52892D2E300919EEB /* ActionDismissNotificationModel.swift */,
|
||||
AF60A7F72892D34D00919EEB /* ActionDismissNotificationHandler.swift */,
|
||||
AFB633712C653C0900791221 /* ActionUpdateVisibilityModel.swift */,
|
||||
AFB6336F2C65175800791221 /* ActionUpdateVisibility.swift */,
|
||||
);
|
||||
path = Actions;
|
||||
sourceTree = "<group>";
|
||||
@ -2009,8 +2055,12 @@
|
||||
BBAA4F00243D8E3B005AAD5F /* RadioBoxModel.swift */,
|
||||
D264FAA6243FE13B00D98315 /* RadioBox.swift */,
|
||||
0116A4E4228B19640094F3ED /* RadioButtonSelectionHelper.swift */,
|
||||
EA7AE54C2C74CAD700107C74 /* RadioButtonsModel.swift */,
|
||||
EA7AE54A2C74CACA00107C74 /* RadioButtons.swift */,
|
||||
011D95AE2407266E000E3791 /* RadioButtonModel.swift */,
|
||||
01004F2F22721C3800991ECC /* RadioButton.swift */,
|
||||
EA7AE5462C73C01A00107C74 /* CheckboxesModel.swift */,
|
||||
EA7AE5482C7403DC00107C74 /* Checkboxes.swift */,
|
||||
31BE15CA23D8924C00452370 /* CheckboxModel.swift */,
|
||||
0A7BAFA0232BE61800FB8E22 /* Checkbox.swift */,
|
||||
AAC6F166243332E400F295C1 /* RadioSwatchesModel.swift */,
|
||||
@ -2161,6 +2211,7 @@
|
||||
D29DF10E21E67A77003B2FB9 /* Molecules */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
EA7AE55A2C7D188900107C74 /* Breadcrumbs */,
|
||||
D2EC7BD22527A1E400F540AF /* HeadersAndFooters */,
|
||||
D2CAC7C9251104CB00C75681 /* TopNotification */,
|
||||
D2509ED42472EE0B001BFB9D /* NavigationBar */,
|
||||
@ -2309,8 +2360,11 @@
|
||||
94C2D9822386F3E30006CF46 /* Label */,
|
||||
31BE15C923D8924C00452370 /* CheckboxLabelModel.swift */,
|
||||
0A7BAFA2232BE63400FB8E22 /* CheckboxLabel.swift */,
|
||||
4B53AF7A2C45BBBA00274685 /* GraphSizeProtocol.swift */,
|
||||
D28A838223CCBD3F00DFE4FC /* WheelModel.swift */,
|
||||
943784F3236B77BB006A1E82 /* Wheel.swift */,
|
||||
4B3408A12C3873B0003BFABF /* CircularProgressBarModel.swift */,
|
||||
4B3408A32C3873E8003BFABF /* CircularProgressBar.swift */,
|
||||
943784F4236B77BB006A1E82 /* WheelAnimationHandler.swift */,
|
||||
0AE98BB623FF18E9004C5109 /* ArrowModel.swift */,
|
||||
0AE98BB423FF18D2004C5109 /* Arrow.swift */,
|
||||
@ -2319,6 +2373,8 @@
|
||||
D20492A524329CE200A5EED6 /* LoadImageView.swift */,
|
||||
0A51F3E02475CB73002E08B6 /* LoadingSpinnerModel.swift */,
|
||||
0A51F3E12475CB73002E08B6 /* LoadingSpinner.swift */,
|
||||
EA7AE55F2C7E554700107C74 /* PaginationModel.swift */,
|
||||
EA7AE5612C7E555D00107C74 /* Pagination.swift */,
|
||||
AA37CBD2251907200027344C /* StarsModel.swift */,
|
||||
AA37CBD42519072F0027344C /* Stars.swift */,
|
||||
AA07EA902510A442009A2AE3 /* StarModel.swift */,
|
||||
@ -2337,6 +2393,8 @@
|
||||
EA7D815F2B2B6E6800D29F9E /* Icon.swift */,
|
||||
EA7D81632B2BABCB00D29F9E /* TooltipModel.swift */,
|
||||
EA7D81652B2BABD200D29F9E /* Tooltip.swift */,
|
||||
EA7AE5502C74EB4500107C74 /* CalendarViewModel.swift */,
|
||||
EA7AE54E2C74EB3700107C74 /* CalendarView.swift */,
|
||||
);
|
||||
path = Views;
|
||||
sourceTree = "<group>";
|
||||
@ -2346,6 +2404,7 @@
|
||||
children = (
|
||||
0A7EF85A23D8A52800B2AAD1 /* EntryFieldModel.swift */,
|
||||
0A21DB7E235DECC500C160A2 /* EntryField.swift */,
|
||||
EA1B02DF2C470AFD00F0758B /* InputEntryField.swift */,
|
||||
0A7EF85C23D8A95600B2AAD1 /* TextEntryFieldModel.swift */,
|
||||
0A41BA7E23453A6400D4C0BC /* TextEntryField.swift */,
|
||||
0A7EF85E23D8ABC500B2AAD1 /* MdnEntryFieldModel.swift */,
|
||||
@ -2487,6 +2546,9 @@
|
||||
D2BEFEF5248A954C00FAB3A9 /* FormFields */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
EA7AE5542C74F20600107C74 /* DatePickerEntryFieldModel.swift */,
|
||||
EA7AE5522C74F1F600107C74 /* DatePickerEntryField.swift */,
|
||||
EA5DBDAA2C35B6C500290DF8 /* FormFieldModel.swift */,
|
||||
D2BEFEF6248A957A00FAB3A9 /* Tags */,
|
||||
D29DF22B21E6A0FA003B2FB9 /* TextFields */,
|
||||
);
|
||||
@ -2590,6 +2652,15 @@
|
||||
path = Alerts;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
EA7AE55A2C7D188900107C74 /* Breadcrumbs */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
EA7AE55B2C7D18A100107C74 /* BreadcrumbsModel.swift */,
|
||||
EA7AE55D2C7D234500107C74 /* Breadcrumbs.swift */,
|
||||
);
|
||||
path = Breadcrumbs;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
EAA0CFAD275E7D5A00D65EB0 /* FormFieldEffect */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -2821,11 +2892,13 @@
|
||||
AA7F32AD246C0F8C00C965BA /* ListLeftVariableRadioButtonAllTextAndLinks.swift in Sources */,
|
||||
EAA482CE2B45F2F300978105 /* MFLoadingSpinner+VDS.swift in Sources */,
|
||||
D272F5F92473163100BD1A8F /* BarButtonItem.swift in Sources */,
|
||||
EA7AE54D2C74CAD700107C74 /* RadioButtonsModel.swift in Sources */,
|
||||
D2D2FCF3252B72CF0033EAAA /* MoleculeSectionFooter.swift in Sources */,
|
||||
0A9D09202433796500D2E6C0 /* BarsIndicatorView.swift in Sources */,
|
||||
D2E2A99423D8CCBC000B42E6 /* HeadlineBodyLinkModel.swift in Sources */,
|
||||
01004F3022721C3800991ECC /* RadioButton.swift in Sources */,
|
||||
D268C70E238C22D7007F2C1C /* DropDownFilterTableViewCell.swift in Sources */,
|
||||
AFB633702C65175800791221 /* ActionUpdateVisibility.swift in Sources */,
|
||||
D236E5B7242007C500C38625 /* MVMControllerModelProtocol.swift in Sources */,
|
||||
AA71AD4024A32FE700ACA76F /* HeadersH2Link.swift in Sources */,
|
||||
D29DF11721E6805F003B2FB9 /* UIColor+MFConvenience.m in Sources */,
|
||||
@ -2833,6 +2906,8 @@
|
||||
D253BB8A24574CC5002DE544 /* StackModel.swift in Sources */,
|
||||
EAB14BC127D935F00012AB2C /* RuleCompareModelProtocol.swift in Sources */,
|
||||
011D95A924057AC7000E3791 /* FormGroupWatcherFieldProtocol.swift in Sources */,
|
||||
EA1B02DE2C41BFD200F0758B /* RuleVDSModel.swift in Sources */,
|
||||
EA7AE5532C74F1F600107C74 /* DatePickerEntryField.swift in Sources */,
|
||||
EA985C892981AB7100F2FF2E /* VDS-TextStyle.swift in Sources */,
|
||||
BB2BF0EA2452A9BB001D0FC2 /* ListDeviceComplexButtonSmall.swift in Sources */,
|
||||
D20C700B250BFDE40095B21C /* NotificationContainerView.swift in Sources */,
|
||||
@ -2874,6 +2949,7 @@
|
||||
D2E2A99D23DA3217000B42E6 /* UIStackViewAlignment+Extension.swift in Sources */,
|
||||
01EB369423609801006832FA /* HeadlineBodyModel.swift in Sources */,
|
||||
D2A92884241ACB25004E01C6 /* ProgrammaticScrollViewController.swift in Sources */,
|
||||
AFB633722C653C0900791221 /* ActionUpdateVisibilityModel.swift in Sources */,
|
||||
EA985C3E2970938F00F2FF2E /* Tilelet.swift in Sources */,
|
||||
D23A90002612347A007E14CE /* PageBehaviorContainerModelProtocol.swift in Sources */,
|
||||
EAA78020290081320057DFDF /* VDSMoleculeViewProtocol.swift in Sources */,
|
||||
@ -2887,6 +2963,7 @@
|
||||
D29DF2CF21E7C104003B2FB9 /* MFLoadingViewController.m in Sources */,
|
||||
D28A837B23C928DA00DFE4FC /* MoleculeListCellProtocol.swift in Sources */,
|
||||
D28BA74D248589C800B75CB8 /* TabPageModelProtocol.swift in Sources */,
|
||||
EA7AE5602C7E554700107C74 /* PaginationModel.swift in Sources */,
|
||||
608211282AC6B57E00C3FC39 /* MVMCoreUILoggingHandler.swift in Sources */,
|
||||
014AA72F23C5059B006F3E93 /* ThreeLayerPageTemplateModel.swift in Sources */,
|
||||
0A21DB91235E0EDB00C160A2 /* DigitBox.swift in Sources */,
|
||||
@ -2912,8 +2989,10 @@
|
||||
525239C02407BCFF00454969 /* ListTwoColumnPriceDetailsModel.swift in Sources */,
|
||||
D2E2A99A23D8D6B4000B42E6 /* HeadlineBodyButtonModel.swift in Sources */,
|
||||
D202AFE6242A6A9C00E5BEDF /* UICollectionViewScrollPosition+Extension.swift in Sources */,
|
||||
EA7AE55C2C7D18A100107C74 /* BreadcrumbsModel.swift in Sources */,
|
||||
D20F3B44252E00E4004B3F56 /* PageProtocol.swift in Sources */,
|
||||
AA37CBD3251907200027344C /* StarsModel.swift in Sources */,
|
||||
EA7AE5622C7E555D00107C74 /* Pagination.swift in Sources */,
|
||||
8D084AD22410BF7600951227 /* ListOneColumnFullWidthTextBodyText.swift in Sources */,
|
||||
94C0150C2421564A005811A9 /* ActionCollapseNotificationModel.swift in Sources */,
|
||||
D2CAC7CB251104E100C75681 /* NotificationXButtonModel.swift in Sources */,
|
||||
@ -2940,6 +3019,7 @@
|
||||
AAE96FA525341F7D0037A989 /* ListStoreLocator.swift in Sources */,
|
||||
D282AABA224131D100C46919 /* MFTransparentGIFView.swift in Sources */,
|
||||
944589232385DA9600DE9FD4 /* ImageViewModel.swift in Sources */,
|
||||
EA7AE55E2C7D234500107C74 /* Breadcrumbs.swift in Sources */,
|
||||
D213347723843825008E41B3 /* Line.swift in Sources */,
|
||||
D2E2A99C23D8D975000B42E6 /* ImageHeadlineBodyModel.swift in Sources */,
|
||||
BB3BC1302550094500297977 /* ListLeftVariableIconWithRightCaretAllTextLinksModel.swift in Sources */,
|
||||
@ -3005,6 +3085,7 @@
|
||||
D29DF2EF21ECEAE1003B2FB9 /* MFFonts.m in Sources */,
|
||||
D22479942316AE5E003FCCF9 /* NSLayoutConstraintExtension.swift in Sources */,
|
||||
D2B18B94236214AD00A9AEDC /* NavigationController.swift in Sources */,
|
||||
4B3408A42C3873E8003BFABF /* CircularProgressBar.swift in Sources */,
|
||||
0A9D09222433796500D2E6C0 /* CarouselIndicator.swift in Sources */,
|
||||
EA17584E2BC9895A00A5C0D9 /* ButtonIcon.swift in Sources */,
|
||||
D29E28DA23D21AFA00ACEA85 /* StringAndMoleculeModel.swift in Sources */,
|
||||
@ -3025,6 +3106,7 @@
|
||||
AA1EC59924373994003D6F50 /* ListThreeColumnSpeedTestDivider.swift in Sources */,
|
||||
AA37CBD52519072F0027344C /* Stars.swift in Sources */,
|
||||
942C378E2412F5B60066E45E /* ModalMoleculeStackTemplate.swift in Sources */,
|
||||
4B3408A22C3873B0003BFABF /* CircularProgressBarModel.swift in Sources */,
|
||||
8D8067D32444473A00203BE8 /* ListRightVariablePriceChangeAllTextAndLinks.swift in Sources */,
|
||||
8D4687E4242E2DF300802879 /* ListFourColumnDataUsageListItem.swift in Sources */,
|
||||
D2874024249BA6F300BE950A /* MVMCoreUISplitViewController+Extension.swift in Sources */,
|
||||
@ -3076,7 +3158,9 @@
|
||||
AA69AAF62445BF5700AF3D3B /* ListLeftVariableCheckboxBodyText.swift in Sources */,
|
||||
AFA4935729EE3DCC001A9663 /* AlertDelegateProtocol.swift in Sources */,
|
||||
58A9DD7D2AC2103300F5E0B0 /* ReplaceableMoleculeBehaviorModel.swift in Sources */,
|
||||
EA7AE54B2C74CACA00107C74 /* RadioButtons.swift in Sources */,
|
||||
D264FAA3243E632F00D98315 /* ProgrammaticCollectionViewController.swift in Sources */,
|
||||
EA7AE5552C74F20600107C74 /* DatePickerEntryFieldModel.swift in Sources */,
|
||||
D29DF27A21E7A533003B2FB9 /* MVMCoreUISession.m in Sources */,
|
||||
27F9736A246750BE00CAB5C5 /* ScreenBrightnessModifierBehavior.swift in Sources */,
|
||||
EA6E8B972B504A4D000139B4 /* ButtonGroupModel.swift in Sources */,
|
||||
@ -3101,6 +3185,7 @@
|
||||
D2A6390522CBCE160052ED1F /* MoleculeCollectionViewCell.swift in Sources */,
|
||||
D2A6390122CBB1820052ED1F /* Carousel.swift in Sources */,
|
||||
C7F8012123E8303200396FBD /* ListRVWheel.swift in Sources */,
|
||||
4B53AF7B2C45BBBA00274685 /* GraphSizeProtocol.swift in Sources */,
|
||||
BB2C968F24330EA7006FF80C /* ListRightVariableTextLinkAllTextAndLinksModel.swift in Sources */,
|
||||
D2FB151B23A2B65B00C20E10 /* MoleculeContainer.swift in Sources */,
|
||||
EA7D81622B2B6E7F00D29F9E /* IconModel.swift in Sources */,
|
||||
@ -3133,6 +3218,7 @@
|
||||
323AC96A24C837F000F8E4C4 /* ListThreeColumnBillChangesModel.swift in Sources */,
|
||||
D2E1FAE12268E81D00AEFD8C /* MoleculeListTemplate.swift in Sources */,
|
||||
525019E72406853600EED91C /* ListFourColumnDataUsageDivider.swift in Sources */,
|
||||
EA1B02E02C470AFD00F0758B /* InputEntryField.swift in Sources */,
|
||||
D28BA730247EC2EB00B75CB8 /* NavigationButtonModelProtocol.swift in Sources */,
|
||||
0AE98BB323FF0934004C5109 /* ExternalLinkModel.swift in Sources */,
|
||||
D20FB165241A5D75004AFC3A /* NavigationItemModel.swift in Sources */,
|
||||
@ -3153,6 +3239,7 @@
|
||||
D2092355244FA0FD0044AD09 /* ThreeLayerTemplateModelProtocol.swift in Sources */,
|
||||
0AE14F64238315D2005417F8 /* TextField.swift in Sources */,
|
||||
0A51F3E22475CB73002E08B6 /* LoadingSpinnerModel.swift in Sources */,
|
||||
AFB6336E2C65166E00791221 /* GoneableProtocol.swift in Sources */,
|
||||
D2169303251E53D9002A6324 /* SectionListTemplateModel.swift in Sources */,
|
||||
EA6642932BCDA97D00D81DC4 /* TileContainerModel.swift in Sources */,
|
||||
AF7E509929E477C1009DC2AD /* AlertController.swift in Sources */,
|
||||
@ -3230,6 +3317,7 @@
|
||||
01F2C20327C81F9700DC3D36 /* SubNavManagerNavigationController.swift in Sources */,
|
||||
EABFC1412763BB8D00E78B40 /* FormLabel.swift in Sources */,
|
||||
AA997252247530B100FC7472 /* ListLeftVariableIconAllTextLinks.swift in Sources */,
|
||||
EA7AE5492C7403DC00107C74 /* Checkboxes.swift in Sources */,
|
||||
D2A5146122121FBF00345BFB /* MoleculeStackTemplate.swift in Sources */,
|
||||
D23EA7FB2475F09800D60C34 /* CarouselItemProtocol.swift in Sources */,
|
||||
D2E2A9A323E096B1000B42E6 /* DisableableModelProtocol.swift in Sources */,
|
||||
@ -3260,6 +3348,7 @@
|
||||
BB6C6AC824225290005F7224 /* ListOneColumnTextWithWhitespaceDividerShort.swift in Sources */,
|
||||
0A41BA6E2344FCD400D4C0BC /* CATransaction+Extension.swift in Sources */,
|
||||
D21B7F73243BAC6800051ABF /* CollectionItemModelProtocol.swift in Sources */,
|
||||
EA7AE5512C74EB4500107C74 /* CalendarViewModel.swift in Sources */,
|
||||
AA104B1A24474A66004D2810 /* HeadersH2Buttons.swift in Sources */,
|
||||
C7192E7D23C301750050C2A0 /* HeadLineBodyCaretLinkImage.swift in Sources */,
|
||||
D2D2FCF0252B72AF0033EAAA /* MoleculeSectionFooterModel.swift in Sources */,
|
||||
@ -3280,6 +3369,7 @@
|
||||
D2D3957D252FDBCD00047B11 /* ModalSectionListTemplateModel.swift in Sources */,
|
||||
D2B9D0E4265EEE9D0084735C /* MoleculeListProtocol.swift in Sources */,
|
||||
D29C559625C099630082E7D6 /* VideoDataManager.swift in Sources */,
|
||||
EA7AE5472C73C01A00107C74 /* CheckboxesModel.swift in Sources */,
|
||||
8D4687E2242E2DE400802879 /* ListFourColumnDataUsageListItemModel.swift in Sources */,
|
||||
D29E28DD23D7404C00ACEA85 /* ContainerHelper.swift in Sources */,
|
||||
EA6642912BCDA97300D81DC4 /* TileContainer.swift in Sources */,
|
||||
@ -3292,6 +3382,7 @@
|
||||
D28BA7432480284E00B75CB8 /* TabBar.swift in Sources */,
|
||||
AA3561AE24C96B9000452EB1 /* ListRightVariableRightCaretAllTextAndLinks.swift in Sources */,
|
||||
AA26850C244840AE00CE34CC /* HeadersH2TinyButton.swift in Sources */,
|
||||
EA5DBDAB2C35B6C500290DF8 /* FormFieldModel.swift in Sources */,
|
||||
011D95AB2405C553000E3791 /* FormItemProtocol.swift in Sources */,
|
||||
D21EE53C23AD3AD4003D1A30 /* NSLayoutConstraintAxis+Extension.swift in Sources */,
|
||||
0A25209824645B76000FA9F6 /* TextViewEntryFieldModel.swift in Sources */,
|
||||
@ -3319,6 +3410,7 @@
|
||||
D2169301251E51E7002A6324 /* SectionListTemplate.swift in Sources */,
|
||||
0A6682AA2435125F00AD3CA1 /* Styler.swift in Sources */,
|
||||
D264FAA7243FE13B00D98315 /* RadioBox.swift in Sources */,
|
||||
EA7AE54F2C74EB3700107C74 /* CalendarView.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@ -3533,7 +3625,7 @@
|
||||
FRAMEWORK_SEARCH_PATHS = "$(PROJECT_DIR)/../SharedFrameworks";
|
||||
INFOPLIST_FILE = MVMCoreUI/Info.plist;
|
||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
@ -3563,7 +3655,7 @@
|
||||
FRAMEWORK_SEARCH_PATHS = "$(PROJECT_DIR)/../SharedFrameworks";
|
||||
INFOPLIST_FILE = MVMCoreUI/Info.plist;
|
||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
|
||||
47
MVMCoreUI/Atomic/Actions/ActionUpdateVisibility.swift
Normal file
47
MVMCoreUI/Atomic/Actions/ActionUpdateVisibility.swift
Normal file
@ -0,0 +1,47 @@
|
||||
//
|
||||
// ActionToggleGone.swift
|
||||
// MVMCoreUI
|
||||
//
|
||||
// Created by Scott Pfeil on 8/8/24.
|
||||
// Copyright © 2024 Verizon Wireless. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MVMCore
|
||||
|
||||
public struct ActionUpdateVisibility: MVMCoreActionHandlerProtocol {
|
||||
|
||||
public init() {}
|
||||
|
||||
public func execute(with model: any MVMCore.ActionModelProtocol, delegateObject: MVMCore.DelegateObject?, additionalData: [AnyHashable : Any]?) async throws {
|
||||
guard let model = model as? ActionUpdateVisibilityModel,
|
||||
let delegate = (delegateObject as? MVMCoreUIDelegateObject)?.moleculeDelegate else { return }
|
||||
|
||||
let stateMap = model.targets.reduce(into: [String: ActionUpdateVisibilityModel.VisibilityTarget.VisibilityState]()) {
|
||||
$0[$1.id] = $1.state
|
||||
}
|
||||
|
||||
let updatedModels: [MoleculeModelProtocol] = delegate.getRootMolecules().reduceDepthFirstTraverse(options: .parentFirst, depth: 0, initialResult: [], nextPartialResult: { accumulator, model, depth in
|
||||
guard var model = model as? (GoneableProtocol & MoleculeModelProtocol),
|
||||
let state = stateMap[model.id] else { return accumulator }
|
||||
model.updateVisibility(state)
|
||||
return accumulator + [model]
|
||||
})
|
||||
|
||||
guard updatedModels.count > 0 else { return }
|
||||
await delegate.updateUI(for: updatedModels)
|
||||
}
|
||||
}
|
||||
|
||||
extension GoneableProtocol {
|
||||
mutating func updateVisibility(_ state: ActionUpdateVisibilityModel.VisibilityTarget.VisibilityState) {
|
||||
switch state {
|
||||
case .true:
|
||||
gone = false
|
||||
case .false:
|
||||
gone = true
|
||||
case .inverted:
|
||||
gone = !gone
|
||||
}
|
||||
}
|
||||
}
|
||||
42
MVMCoreUI/Atomic/Actions/ActionUpdateVisibilityModel.swift
Normal file
42
MVMCoreUI/Atomic/Actions/ActionUpdateVisibilityModel.swift
Normal file
@ -0,0 +1,42 @@
|
||||
//
|
||||
// ActionToggleGoneModel.swift
|
||||
// MVMCoreUI
|
||||
//
|
||||
// Created by Scott Pfeil on 8/8/24.
|
||||
// Copyright © 2024 Verizon Wireless. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MVMCore
|
||||
|
||||
public struct ActionUpdateVisibilityModel: ActionModelProtocol {
|
||||
|
||||
public static var identifier: String = "updateVisibility"
|
||||
public var actionType: String = ActionUpdateVisibilityModel.identifier
|
||||
public var extraParameters: JSONValueDictionary?
|
||||
public var analyticsData: JSONValueDictionary?
|
||||
public var targets: [VisibilityTarget]
|
||||
|
||||
public struct VisibilityTarget: Codable {
|
||||
|
||||
public enum VisibilityState: String, Codable {
|
||||
case `true`
|
||||
case `false`
|
||||
case inverted
|
||||
}
|
||||
|
||||
public var id: String
|
||||
public var state: VisibilityState
|
||||
|
||||
public init(id: String, state: VisibilityState) {
|
||||
self.id = id
|
||||
self.state = state
|
||||
}
|
||||
}
|
||||
|
||||
public init(targets: [VisibilityTarget], extraParameters: JSONValueDictionary? = nil, analyticsData: JSONValueDictionary? = nil) {
|
||||
self.targets = targets
|
||||
self.extraParameters = extraParameters
|
||||
self.analyticsData = analyticsData
|
||||
}
|
||||
}
|
||||
@ -38,7 +38,10 @@ open class ButtonGroup: VDS.ButtonGroup, VDSMoleculeViewProtocol {
|
||||
return PillButton.estimatedHeight(with: buttonModel, delegateObject)
|
||||
}
|
||||
|
||||
public func viewModelDidUpdate() {
|
||||
public func viewModelDidUpdate() {
|
||||
if let accessibilityIdentifier = viewModel.accessibilityIdentifier {
|
||||
self.accessibilityIdentifier = accessibilityIdentifier
|
||||
}
|
||||
surface = viewModel.surface
|
||||
isEnabled = viewModel.enabled
|
||||
alignment = viewModel.alignment
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MVMCore
|
||||
import VDS
|
||||
|
||||
public class ButtonGroupModel: ParentMoleculeModelProtocol {
|
||||
@ -17,6 +18,7 @@ public class ButtonGroupModel: ParentMoleculeModelProtocol {
|
||||
|
||||
public static var identifier: String = "buttonGroup"
|
||||
public var id: String = UUID().uuidString
|
||||
public var accessibilityIdentifier: String?
|
||||
public var backgroundColor: Color?
|
||||
public var children: [MoleculeModelProtocol] { buttons }
|
||||
|
||||
@ -38,6 +40,7 @@ public class ButtonGroupModel: ParentMoleculeModelProtocol {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case moleculeName
|
||||
case accessibilityIdentifier
|
||||
case backgroundColor
|
||||
case buttons
|
||||
case alignment
|
||||
@ -56,6 +59,7 @@ public class ButtonGroupModel: ParentMoleculeModelProtocol {
|
||||
required public init(from decoder: Decoder) throws {
|
||||
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
|
||||
id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString
|
||||
accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier)
|
||||
surface = try typeContainer.decodeIfPresent(Surface.self, forKey: .surface) ?? .light
|
||||
enabled = try typeContainer.decodeIfPresent(Bool.self, forKey: .enabled) ?? true
|
||||
buttons = try typeContainer.decodeModels(codingKey: .buttons)
|
||||
@ -70,6 +74,7 @@ public class ButtonGroupModel: ParentMoleculeModelProtocol {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(id, forKey: .id)
|
||||
try container.encode(moleculeName, forKey: .moleculeName)
|
||||
try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier)
|
||||
try container.encode(surface, forKey: .surface)
|
||||
try container.encode(enabled, forKey: .enabled)
|
||||
try container.encodeModels(buttons, forKey: .buttons)
|
||||
|
||||
@ -15,7 +15,8 @@ open class ImageButtonModel: ButtonModelProtocol, MoleculeModelProtocol, FormGro
|
||||
|
||||
public static var identifier: String = "imageButton"
|
||||
public var id: String = UUID().uuidString
|
||||
|
||||
public var accessibilityIdentifier: String?
|
||||
|
||||
public var backgroundColor: Color?
|
||||
public var image: ImageViewModel?
|
||||
|
||||
@ -45,6 +46,7 @@ open class ImageButtonModel: ButtonModelProtocol, MoleculeModelProtocol, FormGro
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case moleculeName
|
||||
case accessibilityIdentifier
|
||||
case image
|
||||
case backgroundColor
|
||||
case accessibilityText
|
||||
@ -64,6 +66,7 @@ open class ImageButtonModel: ButtonModelProtocol, MoleculeModelProtocol, FormGro
|
||||
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString
|
||||
accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier)
|
||||
backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor)
|
||||
image = try typeContainer.decodeIfPresent(ImageViewModel.self, forKey: .image)
|
||||
accessibilityText = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityText)
|
||||
@ -91,6 +94,7 @@ open class ImageButtonModel: ButtonModelProtocol, MoleculeModelProtocol, FormGro
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(id, forKey: .id)
|
||||
try container.encode(moleculeName, forKey: .moleculeName)
|
||||
try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier)
|
||||
try container.encodeIfPresent(image, forKey: .image)
|
||||
try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor)
|
||||
try container.encodeIfPresent(accessibilityText, forKey: .accessibilityText)
|
||||
|
||||
51
MVMCoreUI/Atomic/Atoms/FormFields/DatePickerEntryField.swift
Normal file
51
MVMCoreUI/Atomic/Atoms/FormFields/DatePickerEntryField.swift
Normal file
@ -0,0 +1,51 @@
|
||||
//
|
||||
// DatePickerEntryField.swift
|
||||
// MVMCoreUI
|
||||
//
|
||||
// Created by Matt Bruce on 8/20/24.
|
||||
// Copyright © 2024 Verizon Wireless. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import VDS
|
||||
|
||||
open class DatePickerEntryField: VDS.DatePicker, VDSMoleculeViewProtocol {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Public Properties
|
||||
//--------------------------------------------------
|
||||
public var viewModel: DatePickerEntryFieldModel!
|
||||
public var delegateObject: MVMCoreUIDelegateObject?
|
||||
public var additionalData: [AnyHashable : Any]?
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Public Methods
|
||||
//--------------------------------------------------
|
||||
open override func setup() {
|
||||
super.setup()
|
||||
//turn off internal required rule
|
||||
useRequiredRule = false
|
||||
|
||||
publisher(for: .valueChanged)
|
||||
.sink { [weak self] control in
|
||||
guard let self, let viewModel, isEnabled else { return }
|
||||
viewModel.selectedDate = control.selectedDate
|
||||
_ = FormValidator.validate(delegate: delegateObject?.formHolderDelegate)
|
||||
}.store(in: &subscribers)
|
||||
}
|
||||
|
||||
public func viewModelDidUpdate() {
|
||||
surface = viewModel.surface
|
||||
labelText = viewModel.title
|
||||
helperText = viewModel.feedback
|
||||
helperTextPlacement = viewModel.feedbackTextPlacement
|
||||
tooltipModel = viewModel.tooltip?.convertToVDSTooltipModel()
|
||||
transparentBackground = viewModel.transparentBackground
|
||||
width = viewModel.width
|
||||
selectedDate = viewModel.selectedDate
|
||||
calendarModel = viewModel.calendar.convertToVDSCalendarModel()
|
||||
FormValidator.setupValidation(for: viewModel, delegate: delegateObject?.formHolderDelegate)
|
||||
}
|
||||
|
||||
public func updateView(_ size: CGFloat) {}
|
||||
|
||||
}
|
||||
@ -0,0 +1,121 @@
|
||||
//
|
||||
// DatePickerEntryFieldModel.swift
|
||||
// MVMCoreUI
|
||||
//
|
||||
// Created by Matt Bruce on 8/20/24.
|
||||
// Copyright © 2024 Verizon Wireless. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import VDS
|
||||
|
||||
open class DatePickerEntryFieldModel: FormFieldModel {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Properties
|
||||
//--------------------------------------------------
|
||||
public override static var identifier: String { "datePicker" }
|
||||
|
||||
public var dateFormatter: DateFormatter = {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateStyle = .medium
|
||||
formatter.timeZone = NSTimeZone.system
|
||||
formatter.locale = .current
|
||||
formatter.formatterBehavior = .default
|
||||
return formatter
|
||||
}()
|
||||
|
||||
/// Update the property value to alter the format of how the date is presented.
|
||||
public var dateFormat: String = "MMM d, y" {
|
||||
didSet { dateFormatter.dateFormat = dateFormat }
|
||||
}
|
||||
|
||||
public var selectedDate: Date?
|
||||
public var calendar: CalendarViewModel = .init()
|
||||
public var title: String?
|
||||
public var feedback: String?
|
||||
public var feedbackTextPlacement: VDS.DatePicker.HelperTextPlacement = .bottom
|
||||
public var tooltip: TooltipModel?
|
||||
public var transparentBackground: Bool = false
|
||||
public var width: CGFloat?
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Keys
|
||||
//--------------------------------------------------
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case dateFormat
|
||||
case selectedDate
|
||||
case calendar
|
||||
case title
|
||||
case feedback
|
||||
case feedbackTextPlacement
|
||||
case tooltip
|
||||
case transparentBackground
|
||||
case width
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Form Validation
|
||||
//--------------------------------------------------
|
||||
|
||||
/// Returns the fieldValue of the selectedDate.
|
||||
public override func formFieldValue() -> AnyHashable? {
|
||||
guard let selectedDate, enabled else { return nil }
|
||||
return dateFormatter.string(from: selectedDate)
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Codec
|
||||
//--------------------------------------------------
|
||||
required public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
if let dateFormat = try container.decodeIfPresent(String.self, forKey: .dateFormat) {
|
||||
self.dateFormat = dateFormat
|
||||
dateFormatter.dateFormat = dateFormat
|
||||
}
|
||||
|
||||
if let date = try container.decodeIfPresent(String.self, forKey: .selectedDate) {
|
||||
selectedDate = calendar.dateFormatter.date(from: date)
|
||||
}
|
||||
|
||||
if let calendar = try container.decodeIfPresent(CalendarViewModel.self, forKey: .calendar) {
|
||||
self.calendar = calendar
|
||||
}
|
||||
|
||||
title = try container.decodeIfPresent(String.self, forKey: .title)
|
||||
feedback = try container.decodeIfPresent(String.self, forKey: .feedback)
|
||||
feedbackTextPlacement = try container.decodeIfPresent(VDS.EntryFieldBase.HelperTextPlacement.self, forKey: .feedbackTextPlacement) ?? .bottom
|
||||
|
||||
tooltip = try container.decodeIfPresent(TooltipModel.self, forKey: .tooltip)
|
||||
transparentBackground = try container.decodeIfPresent(Bool.self, forKey: .transparentBackground) ?? false
|
||||
width = try container.decodeIfPresent(CGFloat.self, forKey: .width)
|
||||
|
||||
try super.init(from: decoder)
|
||||
}
|
||||
|
||||
public override func encode(to encoder: Encoder) throws {
|
||||
try super.encode(to: encoder)
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encodeIfPresent(selectedDate, forKey: .selectedDate)
|
||||
try container.encode(calendar, forKey: .calendar)
|
||||
try container.encodeIfPresent(title, forKey: .title)
|
||||
try container.encodeIfPresent(feedback, forKey: .feedback)
|
||||
try container.encode(feedbackTextPlacement, forKey: .feedbackTextPlacement)
|
||||
try container.encodeIfPresent(tooltip, forKey: .tooltip)
|
||||
try container.encode(transparentBackground, forKey: .transparentBackground)
|
||||
try container.encodeIfPresent(width, forKey: .width)
|
||||
}
|
||||
|
||||
open override func isEqual(to model: any ModelComparisonProtocol) -> Bool {
|
||||
guard super.isEqual(to: model), let model = model as? Self else { return false }
|
||||
return dateFormat == model.dateFormat
|
||||
&& selectedDate == model.selectedDate
|
||||
&& calendar == model.calendar
|
||||
&& title == model.title
|
||||
&& feedback == model.feedback
|
||||
&& feedbackTextPlacement == model.feedbackTextPlacement
|
||||
&& tooltip == model.tooltip
|
||||
&& transparentBackground == model.transparentBackground
|
||||
&& width == model.width
|
||||
}
|
||||
}
|
||||
159
MVMCoreUI/Atomic/Atoms/FormFields/FormFieldModel.swift
Normal file
159
MVMCoreUI/Atomic/Atoms/FormFields/FormFieldModel.swift
Normal file
@ -0,0 +1,159 @@
|
||||
//
|
||||
// FormFieldModel.swift
|
||||
// MVMCoreUI
|
||||
//
|
||||
// Created by Matt Bruce on 7/3/24.
|
||||
// Copyright © 2024 Verizon Wireless. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import VDS
|
||||
|
||||
@objcMembers open class FormFieldModel: MoleculeModelProtocol, FormFieldProtocol, FormRuleWatcherFieldProtocol, UIUpdatableModelProtocol {
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Properties
|
||||
//--------------------------------------------------
|
||||
public class var identifier: String { "" }
|
||||
public var moleculeName: String { Self.identifier }
|
||||
|
||||
public var id: String = UUID().uuidString
|
||||
|
||||
public var backgroundColor: Color?
|
||||
public var accessibilityIdentifier: String?
|
||||
|
||||
public var enabled: Bool = true
|
||||
public var required: Bool = true
|
||||
public var readOnly: Bool = false
|
||||
public var showError: Bool = false
|
||||
public var errorMessage: String?
|
||||
public var initialErrorMessage: String?
|
||||
|
||||
public var fieldKey: String?
|
||||
public var groupName: String = FormValidator.defaultGroupName
|
||||
public var baseValue: AnyHashable?
|
||||
|
||||
public var inverted: Bool = false
|
||||
public var surface: Surface { inverted ? .dark : .light }
|
||||
|
||||
public var dynamicErrorMessage: String? {
|
||||
didSet {
|
||||
isValid = dynamicErrorMessage?.isEmpty ?? true
|
||||
updateUIDynamicError?()
|
||||
}
|
||||
}
|
||||
|
||||
public var isValid: Bool? = true {
|
||||
didSet { updateUI?() }
|
||||
}
|
||||
|
||||
/// Temporary binding mechanism for the view to update on enable changes.
|
||||
public var updateUI: ActionBlock?
|
||||
|
||||
// TODO: Remove once updateUI is fixed with isSelected
|
||||
public var updateUIDynamicError: ActionBlock?
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Initializer
|
||||
//--------------------------------------------------
|
||||
|
||||
public init() {}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Keys
|
||||
//--------------------------------------------------
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case moleculeName
|
||||
case accessibilityIdentifier
|
||||
case errorMessage
|
||||
case showError
|
||||
case enabled
|
||||
case readOnly
|
||||
case required
|
||||
case fieldKey
|
||||
case groupName
|
||||
case inverted
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Validation Methods
|
||||
//--------------------------------------------------
|
||||
|
||||
open func formFieldValue() -> AnyHashable? {
|
||||
fatalError("developer must implement")
|
||||
}
|
||||
|
||||
open func formFieldServerValue() -> AnyHashable? {
|
||||
return formFieldValue()
|
||||
}
|
||||
|
||||
public func setValidity(_ valid: Bool, errorMessage: String?) {
|
||||
|
||||
if let ruleErrorMessage = errorMessage, fieldKey != nil {
|
||||
self.errorMessage = ruleErrorMessage
|
||||
} else {
|
||||
self.errorMessage = initialErrorMessage
|
||||
}
|
||||
|
||||
isValid = valid
|
||||
updateUI?()
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Codable
|
||||
//--------------------------------------------------
|
||||
required public init(from decoder: Decoder) throws {
|
||||
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
|
||||
id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString
|
||||
accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier)
|
||||
errorMessage = try typeContainer.decodeIfPresent(String.self, forKey: .errorMessage)
|
||||
initialErrorMessage = errorMessage
|
||||
showError = try typeContainer.decodeIfPresent(Bool.self, forKey: .showError) ?? false
|
||||
enabled = try typeContainer.decodeIfPresent(Bool.self, forKey: .enabled) ?? true
|
||||
required = try typeContainer.decodeIfPresent(Bool.self, forKey: .required) ?? true
|
||||
readOnly = try typeContainer.decodeIfPresent(Bool.self, forKey: .readOnly) ?? false
|
||||
fieldKey = try typeContainer.decodeIfPresent(String.self, forKey: .fieldKey)
|
||||
groupName = try typeContainer.decodeIfPresent(String.self, forKey: .groupName) ?? FormValidator.defaultGroupName
|
||||
|
||||
if let inverted = try typeContainer.decodeIfPresent(Bool.self, forKey: .inverted) {
|
||||
self.inverted = inverted
|
||||
}
|
||||
}
|
||||
|
||||
open func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(id, forKey: .id)
|
||||
try container.encodeIfPresent(moleculeName, forKey: .moleculeName)
|
||||
try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier)
|
||||
try container.encodeIfPresent(errorMessage, forKey: .errorMessage)
|
||||
try container.encodeIfPresent(fieldKey, forKey: .fieldKey)
|
||||
try container.encodeIfPresent(groupName, forKey: .groupName)
|
||||
try container.encode(showError, forKey: .showError)
|
||||
try container.encode(readOnly, forKey: .readOnly)
|
||||
try container.encode(enabled, forKey: .enabled)
|
||||
try container.encode(required, forKey: .required)
|
||||
try container.encode(inverted, forKey: .inverted)
|
||||
}
|
||||
|
||||
open func isEqual(to model: any ModelComparisonProtocol) -> Bool {
|
||||
guard let model = model as? Self else { return false }
|
||||
return moleculeName == model.moleculeName
|
||||
&& enabled == model.enabled
|
||||
&& showError == model.showError
|
||||
&& errorMessage == model.errorMessage
|
||||
&& readOnly == model.readOnly
|
||||
&& required == model.required
|
||||
&& inverted == model.inverted
|
||||
&& fieldKey == model.fieldKey
|
||||
&& groupName == model.groupName
|
||||
&& accessibilityText == model.accessibilityText
|
||||
&& accessibilityIdentifier == model.accessibilityIdentifier
|
||||
&& accessibilityTraits == model.accessibilityTraits
|
||||
}
|
||||
}
|
||||
|
||||
extension FormFieldModel {
|
||||
public var isEnabled: Bool { enabled && !readOnly }
|
||||
}
|
||||
@ -12,13 +12,15 @@ import MVMCore
|
||||
@objcMembers public class TagModel: MoleculeModelProtocol {
|
||||
public static var identifier: String = "tag"
|
||||
public var id: String = UUID().uuidString
|
||||
|
||||
public var accessibilityIdentifier: String?
|
||||
|
||||
public var label: LabelModel
|
||||
public var action: ActionModelProtocol?
|
||||
public var backgroundColor: Color?
|
||||
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case accessibilityIdentifier
|
||||
case moleculeName
|
||||
case label
|
||||
case action
|
||||
@ -42,6 +44,7 @@ import MVMCore
|
||||
required public init(from decoder: Decoder) throws {
|
||||
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
|
||||
id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString
|
||||
accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier)
|
||||
label = try typeContainer.decode(LabelModel.self, forKey: .label)
|
||||
action = try typeContainer.decodeModelIfPresent(codingKey: .action)
|
||||
backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor)
|
||||
@ -50,6 +53,7 @@ import MVMCore
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(id, forKey: .id)
|
||||
try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier)
|
||||
try container.encode(moleculeName, forKey: .moleculeName)
|
||||
try container.encode(label, forKey: .label)
|
||||
try container.encodeModelIfPresent(action, forKey: .action)
|
||||
|
||||
@ -12,12 +12,14 @@ import MVMCore
|
||||
@objcMembers public class TagsModel: MoleculeModelProtocol {
|
||||
public static var identifier: String = "tags"
|
||||
public var id: String = UUID().uuidString
|
||||
|
||||
public var accessibilityIdentifier: String?
|
||||
|
||||
public var backgroundColor: Color?
|
||||
public var tags: [TagModel]
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case accessibilityIdentifier
|
||||
case moleculeName
|
||||
case backgroundColor
|
||||
case tags
|
||||
@ -34,6 +36,7 @@ import MVMCore
|
||||
required public init(from decoder: Decoder) throws {
|
||||
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
|
||||
id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString
|
||||
accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier)
|
||||
tags = try typeContainer.decode([TagModel].self, forKey: .tags)
|
||||
backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor)
|
||||
}
|
||||
@ -41,6 +44,7 @@ import MVMCore
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(id, forKey: .id)
|
||||
try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier)
|
||||
try container.encode(moleculeName, forKey: .moleculeName)
|
||||
try container.encode(tags, forKey: .tags)
|
||||
try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor)
|
||||
|
||||
@ -345,9 +345,8 @@ import UIKit
|
||||
|
||||
numberOfDigits = model.digits
|
||||
|
||||
if let entryType = model.type {
|
||||
setAsSecureTextEntry(entryType == .secure || entryType == .password)
|
||||
}
|
||||
let entryType = model.type
|
||||
setAsSecureTextEntry(entryType == .secure || entryType == .password)
|
||||
|
||||
let observingDelegate = delegateObject?.observingTextFieldDelegate ?? self
|
||||
|
||||
|
||||
@ -7,19 +7,79 @@
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import VDS
|
||||
|
||||
open class ItemDropdownEntryField: VDS.DropdownSelect, VDSMoleculeViewProtocol, ObservingTextFieldDelegate {
|
||||
//------------------------------------------------------
|
||||
// MARK: - Properties
|
||||
//------------------------------------------------------
|
||||
open var viewModel: ItemDropdownEntryFieldModel!
|
||||
open var delegateObject: MVMCoreUIDelegateObject?
|
||||
open var additionalData: [AnyHashable : Any]?
|
||||
|
||||
open class ItemDropdownEntryField: BaseItemPickerEntryField {
|
||||
// Form Validation
|
||||
var fieldKey: String?
|
||||
var fieldValue: JSONValue?
|
||||
var groupName: String?
|
||||
|
||||
open var pickerData: [String] = [] {
|
||||
didSet {
|
||||
options = pickerData.compactMap({ DropdownOptionModel(text: $0) })
|
||||
}
|
||||
}
|
||||
|
||||
private var isEditting: Bool = false
|
||||
|
||||
//override for to deal with getting the
|
||||
//old selectedItem to pass down to the observeDropdownChange
|
||||
open override var selectId: Int? {
|
||||
didSet {
|
||||
guard let observeDropdownChange, let selectedItem else { return }
|
||||
var oldSelectedItem: DropdownOptionModel?
|
||||
if let oldValue {
|
||||
oldSelectedItem = options[oldValue]
|
||||
}
|
||||
observeDropdownChange(oldSelectedItem?.text, selectedItem.text)
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Properties
|
||||
//--------------------------------------------------
|
||||
public var isValid: Bool = true
|
||||
|
||||
/// Closure passed here will run as picker changes items.
|
||||
public var observeDropdownChange: ((_ oldValue: String?, _ newValue: String) -> ())?
|
||||
|
||||
open var pickerData: [String] = []
|
||||
/// Closure passed here will run upon dismissing the selection picker.
|
||||
public var observeDropdownSelection: ((_ newValue: String) -> ())?
|
||||
|
||||
public var itemDropdownEntryFieldModel: ItemDropdownEntryFieldModel? {
|
||||
model as? ItemDropdownEntryFieldModel
|
||||
/// When selecting for first responder, allow initial selected value to appear in empty text field.
|
||||
public var setInitialValueInTextField = true
|
||||
|
||||
open override var errorText: String? {
|
||||
get {
|
||||
viewModel.dynamicErrorMessage ?? viewModel.errorMessage
|
||||
}
|
||||
set {}
|
||||
}
|
||||
//--------------------------------------------------
|
||||
// MARK: - Delegate Properties
|
||||
//--------------------------------------------------
|
||||
|
||||
/// The delegate and block for validation. Validates if the text that the user has entered.
|
||||
public weak var observingTextFieldDelegate: ObservingTextFieldDelegate?
|
||||
|
||||
/// If you're using a ViewController, you must set this to it
|
||||
open weak var uiTextFieldDelegate: UITextFieldDelegate? {
|
||||
get { dropdownField.delegate }
|
||||
set { dropdownField.delegate = newValue }
|
||||
}
|
||||
|
||||
@objc public func dismissFieldInput(_ sender: Any?) {
|
||||
_ = resignFirstResponder()
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Initializers
|
||||
//--------------------------------------------------
|
||||
@ -28,7 +88,7 @@ open class ItemDropdownEntryField: BaseItemPickerEntryField {
|
||||
super.init(frame: frame)
|
||||
}
|
||||
|
||||
@objc public convenience init() {
|
||||
@objc public convenience required init() {
|
||||
self.init(frame: .zero)
|
||||
}
|
||||
|
||||
@ -40,76 +100,133 @@ open class ItemDropdownEntryField: BaseItemPickerEntryField {
|
||||
@objc required public init?(coder: NSCoder) {
|
||||
fatalError("ItemDropdownEntryField init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
required public init(model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
|
||||
super.init(model: model, delegateObject, additionalData)
|
||||
}
|
||||
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Methods
|
||||
//--------------------------------------------------
|
||||
open override func setup() {
|
||||
super.setup()
|
||||
useRequiredRule = false
|
||||
|
||||
publisher(for: .valueChanged)
|
||||
.sink { [weak self] control in
|
||||
guard let self, let selectedItem, let viewModel else { return }
|
||||
viewModel.selectedIndex = control.selectId
|
||||
observeDropdownSelection?(selectedItem.text)
|
||||
_ = FormValidator.validate(delegate: delegateObject?.formHolderDelegate)
|
||||
}.store(in: &subscribers)
|
||||
|
||||
dropdownField
|
||||
.publisher(for: .editingDidBegin)
|
||||
.sink { [weak self] textField in
|
||||
guard let self else { return }
|
||||
isEditting = true
|
||||
setInitialValueFromPicker()
|
||||
}.store(in: &subscribers)
|
||||
|
||||
dropdownField
|
||||
.publisher(for: .editingDidEnd)
|
||||
.sink { [weak self] textField in
|
||||
guard let self else { return }
|
||||
isEditting = false
|
||||
_ = FormValidator.validate(delegate: delegateObject?.formHolderDelegate)
|
||||
if let viewModel, let valid = viewModel.isValid {
|
||||
updateValidation(valid)
|
||||
}
|
||||
performDropdownAction()
|
||||
}.store(in: &subscribers)
|
||||
}
|
||||
|
||||
public func viewModelDidUpdate() {
|
||||
pickerData = viewModel.options
|
||||
showInlineLabel = viewModel.showInlineLabel
|
||||
helperTextPlacement = viewModel.feedbackTextPlacement
|
||||
labelText = viewModel.title
|
||||
helperText = viewModel.feedback
|
||||
isEnabled = viewModel.enabled
|
||||
isReadOnly = viewModel.readOnly
|
||||
isRequired = viewModel.required
|
||||
tooltipModel = viewModel.tooltip?.convertToVDSTooltipModel()
|
||||
width = viewModel.width
|
||||
transparentBackground = viewModel.transparentBackground
|
||||
|
||||
if let index = viewModel.selectedIndex {
|
||||
selectId = index
|
||||
optionsPicker.selectRow(index, inComponent: 0, animated: false)
|
||||
pickerView(optionsPicker, didSelectRow: index, inComponent: 0)
|
||||
}
|
||||
|
||||
if (viewModel.selected ?? false) && !viewModel.wasInitiallySelected {
|
||||
|
||||
viewModel.wasInitiallySelected = true
|
||||
isEditting = true
|
||||
}
|
||||
|
||||
FormValidator.setupValidation(for: viewModel, delegate: delegateObject?.formHolderDelegate)
|
||||
if isEditting {
|
||||
DispatchQueue.main.async {
|
||||
_ = self.becomeFirstResponder()
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.updateUI = {
|
||||
MVMCoreDispatchUtility.performBlock(onMainThread: { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
if isEditting {
|
||||
updateValidation(viewModel.isValid ?? true)
|
||||
|
||||
} else if viewModel.isValid ?? true && showError {
|
||||
showError = false
|
||||
}
|
||||
isEnabled = viewModel.enabled
|
||||
})
|
||||
}
|
||||
|
||||
viewModel.updateUIDynamicError = {
|
||||
MVMCoreDispatchUtility.performBlock(onMainThread: { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
let validState = viewModel.isValid ?? false
|
||||
if !validState && viewModel.shouldClearText {
|
||||
selectId = nil
|
||||
viewModel.shouldClearText = false
|
||||
}
|
||||
updateValidation(validState)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public func updateView(_ size: CGFloat) { }
|
||||
|
||||
/// Sets the textField with the first value of the available picker data.
|
||||
@objc private func setInitialValueFromPicker() {
|
||||
private func setInitialValueFromPicker() {
|
||||
|
||||
guard !pickerData.isEmpty else { return }
|
||||
|
||||
if setInitialValueInTextField {
|
||||
let pickerIndex = pickerView.selectedRow(inComponent: 0)
|
||||
itemDropdownEntryFieldModel?.selectedIndex = pickerIndex
|
||||
observeDropdownChange?(text, pickerData[pickerIndex])
|
||||
text = pickerData[pickerIndex]
|
||||
let pickerIndex = optionsPicker.selectedRow(inComponent: 0)
|
||||
viewModel?.selectedIndex = pickerIndex
|
||||
selectId = pickerIndex
|
||||
}
|
||||
}
|
||||
|
||||
@objc override func startEditing() {
|
||||
super.startEditing()
|
||||
|
||||
setInitialValueFromPicker()
|
||||
private func performDropdownAction() {
|
||||
guard let actionModel = viewModel.action,
|
||||
!dropdownField.isFirstResponder
|
||||
else { return }
|
||||
MVMCoreUIActionHandler.performActionUnstructured(with: actionModel, sourceModel: viewModel, additionalData: additionalData, delegateObject: delegateObject)
|
||||
}
|
||||
|
||||
@objc override func endInputing() {
|
||||
super.endInputing()
|
||||
private func updateValidation(_ isValid: Bool) {
|
||||
let previousValidity = self.isValid
|
||||
self.isValid = isValid
|
||||
|
||||
guard !pickerData.isEmpty else { return }
|
||||
|
||||
observeDropdownSelection?(pickerData[pickerView.selectedRow(inComponent: 0)])
|
||||
}
|
||||
|
||||
public override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
|
||||
super.set(with: model, delegateObject, additionalData)
|
||||
|
||||
guard let model = model as? ItemDropdownEntryFieldModel else { return }
|
||||
|
||||
pickerData = model.options
|
||||
|
||||
if let index = model.selectedIndex {
|
||||
self.pickerView.selectRow(index, inComponent: 0, animated: false)
|
||||
self.pickerView(pickerView, didSelectRow: index, inComponent: 0)
|
||||
if previousValidity && !isValid {
|
||||
showError = true
|
||||
} else if (!previousValidity && isValid) {
|
||||
showError = false
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Picker Delegate
|
||||
//--------------------------------------------------
|
||||
|
||||
@objc public override func numberOfComponents(in pickerView: UIPickerView) -> Int { 1 }
|
||||
|
||||
@objc public override func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
|
||||
pickerData.count
|
||||
}
|
||||
|
||||
@objc public func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
|
||||
guard !pickerData.isEmpty else { return nil }
|
||||
|
||||
return pickerData[row]
|
||||
}
|
||||
|
||||
@objc public func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
|
||||
guard !pickerData.isEmpty else { return }
|
||||
|
||||
itemDropdownEntryFieldModel?.selectedIndex = row
|
||||
observeDropdownChange?(text, pickerData[row])
|
||||
text = pickerData[row]
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,16 +5,19 @@
|
||||
// Created by Kevin Christiano on 1/22/20.
|
||||
// Copyright © 2020 Verizon Wireless. All rights reserved.
|
||||
//
|
||||
import VDS
|
||||
|
||||
@objcMembers open class ItemDropdownEntryFieldModel: BaseItemPickerEntryFieldModel {
|
||||
@objcMembers open class ItemDropdownEntryFieldModel: TextEntryFieldModel {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Properties
|
||||
//--------------------------------------------------
|
||||
|
||||
public override class var identifier: String { "dropDown" }
|
||||
|
||||
public var action: ActionModelProtocol?
|
||||
public var options: [String] = []
|
||||
public var selectedIndex: Int?
|
||||
public var showInlineLabel: Bool = false
|
||||
public var feedbackTextPlacement: VDS.DropdownSelect.HelperTextPlacement = .bottom
|
||||
|
||||
public init(with options: [String], selectedIndex: Int? = nil) {
|
||||
self.options = options
|
||||
@ -42,6 +45,9 @@
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case options
|
||||
case selectedIndex
|
||||
case action
|
||||
case showInlineLabel
|
||||
case feedbackTextPlacement
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
@ -58,6 +64,9 @@
|
||||
self.selectedIndex = selectedIndex
|
||||
baseValue = options.indices.contains(selectedIndex) ? options[selectedIndex] : nil
|
||||
}
|
||||
showInlineLabel = try typeContainer.decodeIfPresent(Bool.self, forKey: .showInlineLabel) ?? false
|
||||
feedbackTextPlacement = try typeContainer.decodeIfPresent(VDS.EntryFieldBase.HelperTextPlacement.self, forKey: .feedbackTextPlacement) ?? .bottom
|
||||
action = try typeContainer.decodeModelIfPresent(codingKey: .action)
|
||||
}
|
||||
|
||||
public override func encode(to encoder: Encoder) throws {
|
||||
@ -65,5 +74,17 @@
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(options, forKey: .options)
|
||||
try container.encodeIfPresent(selectedIndex, forKey: .selectedIndex)
|
||||
try container.encode(showInlineLabel, forKey: .showInlineLabel)
|
||||
try container.encode(feedbackTextPlacement, forKey: .feedbackTextPlacement)
|
||||
try container.encodeModelIfPresent(action, forKey: .action)
|
||||
}
|
||||
|
||||
open override func isEqual(to model: any ModelComparisonProtocol) -> Bool {
|
||||
guard super.isEqual(to: model), let model = model as? Self else { return false }
|
||||
return options == model.options
|
||||
&& selectedIndex == model.selectedIndex
|
||||
&& showInlineLabel == model.showInlineLabel
|
||||
&& feedbackTextPlacement == model.feedbackTextPlacement
|
||||
&& action.isEqual(to: model.action)
|
||||
}
|
||||
}
|
||||
|
||||
@ -315,7 +315,9 @@ import UIKit
|
||||
self.showError = false
|
||||
}
|
||||
self.isEnabled = model.enabled
|
||||
self.text = model.text
|
||||
if let text = model.text, !text.isEmpty {
|
||||
self.text = model.text
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -9,79 +9,38 @@
|
||||
import Foundation
|
||||
|
||||
|
||||
@objcMembers open class EntryFieldModel: MoleculeModelProtocol, FormFieldProtocol, FormRuleWatcherFieldProtocol, UIUpdatableModelProtocol, ClearableModelProtocol {
|
||||
@objcMembers open class EntryFieldModel: FormFieldModel, ClearableModelProtocol {
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Properties
|
||||
//--------------------------------------------------
|
||||
|
||||
public class var identifier: String { "" }
|
||||
public var id: String = UUID().uuidString
|
||||
|
||||
public var backgroundColor: Color?
|
||||
public var accessibilityIdentifier: String?
|
||||
public var shouldClearText: Bool = false
|
||||
public var dynamicErrorMessage: String? {
|
||||
didSet {
|
||||
isValid = dynamicErrorMessage?.isEmpty ?? true
|
||||
updateUIDynamicError?()
|
||||
}
|
||||
}
|
||||
public var errorMessage: String?
|
||||
public var errorTextColor: Color?
|
||||
public var enabled: Bool = true
|
||||
public var required: Bool = true
|
||||
public var readOnly: Bool = false
|
||||
public var showError: Bool?
|
||||
public var hideBorders = false
|
||||
public var locked: Bool?
|
||||
public var selected: Bool?
|
||||
public var text: String?
|
||||
public var fieldKey: String?
|
||||
public var groupName: String = FormValidator.defaultGroupName
|
||||
public var baseValue: AnyHashable?
|
||||
public var wasInitiallySelected: Bool = false
|
||||
public var text: String?
|
||||
public var title: String?
|
||||
public var feedback: String?
|
||||
public var shouldMaskRecordedView: Bool? = true
|
||||
|
||||
//used to drive the EntryFieldView UI
|
||||
public var titleStateLabel: FormLabelModel
|
||||
public var feedbackStateLabel: FormLabelModel
|
||||
|
||||
public var isValid: Bool? = true {
|
||||
didSet { updateUI?() }
|
||||
}
|
||||
|
||||
/// Temporary binding mechanism for the view to update on enable changes.
|
||||
public var updateUI: ActionBlock?
|
||||
|
||||
// TODO: Remove once updateUI is fixed with isSelected
|
||||
public var updateUIDynamicError: ActionBlock?
|
||||
public var titleStateLabel = FormLabelModel(text: "")
|
||||
public var feedbackStateLabel = FormLabelModel(text: "")
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Keys
|
||||
//--------------------------------------------------
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case moleculeName
|
||||
case backgroundColor
|
||||
case accessibilityIdentifier
|
||||
case title
|
||||
case enabled
|
||||
case readOnly
|
||||
case feedback
|
||||
case errorMessage
|
||||
case errorTextColor
|
||||
case locked
|
||||
case selected
|
||||
case showError
|
||||
case hideBorders
|
||||
case text
|
||||
case fieldKey
|
||||
case groupName
|
||||
case required
|
||||
case shouldMaskRecordedView
|
||||
}
|
||||
|
||||
@ -92,7 +51,7 @@ import Foundation
|
||||
// MARK: - Validation Methods
|
||||
//--------------------------------------------------
|
||||
|
||||
public func formFieldValue() -> AnyHashable? {
|
||||
open override func formFieldValue() -> AnyHashable? {
|
||||
guard enabled else { return nil }
|
||||
|
||||
if dynamicErrorMessage != nil {
|
||||
@ -100,30 +59,15 @@ import Foundation
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
open func formFieldServerValue() -> AnyHashable? {
|
||||
return formFieldValue()
|
||||
}
|
||||
|
||||
public func setValidity(_ valid: Bool, errorMessage: String?) {
|
||||
|
||||
if let ruleErrorMessage = errorMessage, fieldKey != nil {
|
||||
self.errorMessage = ruleErrorMessage
|
||||
}
|
||||
|
||||
self.isValid = valid
|
||||
updateUI?()
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Initializers
|
||||
//--------------------------------------------------
|
||||
|
||||
public init(with text: String) {
|
||||
super.init()
|
||||
self.text = text
|
||||
baseValue = text
|
||||
self.titleStateLabel = FormLabelModel(text: "")
|
||||
self.feedbackStateLabel = FormLabelModel(text: "")
|
||||
setDefaults()
|
||||
}
|
||||
|
||||
@ -131,7 +75,7 @@ import Foundation
|
||||
// MARK: - Initializers
|
||||
//--------------------------------------------------
|
||||
public func clear() {
|
||||
self.text = ""
|
||||
text = ""
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
@ -139,55 +83,46 @@ import Foundation
|
||||
//--------------------------------------------------
|
||||
|
||||
required public init(from decoder: Decoder) throws {
|
||||
try super.init(from: decoder)
|
||||
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
|
||||
id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString
|
||||
backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor)
|
||||
accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier)
|
||||
title = try typeContainer.decodeIfPresent(String.self, forKey: .title)
|
||||
feedback = try typeContainer.decodeIfPresent(String.self, forKey: .feedback)
|
||||
errorMessage = try typeContainer.decodeIfPresent(String.self, forKey: .errorMessage)
|
||||
errorTextColor = try typeContainer.decodeIfPresent(Color.self, forKey: .errorTextColor)
|
||||
enabled = try typeContainer.decodeIfPresent(Bool.self, forKey: .enabled) ?? true
|
||||
required = try typeContainer.decodeIfPresent(Bool.self, forKey: .required) ?? true
|
||||
readOnly = try typeContainer.decodeIfPresent(Bool.self, forKey: .readOnly) ?? false
|
||||
locked = try typeContainer.decodeIfPresent(Bool.self, forKey: .locked)
|
||||
selected = try typeContainer.decodeIfPresent(Bool.self, forKey: .selected)
|
||||
text = try typeContainer.decodeIfPresent(String.self, forKey: .text)
|
||||
hideBorders = try typeContainer.decodeIfPresent(Bool.self, forKey: .hideBorders) ?? false
|
||||
baseValue = text
|
||||
fieldKey = try typeContainer.decodeIfPresent(String.self, forKey: .fieldKey)
|
||||
shouldMaskRecordedView = try typeContainer.decodeIfPresent(Bool.self, forKey: .shouldMaskRecordedView) ?? shouldMaskRecordedView
|
||||
if let groupName = try typeContainer.decodeIfPresent(String.self, forKey: .groupName) {
|
||||
self.groupName = groupName
|
||||
}
|
||||
self.titleStateLabel = FormLabelModel(text: title ?? "")
|
||||
self.feedbackStateLabel = FormLabelModel(model: LabelModel(text: feedback ?? "",
|
||||
titleStateLabel = FormLabelModel(text: title ?? "")
|
||||
feedbackStateLabel = FormLabelModel(model: LabelModel(text: feedback ?? "",
|
||||
fontStyle: FormLabelModel.defaultFontStyle,
|
||||
textColor: Color(uiColor: .mvmCoolGray6)))
|
||||
setDefaults()
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
open override func encode(to encoder: Encoder) throws {
|
||||
try super.encode(to: encoder)
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(id, forKey: .id)
|
||||
try container.encodeIfPresent(moleculeName, forKey: .moleculeName)
|
||||
try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor)
|
||||
try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier)
|
||||
try container.encodeIfPresent(title, forKey: .title)
|
||||
try container.encodeIfPresent(feedback, forKey: .feedback)
|
||||
try container.encodeIfPresent(text, forKey: .text)
|
||||
try container.encodeIfPresent(locked, forKey: .locked)
|
||||
try container.encodeIfPresent(showError, forKey: .showError)
|
||||
try container.encodeIfPresent(selected, forKey: .selected)
|
||||
try container.encodeIfPresent(errorTextColor, forKey: .errorTextColor)
|
||||
try container.encodeIfPresent(errorMessage, forKey: .errorMessage)
|
||||
try container.encodeIfPresent(fieldKey, forKey: .fieldKey)
|
||||
try container.encodeIfPresent(groupName, forKey: .groupName)
|
||||
|
||||
try container.encode(readOnly, forKey: .readOnly)
|
||||
try container.encode(enabled, forKey: .enabled)
|
||||
try container.encode(required, forKey: .required)
|
||||
try container.encode(hideBorders, forKey: .hideBorders)
|
||||
try container.encode(shouldMaskRecordedView, forKey: .shouldMaskRecordedView)
|
||||
}
|
||||
|
||||
open override func isEqual(to model: any ModelComparisonProtocol) -> Bool {
|
||||
guard super.isEqual(to: model), let model = model as? Self else { return false }
|
||||
return selected == model.selected
|
||||
&& title == model.title
|
||||
&& feedback == model.feedback
|
||||
&& errorTextColor == model.errorTextColor
|
||||
&& locked == model.locked
|
||||
&& hideBorders == model.hideBorders
|
||||
&& text == model.text
|
||||
&& shouldMaskRecordedView == model.shouldMaskRecordedView
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,347 @@
|
||||
//
|
||||
// InputEntryField.swift
|
||||
// MVMCoreUI
|
||||
//
|
||||
// Created by Matt Bruce on 7/16/24.
|
||||
// Copyright © 2024 Verizon Wireless. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import VDS
|
||||
|
||||
@objcMembers open class InputEntryField: VDS.InputField, VDSMoleculeViewProtocol, ObservingTextFieldDelegate, ViewMaskingProtocol {
|
||||
|
||||
//------------------------------------------------------
|
||||
// MARK: - Properties
|
||||
//------------------------------------------------------
|
||||
open var viewModel: TextEntryFieldModel!
|
||||
open var delegateObject: MVMCoreUIDelegateObject?
|
||||
open var additionalData: [AnyHashable : Any]?
|
||||
|
||||
// Form Validation
|
||||
var fieldKey: String?
|
||||
var fieldValue: JSONValue?
|
||||
var groupName: String?
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Stored Properties
|
||||
//--------------------------------------------------
|
||||
public var isValid: Bool = true
|
||||
|
||||
/// Holds a reference to the delegating class so this class can internally influence the TextField behavior as well.
|
||||
private weak var proprietorTextDelegate: UITextFieldDelegate?
|
||||
|
||||
private var isEditting: Bool = false {
|
||||
didSet {
|
||||
viewModel?.selected = isEditting
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Stored Properties
|
||||
//--------------------------------------------------
|
||||
|
||||
private var observingForChange: Bool = false
|
||||
|
||||
/// Validate when user resigns editing. Default: true
|
||||
open var validateWhenDoneEditing: Bool = true
|
||||
|
||||
open var shouldMaskWhileRecording: Bool {
|
||||
return viewModel?.shouldMaskRecordedView ?? false
|
||||
}
|
||||
//--------------------------------------------------
|
||||
// MARK: - Computed Properties
|
||||
//--------------------------------------------------
|
||||
|
||||
/// The text of this TextField.
|
||||
open override var text: String? {
|
||||
didSet {
|
||||
viewModel?.text = text
|
||||
}
|
||||
}
|
||||
|
||||
open override var errorText: String? {
|
||||
get {
|
||||
viewModel.dynamicErrorMessage ?? viewModel.errorMessage
|
||||
}
|
||||
set {}
|
||||
}
|
||||
|
||||
/// Placeholder access for the TextField.
|
||||
public var placeholder: String? {
|
||||
get { textField.placeholder }
|
||||
set { textField.placeholder = newValue }
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Delegate Properties
|
||||
//--------------------------------------------------
|
||||
/// The delegate and block for validation. Validates if the text that the user has entered.
|
||||
public weak var observingTextFieldDelegate: ObservingTextFieldDelegate?
|
||||
|
||||
/// If you're using a ViewController, you must set this to it
|
||||
open weak var uiTextFieldDelegate: UITextFieldDelegate?
|
||||
{
|
||||
get { textField.delegate }
|
||||
set {
|
||||
textField.delegate = self
|
||||
proprietorTextDelegate = newValue
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Lifecycle
|
||||
//--------------------------------------------------
|
||||
open override func setup() {
|
||||
super.setup()
|
||||
//turn off internal required rule
|
||||
useRequiredRule = false
|
||||
|
||||
publisher(for: .valueChanged)
|
||||
.sink { [weak self] control in
|
||||
guard let self, let viewModel else { return }
|
||||
_ = FormValidator.validate(delegate: delegateObject?.formHolderDelegate)
|
||||
if (viewModel.type == .email) {
|
||||
// remove spaces (either user entered Or auto-correct suggestion) for the email field
|
||||
text = textField.text?.replacingOccurrences(of: " ", with: "")
|
||||
}
|
||||
}.store(in: &subscribers)
|
||||
|
||||
textField
|
||||
.publisher(for: .editingDidBegin)
|
||||
.sink { [weak self] textView in
|
||||
guard let self else { return }
|
||||
isEditting = true
|
||||
if let viewModel, viewModel.clearTextOnTap {
|
||||
text = ""
|
||||
}
|
||||
}.store(in: &subscribers)
|
||||
|
||||
textField
|
||||
.publisher(for: .editingDidEnd)
|
||||
.sink { [weak self] textView in
|
||||
guard let self else { return }
|
||||
isEditting = false
|
||||
if let viewModel, validateWhenDoneEditing, let valid = viewModel.isValid {
|
||||
updateValidation(valid)
|
||||
}
|
||||
regexTextFieldOutputIfAvailable()
|
||||
|
||||
}.store(in: &subscribers)
|
||||
|
||||
}
|
||||
|
||||
open override func updateView() {
|
||||
super.updateView()
|
||||
|
||||
if let viewModel {
|
||||
switch viewModel.type {
|
||||
case .secure:
|
||||
textField.isSecureTextEntry = true
|
||||
textField.shouldMaskWhileRecording = true
|
||||
|
||||
case .numberSecure:
|
||||
textField.isSecureTextEntry = true
|
||||
textField.shouldMaskWhileRecording = true
|
||||
textField.keyboardType = .numberPad
|
||||
|
||||
case .email:
|
||||
textField.keyboardType = .emailAddress
|
||||
|
||||
case .securityCode, .creditCard, .password:
|
||||
textField.shouldMaskWhileRecording = true
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// Override the preset keyboard set in type.
|
||||
if let keyboardType = viewModel.assignKeyboardType() {
|
||||
textField.keyboardType = keyboardType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open func viewModelDidUpdate() {
|
||||
|
||||
fieldType = viewModel.type.toVDSFieldType()
|
||||
text = viewModel.text
|
||||
placeholder = viewModel.placeholder
|
||||
|
||||
labelText = viewModel.title
|
||||
helperText = viewModel.feedback
|
||||
isEnabled = viewModel.enabled
|
||||
isReadOnly = viewModel.readOnly
|
||||
isRequired = viewModel.required
|
||||
tooltipModel = viewModel.tooltip?.convertToVDSTooltipModel()
|
||||
width = viewModel.width
|
||||
transparentBackground = viewModel.transparentBackground
|
||||
|
||||
containerView.accessibilityIdentifier = model.accessibilityIdentifier
|
||||
textField.textAlignment = viewModel.textAlignment
|
||||
textField.enableClipboardActions = viewModel.enableClipboardActions
|
||||
textField.placeholder = viewModel.placeholder ?? ""
|
||||
uiTextFieldDelegate = delegateObject?.uiTextFieldDelegate
|
||||
observingTextFieldDelegate = delegateObject?.observingTextFieldDelegate
|
||||
|
||||
if (viewModel.selected ?? false) && !viewModel.wasInitiallySelected {
|
||||
|
||||
viewModel.wasInitiallySelected = true
|
||||
isEditting = true
|
||||
}
|
||||
|
||||
viewModel.rules = rules
|
||||
|
||||
FormValidator.setupValidation(for: viewModel, delegate: delegateObject?.formHolderDelegate)
|
||||
|
||||
if isEditting {
|
||||
DispatchQueue.main.async {
|
||||
_ = self.becomeFirstResponder()
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.updateUI = {
|
||||
MVMCoreDispatchUtility.performBlock(onMainThread: { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
if isEditting {
|
||||
updateValidation(viewModel.isValid ?? true)
|
||||
|
||||
} else if viewModel.isValid ?? true && showError {
|
||||
showError = false
|
||||
}
|
||||
isEnabled = viewModel.enabled
|
||||
})
|
||||
}
|
||||
|
||||
viewModel.updateUIDynamicError = {
|
||||
MVMCoreDispatchUtility.performBlock(onMainThread: { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
let validState = viewModel.isValid ?? false
|
||||
if !validState && viewModel.shouldClearText {
|
||||
text = ""
|
||||
viewModel.shouldClearText = false
|
||||
}
|
||||
updateValidation(validState)
|
||||
})
|
||||
}
|
||||
|
||||
//Added to override text when view is reloaded.
|
||||
if let text = viewModel.text, !text.isEmpty {
|
||||
regexTextFieldOutputIfAvailable()
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Observing for Change (TextFieldDelegate)
|
||||
//--------------------------------------------------
|
||||
@objc public func setBothTextDelegates(to delegate: (UITextFieldDelegate & ObservingTextFieldDelegate)?) {
|
||||
observingTextFieldDelegate = delegate
|
||||
uiTextFieldDelegate = delegate
|
||||
}
|
||||
|
||||
func regexTextFieldOutputIfAvailable() {
|
||||
|
||||
if let regex = viewModel?.displayFormat,
|
||||
let mask = viewModel?.displayMask,
|
||||
let finalText = text {
|
||||
|
||||
let range = NSRange(finalText.startIndex..., in: finalText)
|
||||
|
||||
if let regex = try? NSRegularExpression(pattern: regex) {
|
||||
let maskedText = regex.stringByReplacingMatches(in: finalText,
|
||||
range: range,
|
||||
withTemplate: mask)
|
||||
textField.text = maskedText
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc public func dismissFieldInput(_ sender: Any?) {
|
||||
_ = resignFirstResponder()
|
||||
}
|
||||
|
||||
private func updateValidation(_ isValid: Bool) {
|
||||
let previousValidity = self.isValid
|
||||
self.isValid = isValid
|
||||
|
||||
if previousValidity && !isValid {
|
||||
showError = true
|
||||
observingTextFieldDelegate?.isValid?(textfield: self)
|
||||
} else if (!previousValidity && isValid) {
|
||||
showError = false
|
||||
observingTextFieldDelegate?.isInvalid?(textfield: self)
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - MoleculeViewProtocol
|
||||
//--------------------------------------------------
|
||||
@objc open func updateView(_ size: CGFloat) {}
|
||||
}
|
||||
|
||||
extension InputEntryField {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Implemented TextField Delegate
|
||||
//--------------------------------------------------
|
||||
@discardableResult
|
||||
@objc public func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
||||
proprietorTextDelegate?.textFieldShouldReturn?(textField) ?? true
|
||||
}
|
||||
|
||||
@objc public override func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
|
||||
proprietorTextDelegate?.textField?(textField, shouldChangeCharactersIn: range, replacementString: string)
|
||||
??
|
||||
super.textField(textField, shouldChangeCharactersIn: range, replacementString: string)
|
||||
}
|
||||
|
||||
@objc public override func textFieldDidBeginEditing(_ textField: UITextField) {
|
||||
proprietorTextDelegate?.textFieldDidBeginEditing?(textField) ?? super.textFieldDidBeginEditing(textField)
|
||||
}
|
||||
|
||||
@objc public override func textFieldDidEndEditing(_ textField: UITextField) {
|
||||
proprietorTextDelegate?.textFieldDidEndEditing?(textField) ?? super.textFieldDidEndEditing(textField)
|
||||
}
|
||||
|
||||
@objc public func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
|
||||
proprietorTextDelegate?.textFieldShouldBeginEditing?(textField) ?? true
|
||||
}
|
||||
|
||||
@objc public func textFieldShouldEndEditing(_ textField: UITextField) -> Bool {
|
||||
proprietorTextDelegate?.textFieldShouldEndEditing?(textField) ?? true
|
||||
}
|
||||
|
||||
@objc public func textFieldShouldClear(_ textField: UITextField) -> Bool {
|
||||
proprietorTextDelegate?.textFieldShouldClear?(textField) ?? true
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Accessibility
|
||||
extension InputEntryField {
|
||||
|
||||
@objc open func pushAccessibilityNotification() {
|
||||
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
UIAccessibility.post(notification: .layoutChanged, argument: containerView)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal struct ViewMasking {
|
||||
static var shouldMaskWhileRecording: UInt8 = 0
|
||||
}
|
||||
|
||||
extension VDS.TextField: ViewMaskingProtocol {
|
||||
public var shouldMaskWhileRecording: Bool {
|
||||
get {
|
||||
return (objc_getAssociatedObject(self, &ViewMasking.shouldMaskWhileRecording) as? Bool) ?? false
|
||||
}
|
||||
set {
|
||||
objc_setAssociatedObject(self, &ViewMasking.shouldMaskWhileRecording, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,7 +14,7 @@ import MVMCore
|
||||
/**
|
||||
This class provides the convenience of formatting the MDN entered/displayer for the user.
|
||||
*/
|
||||
@objcMembers open class MdnEntryField: TextEntryField, ABPeoplePickerNavigationControllerDelegate, CNContactPickerDelegate {
|
||||
@objcMembers open class MdnEntryField: InputEntryField, ABPeoplePickerNavigationControllerDelegate, CNContactPickerDelegate {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Stored Properties
|
||||
//--------------------------------------------------
|
||||
@ -47,52 +47,17 @@ import MVMCore
|
||||
get { MVMCoreUIUtility.removeMdnFormat(text) }
|
||||
set { text = MVMCoreUIUtility.formatMdn(newValue) }
|
||||
}
|
||||
|
||||
/// Toggles selected or original (unselected) UI.
|
||||
public override var isSelected: Bool {
|
||||
get { return entryFieldContainer.isSelected }
|
||||
set (selected) {
|
||||
if selected && showError {
|
||||
showError = false
|
||||
}
|
||||
|
||||
super.isSelected = selected
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Initializers
|
||||
//--------------------------------------------------
|
||||
|
||||
@objc public override init(frame: CGRect) {
|
||||
super.init(frame: .zero)
|
||||
}
|
||||
|
||||
@objc public convenience init() {
|
||||
self.init(frame: .zero)
|
||||
}
|
||||
|
||||
@objc required public init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
fatalError("MdnEntryField xib not supported.")
|
||||
}
|
||||
|
||||
required public init(model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
|
||||
super.init(model: model, delegateObject, additionalData)
|
||||
}
|
||||
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Setup
|
||||
//--------------------------------------------------
|
||||
|
||||
@objc public override func setupFieldContainerContent(_ container: UIView) {
|
||||
super.setupFieldContainerContent(container)
|
||||
|
||||
textField.keyboardType = .numberPad
|
||||
open override func setup() {
|
||||
super.setup()
|
||||
setupTextFieldToolbar()
|
||||
}
|
||||
|
||||
open override func setupTextFieldToolbar() {
|
||||
|
||||
open func setupTextFieldToolbar() {
|
||||
let toolbar = UIToolbar.createEmptyToolbar()
|
||||
let space = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
|
||||
let contacts = UIBarButtonItem(title: MVMCoreUIUtility.hardcodedString(withKey: "textfield_contacts_barbutton"), style: .plain, target: self, action: #selector(getContacts))
|
||||
@ -103,40 +68,7 @@ import MVMCore
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Methods
|
||||
//--------------------------------------------------
|
||||
|
||||
@objc public func hasValidMDN() -> Bool {
|
||||
|
||||
guard let MDN = mdn, !MDN.isEmpty else { return false }
|
||||
|
||||
if isNationalMDN {
|
||||
return MVMCoreUIUtility.validateMDNString(MDN)
|
||||
}
|
||||
|
||||
return MVMCoreUIUtility.validateInternationalMDNString(MDN)
|
||||
}
|
||||
|
||||
@objc public func validateMDNTextField() -> Bool {
|
||||
|
||||
guard !shouldValidateMDN, let MDN = mdn, !MDN.isEmpty else {
|
||||
isValid = true
|
||||
return true
|
||||
}
|
||||
|
||||
isValid = hasValidMDN()
|
||||
|
||||
if self.isValid {
|
||||
showError = false
|
||||
|
||||
} else {
|
||||
entryFieldModel?.errorMessage = entryFieldModel?.errorMessage ?? MVMCoreUIUtility.hardcodedString(withKey: "textfield_phone_format_error_message")
|
||||
showError = true
|
||||
UIAccessibility.post(notification: .layoutChanged, argument: textField)
|
||||
}
|
||||
|
||||
return isValid
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
@objc public func getContacts(_ sender: Any?) {
|
||||
|
||||
let picker = CNContactPickerViewController()
|
||||
@ -152,11 +84,12 @@ import MVMCore
|
||||
//--------------------------------------------------
|
||||
// MARK: - MoleculeViewProtocol
|
||||
//--------------------------------------------------
|
||||
|
||||
public override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
|
||||
super.set(with: model, delegateObject, additionalData)
|
||||
|
||||
textField.keyboardType = .phonePad
|
||||
public override func viewModelDidUpdate() {
|
||||
viewModel.type = .phone
|
||||
super.viewModelDidUpdate()
|
||||
if let phoneNumber = viewModel.text {
|
||||
text = phoneNumber.formatUSNumber()
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
@ -179,62 +112,47 @@ import MVMCore
|
||||
let startIndex = unformedMDN.index(unformedMDN.startIndex, offsetBy: 1)
|
||||
unformattedMDN = String(unformedMDN[startIndex...])
|
||||
}
|
||||
|
||||
text = unformattedMDN
|
||||
textFieldShouldReturn(textField)
|
||||
textFieldDidEndEditing(textField)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Implemented TextField Delegate
|
||||
//--------------------------------------------------
|
||||
|
||||
@discardableResult
|
||||
@objc public func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
||||
|
||||
textField.resignFirstResponder()
|
||||
|
||||
return proprietorTextDelegate?.textFieldShouldReturn?(textField) ?? true
|
||||
@objc public override func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
||||
_ = resignFirstResponder()
|
||||
let superValue = super.textFieldShouldReturn(textField)
|
||||
return proprietorTextDelegate?.textFieldShouldReturn?(textField) ?? superValue
|
||||
}
|
||||
|
||||
@objc public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
|
||||
|
||||
if !MVMCoreUIUtility.validate(string, withRegularExpression: RegularExpressionDigitOnly) {
|
||||
return false
|
||||
}
|
||||
|
||||
return proprietorTextDelegate?.textField?(textField, shouldChangeCharactersIn: range, replacementString: string) ?? true
|
||||
@objc public override func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
|
||||
let superValue = super.textField(textField, shouldChangeCharactersIn: range, replacementString: string)
|
||||
return proprietorTextDelegate?.textField?(textField, shouldChangeCharactersIn: range, replacementString: string) ?? superValue
|
||||
}
|
||||
|
||||
@objc public func textFieldDidBeginEditing(_ textField: UITextField) {
|
||||
|
||||
textField.text = MVMCoreUIUtility.removeMdnFormat(textField.text)
|
||||
@objc public override func textFieldDidBeginEditing(_ textField: UITextField) {
|
||||
super.textFieldDidBeginEditing(textField)
|
||||
proprietorTextDelegate?.textFieldDidBeginEditing?(textField)
|
||||
}
|
||||
|
||||
@objc public func textFieldDidEndEditing(_ textField: UITextField) {
|
||||
|
||||
@objc public override func textFieldDidEndEditing(_ textField: UITextField) {
|
||||
proprietorTextDelegate?.textFieldDidEndEditing?(textField)
|
||||
|
||||
if validateMDNTextField() {
|
||||
if isNationalMDN {
|
||||
textField.text = MVMCoreUIUtility.formatMdn(textField.text)
|
||||
}
|
||||
// Validate the base input field along with triggering form field validation rules.
|
||||
validateText()
|
||||
}
|
||||
super.textFieldDidEndEditing(textField)
|
||||
}
|
||||
|
||||
@objc public func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
|
||||
@objc public override func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
|
||||
proprietorTextDelegate?.textFieldShouldBeginEditing?(textField) ?? true
|
||||
}
|
||||
|
||||
@objc public func textFieldShouldEndEditing(_ textField: UITextField) -> Bool {
|
||||
@objc public override func textFieldShouldEndEditing(_ textField: UITextField) -> Bool {
|
||||
proprietorTextDelegate?.textFieldShouldEndEditing?(textField) ?? true
|
||||
}
|
||||
|
||||
@objc public func textFieldShouldClear(_ textField: UITextField) -> Bool {
|
||||
@objc public override func textFieldShouldClear(_ textField: UITextField) -> Bool {
|
||||
proprietorTextDelegate?.textFieldShouldClear?(textField) ?? true
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,4 +12,9 @@
|
||||
//--------------------------------------------------
|
||||
|
||||
public override class var identifier: String { "mdnEntryField" }
|
||||
|
||||
open override func formFieldServerValue() -> AnyHashable? {
|
||||
guard let value = formFieldValue() as? String else { return nil }
|
||||
return value.filter { $0.isNumber }
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,9 +11,9 @@ import UIKit
|
||||
|
||||
@objc public protocol ObservingTextFieldDelegate {
|
||||
/// Called when the entered text becomes valid based on the validation block
|
||||
@objc optional func isValid(textfield: TextEntryField?)
|
||||
@objc optional func isValid(textfield: Any?)
|
||||
/// Called when the entered text becomes invalid based on the validation block
|
||||
@objc optional func isInvalid(textfield: TextEntryField?)
|
||||
@objc optional func isInvalid(textfield: Any?)
|
||||
/// Dismisses the keyboard.
|
||||
@objc optional func dismissFieldInput(_ sender: Any?)
|
||||
}
|
||||
@ -317,9 +317,9 @@ import UIKit
|
||||
super.shouldShowError(showError)
|
||||
|
||||
if showError {
|
||||
observingTextFieldDelegate?.isValid?(textfield: self)
|
||||
} else {
|
||||
observingTextFieldDelegate?.isInvalid?(textfield: self)
|
||||
} else {
|
||||
observingTextFieldDelegate?.isValid?(textfield: self)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -5,9 +5,10 @@
|
||||
// Created by Kevin Christiano on 1/22/20.
|
||||
// Copyright © 2020 Verizon Wireless. All rights reserved.
|
||||
//
|
||||
import VDS
|
||||
|
||||
|
||||
@objcMembers open class TextEntryFieldModel: EntryFieldModel {
|
||||
@objcMembers open class TextEntryFieldModel: EntryFieldModel, FormFieldInternalValidatableProtocol {
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Types
|
||||
//--------------------------------------------------
|
||||
@ -20,6 +21,39 @@
|
||||
case email
|
||||
case text
|
||||
case phone
|
||||
|
||||
//additional
|
||||
case inlineAction
|
||||
case creditCard
|
||||
case date
|
||||
case securityCode
|
||||
|
||||
public func toVDSFieldType() -> VDS.InputField.FieldType {
|
||||
switch self {
|
||||
case .password:
|
||||
.password
|
||||
case .secure:
|
||||
.text
|
||||
case .number:
|
||||
.number
|
||||
case .numberSecure:
|
||||
.number
|
||||
case .email:
|
||||
.text
|
||||
case .text:
|
||||
.text
|
||||
case .phone:
|
||||
.telephone
|
||||
case .inlineAction:
|
||||
.inlineAction
|
||||
case .creditCard:
|
||||
.creditCard
|
||||
case .date:
|
||||
.date
|
||||
case .securityCode:
|
||||
.securityCode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
@ -33,12 +67,21 @@
|
||||
public var disabledTextColor: Color = Color(uiColor: .mvmCoolGray3)
|
||||
public var textAlignment: NSTextAlignment = .left
|
||||
public var keyboardOverride: String?
|
||||
public var type: EntryType?
|
||||
public var type: EntryType = .text
|
||||
public var clearTextOnTap: Bool = false
|
||||
public var displayFormat: String?
|
||||
public var displayMask: String?
|
||||
public var enableClipboardActions: Bool = true
|
||||
|
||||
public var tooltip: TooltipModel?
|
||||
public var transparentBackground: Bool = false
|
||||
public var width: CGFloat?
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - FormFieldInternalValidatableProtocol
|
||||
//--------------------------------------------------
|
||||
open var rules: [AnyRule<String>]?
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Initializers
|
||||
//--------------------------------------------------
|
||||
@ -114,6 +157,9 @@
|
||||
case displayFormat
|
||||
case displayMask
|
||||
case enableClipboardActions
|
||||
case tooltip
|
||||
case transparentBackground
|
||||
case width
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
@ -128,7 +174,7 @@
|
||||
displayFormat = try typeContainer.decodeIfPresent(String.self, forKey: .displayFormat)
|
||||
keyboardOverride = try typeContainer.decodeIfPresent(String.self, forKey: .keyboardOverride)
|
||||
displayMask = try typeContainer.decodeIfPresent(String.self, forKey: .displayMask)
|
||||
type = try typeContainer.decodeIfPresent(EntryType.self, forKey: .type)
|
||||
type = try typeContainer.decodeIfPresent(EntryType.self, forKey: .type) ?? .text
|
||||
|
||||
if let clearTextOnTap = try typeContainer.decodeIfPresent(Bool.self, forKey: .clearTextOnTap) {
|
||||
self.clearTextOnTap = clearTextOnTap
|
||||
@ -149,6 +195,10 @@
|
||||
if let enableClipboardActions = try typeContainer.decodeIfPresent(Bool.self, forKey: .enableClipboardActions) {
|
||||
self.enableClipboardActions = enableClipboardActions
|
||||
}
|
||||
|
||||
tooltip = try typeContainer.decodeIfPresent(TooltipModel.self, forKey: .tooltip)
|
||||
transparentBackground = try typeContainer.decodeIfPresent(Bool.self, forKey: .transparentBackground) ?? false
|
||||
width = try typeContainer.decodeIfPresent(CGFloat.self, forKey: .width)
|
||||
}
|
||||
|
||||
open override func encode(to encoder: Encoder) throws {
|
||||
@ -164,5 +214,25 @@
|
||||
try container.encode(disabledTextColor, forKey: .disabledTextColor)
|
||||
try container.encode(clearTextOnTap, forKey: .clearTextOnTap)
|
||||
try container.encode(enableClipboardActions, forKey: .enableClipboardActions)
|
||||
try container.encodeIfPresent(tooltip, forKey: .tooltip)
|
||||
try container.encode(transparentBackground, forKey: .transparentBackground)
|
||||
try container.encodeIfPresent(width, forKey: .width)
|
||||
}
|
||||
|
||||
open override func isEqual(to model: any ModelComparisonProtocol) -> Bool {
|
||||
guard super.isEqual(to: model), let model = model as? Self else { return false }
|
||||
return placeholder == model.placeholder
|
||||
&& textAlignment == model.textAlignment
|
||||
&& enabledTextColor == model.enabledTextColor
|
||||
&& disabledTextColor == model.disabledTextColor
|
||||
&& keyboardOverride == model.keyboardOverride
|
||||
&& type == model.type
|
||||
&& clearTextOnTap == model.clearTextOnTap
|
||||
&& displayFormat == model.displayFormat
|
||||
&& displayMask == model.displayMask
|
||||
&& enableClipboardActions == model.enableClipboardActions
|
||||
&& tooltip == model.tooltip
|
||||
&& transparentBackground == model.transparentBackground
|
||||
&& width == model.width
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,100 +7,60 @@
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import VDS
|
||||
|
||||
|
||||
class TextViewEntryField: EntryField, UITextViewDelegate, ObservingTextFieldDelegate {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Outlets
|
||||
//--------------------------------------------------
|
||||
|
||||
open private(set) var textView: TextView = {
|
||||
let textView = TextView()
|
||||
textView.setContentCompressionResistancePriority(.required, for: .vertical)
|
||||
return textView
|
||||
}()
|
||||
|
||||
//--------------------------------------------------
|
||||
open class TextViewEntryField: VDS.TextArea, VDSMoleculeViewProtocol, ObservingTextFieldDelegate, ViewMaskingProtocol {
|
||||
//------------------------------------------------------
|
||||
// MARK: - Properties
|
||||
//--------------------------------------------------
|
||||
//------------------------------------------------------
|
||||
open var viewModel: TextViewEntryFieldModel!
|
||||
open var delegateObject: MVMCoreUIDelegateObject?
|
||||
open var additionalData: [AnyHashable : Any]?
|
||||
|
||||
// Form Validation
|
||||
open var fieldKey: String?
|
||||
open var fieldValue: JSONValue?
|
||||
open var groupName: String?
|
||||
|
||||
private var observingForChange: Bool = false
|
||||
//--------------------------------------------------
|
||||
// MARK: - Stored Properties
|
||||
//--------------------------------------------------
|
||||
public var isValid: Bool = true
|
||||
|
||||
private var isEditting: Bool = false {
|
||||
didSet {
|
||||
viewModel?.selected = isEditting
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Computed Properties
|
||||
//--------------------------------------------------
|
||||
|
||||
public var textViewEntryFieldModel: TextViewEntryFieldModel? {
|
||||
model as? TextViewEntryFieldModel
|
||||
open var shouldMaskWhileRecording: Bool {
|
||||
return viewModel?.shouldMaskRecordedView ?? false
|
||||
}
|
||||
|
||||
public override var isEnabled: Bool {
|
||||
get { super.isEnabled }
|
||||
set (enabled) {
|
||||
super.isEnabled = enabled
|
||||
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
self.textView.isEnabled = enabled
|
||||
if self.textView.isShowingPlaceholder {
|
||||
self.textView.textColor = self.textView.placeholderTextColor
|
||||
} else {
|
||||
self.textView.textColor = (self.textView.isEnabled ? self.textViewEntryFieldModel?.enabledTextColor : self.textViewEntryFieldModel?.disabledTextColor)?.uiColor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override var showError: Bool {
|
||||
get { super.showError }
|
||||
set (error) {
|
||||
|
||||
if error {
|
||||
textView.accessibilityValue = String(format: MVMCoreUIUtility.hardcodedString(withKey: "textView_error_message") ?? "", textView.text ?? "", entryFieldModel?.errorMessage ?? "")
|
||||
} else {
|
||||
textView.accessibilityValue = nil
|
||||
}
|
||||
|
||||
super.showError = error
|
||||
|
||||
/// Placeholder access for the textView.
|
||||
open var placeholder: String? {
|
||||
get { viewModel?.placeholder }
|
||||
set {
|
||||
textView.placeholder = newValue ?? ""
|
||||
viewModel?.placeholder = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// The text of this textView.
|
||||
open override var text: String? {
|
||||
get { textViewEntryFieldModel?.text }
|
||||
set {
|
||||
textView.text = newValue
|
||||
textViewEntryFieldModel?.text = newValue
|
||||
didSet {
|
||||
viewModel?.text = text
|
||||
}
|
||||
}
|
||||
|
||||
/// Placeholder access for the textView.
|
||||
public var placeholder: String? {
|
||||
get { textViewEntryFieldModel?.placeholder }
|
||||
set {
|
||||
textView.placeholder = newValue ?? ""
|
||||
textViewEntryFieldModel?.placeholder = newValue
|
||||
textView.setPlaceholderIfAvailable()
|
||||
open override var errorText: String? {
|
||||
get {
|
||||
viewModel.dynamicErrorMessage ?? viewModel.errorMessage
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Constraint
|
||||
//--------------------------------------------------
|
||||
|
||||
public var heightConstraint: NSLayoutConstraint?
|
||||
private var topConstraint: NSLayoutConstraint?
|
||||
private var leadingConstraint: NSLayoutConstraint?
|
||||
private var trailingConstraint: NSLayoutConstraint?
|
||||
private var bottomConstraint: NSLayoutConstraint?
|
||||
|
||||
private func adjustMarginConstraints(constant: CGFloat) {
|
||||
|
||||
topConstraint?.constant = constant
|
||||
leadingConstraint?.constant = constant
|
||||
trailingConstraint?.constant = constant
|
||||
bottomConstraint?.constant = constant
|
||||
set {}
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
@ -108,198 +68,178 @@ class TextViewEntryField: EntryField, UITextViewDelegate, ObservingTextFieldDele
|
||||
//--------------------------------------------------
|
||||
|
||||
/// The delegate and block for validation. Validates if the text that the user has entered.
|
||||
public weak var observingTextViewDelegate: ObservingTextFieldDelegate? {
|
||||
didSet {
|
||||
if observingTextViewDelegate != nil && !observingForChange {
|
||||
observingForChange = true
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(valueChanged), name: UITextView.textDidChangeNotification, object: textView)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(endInputing), name: UITextView.textDidEndEditingNotification, object: textView)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(startEditing), name: UITextView.textDidBeginEditingNotification, object: textView)
|
||||
|
||||
} else if observingTextViewDelegate == nil && observingForChange {
|
||||
observingForChange = false
|
||||
NotificationCenter.default.removeObserver(self, name: UITextView.textDidChangeNotification, object: textView)
|
||||
NotificationCenter.default.removeObserver(self, name: UITextView.textDidEndEditingNotification, object: textView)
|
||||
NotificationCenter.default.removeObserver(self, name: UITextView.textDidBeginEditingNotification, object: textView)
|
||||
}
|
||||
}
|
||||
}
|
||||
open weak var observingTextViewDelegate: ObservingTextFieldDelegate?
|
||||
|
||||
/// If you're using a ViewController, you must set this to it
|
||||
public weak var uiTextViewDelegate: UITextViewDelegate? {
|
||||
open weak var uiTextViewDelegate: UITextViewDelegate? {
|
||||
get { textView.delegate }
|
||||
set { textView.delegate = newValue }
|
||||
}
|
||||
|
||||
@objc public func setBothTextDelegates(to delegate: (UITextViewDelegate & ObservingTextFieldDelegate)?) {
|
||||
@objc open func setBothTextDelegates(to delegate: (UITextViewDelegate & ObservingTextFieldDelegate)?) {
|
||||
observingTextViewDelegate = delegate
|
||||
uiTextViewDelegate = delegate
|
||||
}
|
||||
|
||||
open func setupTextViewToolbar() {
|
||||
let observingDelegate = observingTextViewDelegate ?? self
|
||||
textView.inputAccessoryView = UIToolbar.getToolbarWithDoneButton(delegate: observingDelegate,
|
||||
action: #selector(observingDelegate.dismissFieldInput))
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Lifecycle
|
||||
//--------------------------------------------------
|
||||
|
||||
@objc open override func setupFieldContainerContent(_ container: UIView) {
|
||||
open override func setup() {
|
||||
super.setup()
|
||||
//turn off internal required rule
|
||||
useRequiredRule = false
|
||||
|
||||
container.addSubview(textView)
|
||||
publisher(for: .valueChanged)
|
||||
.sink { [weak self] control in
|
||||
guard let self, let viewModel else { return }
|
||||
_ = FormValidator.validate(delegate: delegateObject?.formHolderDelegate)
|
||||
}.store(in: &subscribers)
|
||||
|
||||
topConstraint = textView.topAnchor.constraint(equalTo: container.topAnchor, constant: Padding.Three)
|
||||
leadingConstraint = textView.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: Padding.Three)
|
||||
trailingConstraint = container.trailingAnchor.constraint(equalTo: textView.trailingAnchor, constant: Padding.Three)
|
||||
bottomConstraint = container.bottomAnchor.constraint(equalTo: textView.bottomAnchor, constant: Padding.Three)
|
||||
textView
|
||||
.publisher(for: .editingDidBegin)
|
||||
.sink { [weak self] textView in
|
||||
guard let self else { return }
|
||||
isEditting = true
|
||||
|
||||
}.store(in: &subscribers)
|
||||
|
||||
topConstraint?.isActive = true
|
||||
leadingConstraint?.isActive = true
|
||||
trailingConstraint?.isActive = true
|
||||
bottomConstraint?.isActive = true
|
||||
|
||||
heightConstraint = textView.heightAnchor.constraint(equalToConstant: 0)
|
||||
accessibilityElements = [textView]
|
||||
textView
|
||||
.publisher(for: .editingDidEnd)
|
||||
.sink { [weak self] textView in
|
||||
guard let self else { return }
|
||||
isEditting = false
|
||||
if let viewModel, let valid = viewModel.isValid {
|
||||
updateValidation(valid)
|
||||
}
|
||||
|
||||
}.store(in: &subscribers)
|
||||
}
|
||||
|
||||
open override func updateView(_ size: CGFloat) {
|
||||
super.updateView(size)
|
||||
textView.updateView(size)
|
||||
}
|
||||
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
|
||||
textView.reset()
|
||||
adjustMarginConstraints(constant: Padding.Three)
|
||||
heightConstraint?.constant = 0
|
||||
heightConstraint?.isActive = false
|
||||
}
|
||||
open func viewModelDidUpdate() {
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Methods
|
||||
//--------------------------------------------------
|
||||
|
||||
/// Validates the text of the entry field.
|
||||
@objc public override func validateText() {
|
||||
text = textView.text
|
||||
super.validateText()
|
||||
}
|
||||
|
||||
/// Executes on UITextView.textDidBeginEditingNotification
|
||||
@objc override func startEditing() {
|
||||
super.startEditing()
|
||||
_ = textView.becomeFirstResponder()
|
||||
}
|
||||
|
||||
/// Executes on UITextView.textDidChangeNotification (each character entry)
|
||||
@objc override func valueChanged() {
|
||||
super.valueChanged()
|
||||
validateText()
|
||||
}
|
||||
|
||||
/// Executes on UITextView.textDidEndEditingNotification
|
||||
@objc override func endInputing() {
|
||||
super.endInputing()
|
||||
text = viewModel.text
|
||||
minHeight = viewModel.minHeight
|
||||
maxLength = viewModel.maxLength
|
||||
|
||||
// Don't show error till user starts typing.
|
||||
guard text?.count ?? 0 != 0 else {
|
||||
showError = false
|
||||
return
|
||||
}
|
||||
|
||||
if let isValid = textViewEntryFieldModel?.isValid {
|
||||
self.isValid = isValid
|
||||
}
|
||||
|
||||
showError = !isValid
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - MoleculeViewProtocol
|
||||
//--------------------------------------------------
|
||||
|
||||
open override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
|
||||
super.set(with: model, delegateObject, additionalData)
|
||||
|
||||
guard let model = model as? TextViewEntryFieldModel else { return }
|
||||
|
||||
if let height = model.height {
|
||||
heightConstraint?.constant = height
|
||||
heightConstraint?.isActive = true
|
||||
}
|
||||
|
||||
text = model.text
|
||||
labelText = viewModel.title
|
||||
helperText = viewModel.feedback
|
||||
isEnabled = viewModel.enabled
|
||||
isReadOnly = viewModel.readOnly
|
||||
isRequired = viewModel.required
|
||||
tooltipModel = viewModel.tooltip?.convertToVDSTooltipModel()
|
||||
width = viewModel.width
|
||||
transparentBackground = viewModel.transparentBackground
|
||||
|
||||
uiTextViewDelegate = delegateObject?.uiTextViewDelegate
|
||||
observingTextViewDelegate = delegateObject?.observingTextFieldDelegate
|
||||
|
||||
if let accessibilityText = model.accessibilityText {
|
||||
if let accessibilityText = viewModel.accessibilityText {
|
||||
accessibilityLabel = accessibilityText
|
||||
}
|
||||
containerView.accessibilityIdentifier = viewModel.accessibilityIdentifier
|
||||
textView.isEditable = viewModel.editable
|
||||
textView.textAlignment = viewModel.textAlignment
|
||||
textView.placeholder = viewModel.placeholder ?? ""
|
||||
|
||||
if (viewModel.selected ?? false) && !viewModel.wasInitiallySelected {
|
||||
|
||||
viewModel.wasInitiallySelected = true
|
||||
isEditting = true
|
||||
}
|
||||
|
||||
textView.isEditable = model.editable
|
||||
textView.textAlignment = model.textAlignment
|
||||
textView.accessibilityIdentifier = model.accessibilityIdentifier
|
||||
textView.textColor = model.enabled ? model.enabledTextColor.uiColor : model.disabledTextColor.uiColor
|
||||
textView.font = model.fontStyle.getFont()
|
||||
textView.placeholder = model.placeholder ?? ""
|
||||
textView.placeholderFontStyle = model.placeholderFontStyle
|
||||
textView.placeholderTextColor = model.placeholderTextColor.uiColor
|
||||
textView.setPlaceholderIfAvailable()
|
||||
|
||||
switch model.type {
|
||||
case .secure, .password:
|
||||
switch viewModel.type {
|
||||
case .secure:
|
||||
textView.isSecureTextEntry = true
|
||||
textView.shouldMaskWhileRecording = true
|
||||
|
||||
case .numberSecure:
|
||||
textView.isSecureTextEntry = true
|
||||
textView.keyboardType = .numberPad
|
||||
|
||||
case .number:
|
||||
textView.shouldMaskWhileRecording = true
|
||||
textView.keyboardType = .numberPad
|
||||
|
||||
case .email:
|
||||
textView.keyboardType = .emailAddress
|
||||
|
||||
default: break
|
||||
case .securityCode, .creditCard, .password:
|
||||
textView.shouldMaskWhileRecording = true
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// Override the preset keyboard set in type.
|
||||
if let keyboardType = viewModel.assignKeyboardType() {
|
||||
textView.keyboardType = keyboardType
|
||||
}
|
||||
|
||||
/// append any internal rules:
|
||||
viewModel.rules = rules
|
||||
|
||||
/// No point in configuring if the TextView is Read-only.
|
||||
if textView.isEditable {
|
||||
FormValidator.setupValidation(for: model, delegate: delegateObject?.formHolderDelegate)
|
||||
setupTextViewToolbar()
|
||||
FormValidator.setupValidation(for: viewModel, delegate: delegateObject?.formHolderDelegate)
|
||||
|
||||
if isSelected {
|
||||
if isEditting {
|
||||
DispatchQueue.main.async {
|
||||
_ = self.textView.becomeFirstResponder()
|
||||
_ = self.becomeFirstResponder()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if model.hideBorders {
|
||||
adjustMarginConstraints(constant: 0)
|
||||
viewModel.updateUI = {
|
||||
MVMCoreDispatchUtility.performBlock(onMainThread: { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
if isEditting {
|
||||
updateValidation(viewModel.isValid ?? true)
|
||||
|
||||
} else if viewModel.isValid ?? true && showError {
|
||||
showError = false
|
||||
}
|
||||
isEnabled = viewModel.enabled
|
||||
})
|
||||
}
|
||||
updateAccessibility(model: model)
|
||||
|
||||
viewModel.updateUIDynamicError = {
|
||||
MVMCoreDispatchUtility.performBlock(onMainThread: { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
let validState = viewModel.isValid ?? false
|
||||
if !validState && viewModel.shouldClearText {
|
||||
text = ""
|
||||
viewModel.shouldClearText = false
|
||||
}
|
||||
updateValidation(validState)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func updateAccessibility(model: TextViewEntryFieldModel) {
|
||||
private func updateValidation(_ isValid: Bool) {
|
||||
let previousValidity = self.isValid
|
||||
self.isValid = isValid
|
||||
|
||||
var message = ""
|
||||
|
||||
if let titleText = model.accessibilityText ?? model.title {
|
||||
message += "\(titleText) \( model.enabled ? String(format: (MVMCoreUIUtility.hardcodedString(withKey: "textfield_optional")) ?? "") : "" ) \(self.textView.isEnabled ? "" : MVMCoreUIUtility.hardcodedString(withKey: "textfield_disabled_state") ?? "")"
|
||||
if previousValidity && !isValid {
|
||||
showError = true
|
||||
} else if (!previousValidity && isValid) {
|
||||
showError = false
|
||||
}
|
||||
|
||||
if let feedback = model.feedback {
|
||||
message += ", " + feedback
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - MoleculeViewProtocol
|
||||
//--------------------------------------------------
|
||||
open func updateView(_ size: CGFloat) {}
|
||||
}
|
||||
|
||||
extension VDS.TextView: ViewMaskingProtocol {
|
||||
public var shouldMaskWhileRecording: Bool {
|
||||
get {
|
||||
return (objc_getAssociatedObject(self, &ViewMasking.shouldMaskWhileRecording) as? Bool) ?? false
|
||||
}
|
||||
|
||||
if let errorMessage = errorLabel.text {
|
||||
message += ", " + errorMessage
|
||||
set {
|
||||
objc_setAssociatedObject(self, &ViewMasking.shouldMaskWhileRecording, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
||||
}
|
||||
|
||||
textView.accessibilityLabel = message
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -7,9 +7,9 @@
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import VDS
|
||||
|
||||
|
||||
class TextViewEntryFieldModel: TextEntryFieldModel {
|
||||
public class TextViewEntryFieldModel: TextEntryFieldModel {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Properties
|
||||
//--------------------------------------------------
|
||||
@ -17,12 +17,10 @@ class TextViewEntryFieldModel: TextEntryFieldModel {
|
||||
public override class var identifier: String { "textView" }
|
||||
|
||||
public var accessibilityText: String?
|
||||
public var fontStyle: Styler.Font = Styler.Font.RegularBodyLarge
|
||||
public var height: CGFloat?
|
||||
public var placeholderTextColor: Color = Color(uiColor: .mvmCoolGray3)
|
||||
public var placeholderFontStyle: Styler.Font = Styler.Font.RegularMicro
|
||||
public var editable: Bool = true
|
||||
public var showsPlaceholder: Bool = false
|
||||
public var minHeight: VDS.TextArea.Height = .twoX
|
||||
public var maxLength: Int?
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Keys
|
||||
@ -30,11 +28,9 @@ class TextViewEntryFieldModel: TextEntryFieldModel {
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case accessibilityText
|
||||
case fontStyle
|
||||
case height
|
||||
case placeholderFontStyle
|
||||
case placeholderTextColor
|
||||
case editable
|
||||
case minHeight
|
||||
case maxLength
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
@ -45,34 +41,27 @@ class TextViewEntryFieldModel: TextEntryFieldModel {
|
||||
try super.init(from: decoder)
|
||||
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
if let placeholderFontStyle = try typeContainer.decodeIfPresent(Styler.Font.self, forKey: .placeholderFontStyle) {
|
||||
self.placeholderFontStyle = placeholderFontStyle
|
||||
}
|
||||
|
||||
if let placeholderTextColor = try typeContainer.decodeIfPresent(Color.self, forKey: .placeholderTextColor) {
|
||||
self.placeholderTextColor = placeholderTextColor
|
||||
}
|
||||
|
||||
if let fontStyle = try typeContainer.decodeIfPresent(Styler.Font.self, forKey: .fontStyle) {
|
||||
self.fontStyle = fontStyle
|
||||
}
|
||||
|
||||
if let editable = try typeContainer.decodeIfPresent(Bool.self, forKey: .editable) {
|
||||
self.editable = editable
|
||||
}
|
||||
|
||||
editable = try typeContainer.decodeIfPresent(Bool.self, forKey: .editable) ?? true
|
||||
minHeight = try typeContainer.decodeIfPresent(VDS.TextArea.Height.self, forKey: .minHeight) ?? .twoX
|
||||
maxLength = try typeContainer.decodeIfPresent(Int.self, forKey: .maxLength)
|
||||
accessibilityText = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityText)
|
||||
height = try typeContainer.decodeIfPresent(CGFloat.self, forKey: .height)
|
||||
}
|
||||
|
||||
public override func encode(to encoder: Encoder) throws {
|
||||
try super.encode(to: encoder)
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encodeIfPresent(accessibilityText, forKey: .accessibilityText)
|
||||
try container.encodeIfPresent(height, forKey: .height)
|
||||
try container.encode(fontStyle, forKey: .fontStyle)
|
||||
try container.encode(editable, forKey: .editable)
|
||||
try container.encode(placeholderFontStyle, forKey: .placeholderFontStyle)
|
||||
try container.encode(placeholderTextColor, forKey: .placeholderTextColor)
|
||||
try container.encode(minHeight, forKey: .minHeight)
|
||||
try container.encodeIfPresent(maxLength, forKey: .maxLength)
|
||||
}
|
||||
|
||||
open override func isEqual(to model: any ModelComparisonProtocol) -> Bool {
|
||||
guard super.isEqual(to: model), let model = model as? Self else { return false }
|
||||
return accessibilityText == model.accessibilityText
|
||||
&& editable == model.editable
|
||||
&& minHeight == model.minHeight
|
||||
&& maxLength == model.maxLength
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -7,144 +7,41 @@
|
||||
//
|
||||
|
||||
import MVMCore
|
||||
import VDS
|
||||
|
||||
/**
|
||||
This class expects its height and width to be equal.
|
||||
*/
|
||||
@objcMembers open class Checkbox: Control, MVMCoreUIViewConstrainingProtocol {
|
||||
//--------------------------------------------------
|
||||
@objcMembers open class Checkbox: VDS.Checkbox, VDSMoleculeViewProtocol, MVMCoreUIViewConstrainingProtocol {
|
||||
//------------------------------------------------------
|
||||
// MARK: - Properties
|
||||
//--------------------------------------------------
|
||||
|
||||
public var sizeObject: MFSizeObject? = MFSizeObject(standardSize: Checkbox.defaultHeightWidth, standardiPadPortraitSize: Checkbox.defaultHeightWidth + 6.0)
|
||||
|
||||
//------------------------------------------------------
|
||||
open var viewModel: CheckboxModel!
|
||||
open var delegateObject: MVMCoreUIDelegateObject?
|
||||
open var additionalData: [AnyHashable : Any]?
|
||||
|
||||
// Form Validation
|
||||
var fieldKey: String?
|
||||
var fieldValue: JSONValue?
|
||||
var groupName: String?
|
||||
var delegateObject: MVMCoreUIDelegateObject?
|
||||
|
||||
public var checkboxModel: CheckboxModel? {
|
||||
model as? CheckboxModel
|
||||
}
|
||||
|
||||
public static let defaultHeightWidth: CGFloat = 18.0
|
||||
|
||||
/// If true the border of this checkbox will be circular.
|
||||
public var isRound: Bool = false
|
||||
|
||||
/// Determined if the checkbox's UI should animated when selected.
|
||||
public var isAnimated: Bool = true
|
||||
|
||||
/// Disables all selection logic when setting the value of isSelected, reducing it to a stored property.
|
||||
public var updateSelectionOnly: Bool = false
|
||||
|
||||
/// The color of the background when checked.
|
||||
public var checkedBackgroundColor: UIColor = .clear {
|
||||
didSet {
|
||||
if isSelected {
|
||||
backgroundColor = checkedBackgroundColor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The color of the background when unChecked.
|
||||
public var unCheckedBackgroundColor: UIColor = .clear {
|
||||
didSet {
|
||||
if !isSelected {
|
||||
backgroundColor = unCheckedBackgroundColor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieves ideeal radius value to curve square into a circle.
|
||||
public var cornerRadiusValue: CGFloat {
|
||||
bounds.size.height / 2
|
||||
}
|
||||
|
||||
|
||||
/// Action Block called when the switch is selected.
|
||||
public var actionBlock: ActionBlock?
|
||||
|
||||
/// Manages the appearance of the checkbox.
|
||||
private var shapeLayer: CAShapeLayer?
|
||||
|
||||
/// Width of the check mark.
|
||||
public var checkWidth: CGFloat = 2 {
|
||||
open var actionBlock: ActionBlock? {
|
||||
didSet {
|
||||
if let shapeLayer = shapeLayer {
|
||||
CATransaction.withDisabledAnimations {
|
||||
shapeLayer.lineWidth = checkWidth
|
||||
if let actionBlock {
|
||||
onChange = { _ in
|
||||
actionBlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open override var isEnabled: Bool {
|
||||
didSet {
|
||||
|
||||
isUserInteractionEnabled = isEnabled
|
||||
|
||||
if isEnabled {
|
||||
layer.borderColor = borderColor.cgColor
|
||||
backgroundColor = isSelected ? checkedBackgroundColor : unCheckedBackgroundColor
|
||||
setShapeLayerStrokeColor(checkColor)
|
||||
} else {
|
||||
layer.borderColor = disabledBorderColor.cgColor
|
||||
backgroundColor = disabledBackgroundColor
|
||||
setShapeLayerStrokeColor(disabledCheckColor)
|
||||
onChange = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public var disabledBackgroundColor: UIColor = .clear
|
||||
public var disabledBorderColor: UIColor = .mvmCoolGray3
|
||||
public var disabledCheckColor: UIColor = .mvmCoolGray3
|
||||
|
||||
/// Color of the check mark.
|
||||
public var checkColor: UIColor = .mvmBlack {
|
||||
didSet { setShapeLayerStrokeColor(checkColor) }
|
||||
}
|
||||
|
||||
/// Border width of the checkbox
|
||||
public var borderWidth: CGFloat = 1 {
|
||||
didSet { layer.borderWidth = borderWidth }
|
||||
}
|
||||
|
||||
/// border color of the Checkbox
|
||||
public var borderColor: UIColor = .mvmBlack {
|
||||
didSet { layer.borderColor = borderColor.cgColor }
|
||||
}
|
||||
|
||||
/**
|
||||
The represented state of the Checkbox.
|
||||
|
||||
Setting updateSelectionOnly to true bypasses the animation logic inherent with setting this property.
|
||||
*/
|
||||
override open var isSelected: Bool {
|
||||
didSet {
|
||||
if !updateSelectionOnly {
|
||||
layoutIfNeeded()
|
||||
(model as? CheckboxModel)?.selected = isSelected
|
||||
shapeLayer?.removeAllAnimations()
|
||||
updateCheckboxUI(isSelected: isSelected, isAnimated: isAnimated)
|
||||
_ = FormValidator.validate(delegate: delegateObject?.formHolderDelegate)
|
||||
updateAccessibilityLabel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Constraints
|
||||
//--------------------------------------------------
|
||||
|
||||
public var heightConstraint: NSLayoutConstraint?
|
||||
public var widthConstraint: NSLayoutConstraint?
|
||||
|
||||
/// Updates the height and width anchors of the Checkbox with the assigned value.
|
||||
public var heigthWidthConstant: CGFloat = Checkbox.defaultHeightWidth {
|
||||
didSet {
|
||||
heightConstraint?.constant = heigthWidthConstant
|
||||
widthConstraint?.constant = heigthWidthConstant
|
||||
viewModel?.selected = isSelected
|
||||
_ = FormValidator.validate(delegate: delegateObject?.formHolderDelegate)
|
||||
}
|
||||
}
|
||||
|
||||
@ -154,11 +51,6 @@ import MVMCore
|
||||
|
||||
override public init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
isAccessibilityElement = true
|
||||
accessibilityHint = MVMCoreUIUtility.hardcodedString(withKey: "checkbox_action_hint")
|
||||
accessibilityTraits = .button
|
||||
updateAccessibilityLabel()
|
||||
}
|
||||
|
||||
/// There is currently no intention on using xib files.
|
||||
@ -167,278 +59,110 @@ import MVMCore
|
||||
fatalError("xib file is not implemented for Checkbox.")
|
||||
}
|
||||
|
||||
public convenience override init() {
|
||||
public convenience required init() {
|
||||
self.init(frame:.zero)
|
||||
}
|
||||
|
||||
public convenience init(isChecked: Bool) {
|
||||
self.init(frame: .zero)
|
||||
checkAndBypassAnimations(selected: isChecked)
|
||||
isSelected = isChecked
|
||||
}
|
||||
|
||||
public convenience init(checkedBackgroundColor: UIColor, unCheckedBackgroundColor: UIColor, isChecked: Bool = false) {
|
||||
self.init(frame: .zero)
|
||||
checkAndBypassAnimations(selected: isChecked)
|
||||
self.checkedBackgroundColor = checkedBackgroundColor
|
||||
self.unCheckedBackgroundColor = unCheckedBackgroundColor
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Lifecycle
|
||||
//--------------------------------------------------
|
||||
|
||||
override open func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
drawShapeLayer()
|
||||
layer.cornerRadius = isRound ? cornerRadiusValue : 0
|
||||
}
|
||||
|
||||
open override func setupView() {
|
||||
super.setupView()
|
||||
|
||||
isUserInteractionEnabled = true
|
||||
translatesAutoresizingMaskIntoConstraints = false
|
||||
backgroundColor = .clear
|
||||
|
||||
widthConstraint = widthAnchor.constraint(equalToConstant: Checkbox.defaultHeightWidth)
|
||||
heightConstraint = heightAnchor.constraint(equalToConstant: Checkbox.defaultHeightWidth)
|
||||
heightWidthIsActive(true)
|
||||
isSelected = isChecked
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Actions
|
||||
//--------------------------------------------------
|
||||
|
||||
open override func sendAction(_ action: Selector, to target: Any?, for event: UIEvent?) {
|
||||
super.sendAction(action, to: target, for: event)
|
||||
toggleAndAction()
|
||||
}
|
||||
|
||||
open override func sendActions(for controlEvents: UIControl.Event) {
|
||||
super.sendActions(for: controlEvents)
|
||||
toggleAndAction()
|
||||
}
|
||||
|
||||
/// This will toggle the state of the Checkbox and execute the actionBlock if provided.
|
||||
public func toggleAndAction() {
|
||||
isSelected.toggle()
|
||||
actionBlock?()
|
||||
open func toggleAndAction() {
|
||||
toggle()
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Methods
|
||||
//--------------------------------------------------
|
||||
|
||||
/// Creates the check mark layer.
|
||||
private func drawShapeLayer() {
|
||||
|
||||
if shapeLayer == nil {
|
||||
|
||||
let shapeLayer = CAShapeLayer()
|
||||
self.shapeLayer = shapeLayer
|
||||
shapeLayer.frame = bounds
|
||||
layer.addSublayer(shapeLayer)
|
||||
shapeLayer.strokeColor = isEnabled ? checkColor.cgColor : disabledCheckColor.cgColor
|
||||
shapeLayer.fillColor = UIColor.clear.cgColor
|
||||
shapeLayer.path = checkMarkPath()
|
||||
shapeLayer.lineJoin = .miter
|
||||
shapeLayer.lineWidth = checkWidth
|
||||
|
||||
CATransaction.withDisabledAnimations {
|
||||
shapeLayer.strokeEnd = isSelected ? 1 : 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// - returns: The CGPath of a UIBezierPath detailing the path of a checkmark
|
||||
func checkMarkPath() -> CGPath {
|
||||
|
||||
let length = max(bounds.size.height, bounds.size.width)
|
||||
let xInsetLeft = length * 0.25
|
||||
let yInsetTop = length * 0.3
|
||||
let innerWidth = length - (xInsetLeft + length * 0.25) // + Right X Inset
|
||||
let innerHeight = length - (yInsetTop + length * 0.35) // + Bottom Y Inset
|
||||
|
||||
let startPoint = CGPoint(x: xInsetLeft, y: yInsetTop + (innerHeight / 2))
|
||||
let pivotOffSet = CGPoint(x: xInsetLeft + (innerWidth * 0.33), y: yInsetTop + innerHeight)
|
||||
let endOffset = CGPoint(x: xInsetLeft + innerWidth, y: yInsetTop)
|
||||
|
||||
let bezierPath = UIBezierPath()
|
||||
bezierPath.move(to: startPoint)
|
||||
bezierPath.addLine(to: pivotOffSet)
|
||||
bezierPath.addLine(to: endOffset)
|
||||
|
||||
return bezierPath.cgPath
|
||||
}
|
||||
|
||||
/// Programmatic means to check/uncheck the box.
|
||||
/// - parameter selected: state of the check box: true = checked OR false = unchecked.
|
||||
/// - parameter animated: allows the state of the checkbox to change with or without animation.
|
||||
public func updateSelection(to selected: Bool, animated: Bool) {
|
||||
open func updateSelection(to selected: Bool, animated: Bool) {
|
||||
|
||||
DispatchQueue.main.async {
|
||||
|
||||
self.checkAndBypassAnimations(selected: selected)
|
||||
self.drawShapeLayer()
|
||||
self.shapeLayer?.removeAllAnimations()
|
||||
self.updateCheckboxUI(isSelected: selected, isAnimated: animated)
|
||||
self.isAnimated = animated
|
||||
self.isSelected = selected
|
||||
}
|
||||
}
|
||||
|
||||
/// updates the visuals of the check mark and background.
|
||||
/// - parameter isSelected: the check state of the checkbox.
|
||||
/// - parameter isAnimated: determines of the changes should animate or immediately refelect.
|
||||
public func updateCheckboxUI(isSelected: Bool, isAnimated: Bool) {
|
||||
open func updateCheckboxUI(isSelected: Bool, isAnimated: Bool) {
|
||||
|
||||
if isAnimated {
|
||||
let animateStrokeEnd = CABasicAnimation(keyPath: "strokeEnd")
|
||||
animateStrokeEnd.timingFunction = CAMediaTimingFunction(name: .linear)
|
||||
animateStrokeEnd.duration = 0.3
|
||||
animateStrokeEnd.fillMode = .both
|
||||
animateStrokeEnd.isRemovedOnCompletion = false
|
||||
animateStrokeEnd.fromValue = !isSelected ? 1 : 0
|
||||
animateStrokeEnd.toValue = isSelected ? 1 : 0
|
||||
self.shapeLayer?.add(animateStrokeEnd, forKey: "strokeEnd")
|
||||
|
||||
UIView.animate(withDuration: 0.2, delay: 0.1, options: .curveEaseOut, animations: {
|
||||
self.backgroundColor = isSelected ? self.checkedBackgroundColor : self.unCheckedBackgroundColor
|
||||
})
|
||||
} else {
|
||||
CATransaction.withDisabledAnimations {
|
||||
self.shapeLayer?.strokeEnd = isSelected ? 1 : 0
|
||||
}
|
||||
|
||||
backgroundColor = isSelected ? checkedBackgroundColor : unCheckedBackgroundColor
|
||||
DispatchQueue.main.async {
|
||||
self.isAnimated = isAnimated
|
||||
self.isSelected = isSelected
|
||||
}
|
||||
}
|
||||
|
||||
/// Adjust accessibility label based on state of Checkbox.
|
||||
public func updateAccessibilityLabel() {
|
||||
// Attention: This needs to be addressed with the accessibility team.
|
||||
// NOTE: Currently emptying description part of MVMCoreUICheckBox accessibility label to avoid crashing!
|
||||
if let state = MVMCoreUIUtility.hardcodedString(withKey: isSelected ? "checkbox_checked_state" : "checkbox_unchecked_state") {
|
||||
accessibilityLabel = String(format: MVMCoreUIUtility.hardcodedString(withKey: "checkbox_desc_state") ?? "%@%@", "", state)
|
||||
}
|
||||
}
|
||||
|
||||
private func setShapeLayerStrokeColor(_ color: UIColor) {
|
||||
|
||||
if let shapeLayer = shapeLayer {
|
||||
CATransaction.withDisabledAnimations {
|
||||
shapeLayer.strokeColor = color.cgColor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func heightWidthIsActive(_ isActive: Bool) {
|
||||
|
||||
heightConstraint?.isActive = isActive
|
||||
widthConstraint?.isActive = isActive
|
||||
}
|
||||
|
||||
private func checkAndBypassAnimations(selected: Bool) {
|
||||
updateSelectionOnly = true
|
||||
isSelected = selected
|
||||
updateSelectionOnly = false
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - UITouch
|
||||
//--------------------------------------------------
|
||||
|
||||
open override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
|
||||
sendActions(for: .touchUpInside)
|
||||
}
|
||||
|
||||
override open func accessibilityActivate() -> Bool {
|
||||
guard isEnabled else { return false }
|
||||
sendActions(for: .touchUpInside)
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Molecular
|
||||
//--------------------------------------------------
|
||||
|
||||
open func needsToBeConstrained() -> Bool { true }
|
||||
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
|
||||
isEnabled = true
|
||||
shapeLayer?.removeAllAnimations()
|
||||
shapeLayer?.removeFromSuperlayer()
|
||||
shapeLayer = nil
|
||||
backgroundColor = .clear
|
||||
borderColor = .mvmBlack
|
||||
borderWidth = 1
|
||||
checkColor = .mvmBlack
|
||||
checkWidth = 2
|
||||
checkAndBypassAnimations(selected: false)
|
||||
}
|
||||
|
||||
public override func updateView(_ size: CGFloat) {
|
||||
super.updateView(size)
|
||||
|
||||
if let dimension = sizeObject?.getValueBased(onSize: size) {
|
||||
widthConstraint?.constant = dimension
|
||||
heightConstraint?.constant = dimension
|
||||
}
|
||||
}
|
||||
open func horizontalAlignment() -> UIStackView.Alignment { .leading }
|
||||
|
||||
open func updateView(_ size: CGFloat) {}
|
||||
|
||||
private func performCheckboxAction(with actionModel: ActionModelProtocol, delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) {
|
||||
MVMCoreUIActionHandler.performActionUnstructured(with: actionModel, sourceModel: checkboxModel, additionalData: additionalData, delegateObject: delegateObject)
|
||||
MVMCoreUIActionHandler.performActionUnstructured(with: actionModel, sourceModel: viewModel, additionalData: additionalData, delegateObject: delegateObject)
|
||||
}
|
||||
|
||||
public override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
|
||||
super.set(with: model, delegateObject, additionalData)
|
||||
self.delegateObject = delegateObject
|
||||
|
||||
guard let model = model as? CheckboxModel else { return }
|
||||
|
||||
FormValidator.setupValidation(for: model, delegate: delegateObject?.formHolderDelegate)
|
||||
|
||||
if let fieldKey = model.fieldKey {
|
||||
|
||||
public func viewModelDidUpdate() {
|
||||
if let accessibilityIdentifier = viewModel.accessibilityIdentifier {
|
||||
self.accessibilityIdentifier = accessibilityIdentifier
|
||||
}
|
||||
//forms
|
||||
FormValidator.setupValidation(for: viewModel, delegate: delegateObject?.formHolderDelegate)
|
||||
groupName = viewModel.groupName
|
||||
if let fieldKey = viewModel.fieldKey {
|
||||
self.fieldKey = fieldKey
|
||||
}
|
||||
|
||||
borderColor = (model.inverted ? model.invertedColor : model.borderColor).uiColor
|
||||
borderWidth = model.borderWidth
|
||||
|
||||
checkColor = (model.inverted ? model.invertedColor : model.checkColor).uiColor
|
||||
unCheckedBackgroundColor = (model.inverted ? model.invertedBackgroundColor : model.unCheckedBackgroundColor).uiColor
|
||||
checkedBackgroundColor = (model.inverted ? model.invertedBackgroundColor : model.checkedBackgroundColor).uiColor
|
||||
disabledCheckColor = (model.inverted ? model.invertedColor : model.disabledCheckColor).uiColor
|
||||
disabledBorderColor = (model.inverted ? model.invertedColor : model.disabledBorderColor).uiColor
|
||||
disabledBackgroundColor = (model.inverted ? model.invertedColor : model.disabledBackgroundColor).uiColor
|
||||
|
||||
isAnimated = model.animated
|
||||
isRound = model.round
|
||||
|
||||
if model.selected {
|
||||
checkAndBypassAnimations(selected: model.selected)
|
||||
}
|
||||
|
||||
model.updateUI = { [weak self] in
|
||||
MVMCoreDispatchUtility.performBlock(onMainThread: {
|
||||
//properties
|
||||
isEnabled = viewModel.isEnabled
|
||||
isAnimated = viewModel.animated
|
||||
|
||||
//call super here to go around the didSet
|
||||
//in this class
|
||||
super.isSelected = viewModel.selected
|
||||
|
||||
//events
|
||||
viewModel.updateUI = {
|
||||
MVMCoreDispatchUtility.performBlock(onMainThread: { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.isEnabled = model.enabled
|
||||
//let isValid = viewModel.isValid ?? true
|
||||
//TODO: Fix issue with default state
|
||||
//showError = !isValid
|
||||
isEnabled = viewModel.enabled
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
isEnabled = model.enabled && !model.readOnly
|
||||
|
||||
if (model.action != nil || model.offAction != nil) {
|
||||
//onChange
|
||||
if (viewModel.action != nil || viewModel.offAction != nil) {
|
||||
actionBlock = { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
if let offAction = model.offAction, !self.isSelected {
|
||||
self.performCheckboxAction(with: offAction, delegateObject: delegateObject, additionalData: additionalData)
|
||||
if let offAction = viewModel.offAction, !isSelected {
|
||||
performCheckboxAction(with: offAction, delegateObject: delegateObject, additionalData: additionalData)
|
||||
|
||||
} else if let action = model.action {
|
||||
self.performCheckboxAction(with: action, delegateObject: delegateObject, additionalData: additionalData)
|
||||
} else if let action = viewModel.action {
|
||||
performCheckboxAction(with: action, delegateObject: delegateObject, additionalData: additionalData)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
// Created by Chintakrinda, Arun Kumar (Arun) on 21/01/20.
|
||||
// Copyright © 2020 Verizon Wireless. All rights reserved.
|
||||
//
|
||||
import VDS
|
||||
|
||||
/// Protocol to apply to any model of a UI Control with a binary on/off nature.
|
||||
///
|
||||
@ -13,67 +14,25 @@
|
||||
var selected: Bool { get set }
|
||||
}
|
||||
|
||||
@objcMembers public class CheckboxModel: MoleculeModelProtocol, SelectableMoleculeModelProtocol, FormFieldProtocol, UIUpdatableModelProtocol {
|
||||
@objcMembers public class CheckboxModel: FormFieldModel, SelectableMoleculeModelProtocol{
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Properties
|
||||
//--------------------------------------------------
|
||||
|
||||
public static var identifier: String = "checkbox"
|
||||
public var id: String = UUID().uuidString
|
||||
public var backgroundColor: Color?
|
||||
public var accessibilityIdentifier: String?
|
||||
public static override var identifier: String { "checkbox" }
|
||||
public var selected: Bool = false
|
||||
public var enabled: Bool = true
|
||||
public var readOnly: Bool = false
|
||||
public var animated: Bool = true
|
||||
public var inverted: Bool = false
|
||||
public var round: Bool = false
|
||||
public var borderWidth: CGFloat = 1
|
||||
public var borderColor: Color = Color(uiColor: .mvmBlack)
|
||||
public var checkColor: Color = Color(uiColor: .mvmBlack)
|
||||
public var unCheckedBackgroundColor: Color = Color(uiColor: .clear)
|
||||
public var checkedBackgroundColor: Color = Color(uiColor: .clear)
|
||||
public var disabledBackgroundColor: Color = Color(uiColor: .clear)
|
||||
public var disabledBorderColor: Color = Color(uiColor: .mvmCoolGray3)
|
||||
public var disabledCheckColor: Color = Color(uiColor: .mvmCoolGray3)
|
||||
public var invertedColor: Color = Color(uiColor: .mvmWhite)
|
||||
public var invertedBackgroundColor: Color = Color(uiColor: .mvmBlack)
|
||||
public var action: ActionModelProtocol?
|
||||
public var offAction: ActionModelProtocol?
|
||||
|
||||
public var fieldKey: String?
|
||||
public var groupName: String = FormValidator.defaultGroupName
|
||||
public var baseValue: AnyHashable?
|
||||
public var updateUI: ActionBlock?
|
||||
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Keys
|
||||
//--------------------------------------------------
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case moleculeName
|
||||
case accessibilityIdentifier
|
||||
case checked
|
||||
case enabled
|
||||
case readOnly
|
||||
case inverted
|
||||
case animated
|
||||
case round
|
||||
case borderWidth
|
||||
case borderColor
|
||||
case checkColor
|
||||
case invertedColor
|
||||
case invertedBackgroundColor
|
||||
case unCheckedBackgroundColor
|
||||
case checkedBackgroundColor
|
||||
case disabledBackgroundColor
|
||||
case disabledCheckColor
|
||||
case disabledBorderColor
|
||||
case action
|
||||
case fieldKey
|
||||
case groupName
|
||||
case offAction
|
||||
}
|
||||
|
||||
@ -81,16 +40,17 @@
|
||||
// MARK: - Form Validation
|
||||
//--------------------------------------------------
|
||||
|
||||
public func formFieldValue() -> AnyHashable? {
|
||||
open override func formFieldValue() -> AnyHashable? {
|
||||
guard enabled else { return nil }
|
||||
return selected
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Server Value
|
||||
//--------------------------------------------------
|
||||
open func formFieldServerValue() -> AnyHashable? {
|
||||
return formFieldValue()
|
||||
open override func setValidity(_ valid: Bool, errorMessage: String?) {
|
||||
if let ruleErrorMessage = errorMessage, fieldKey != nil {
|
||||
self.errorMessage = ruleErrorMessage
|
||||
}
|
||||
isValid = valid
|
||||
updateUI?()
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
@ -98,7 +58,8 @@
|
||||
//--------------------------------------------------
|
||||
|
||||
public init(isChecked: Bool = false) {
|
||||
self.selected = isChecked
|
||||
super.init()
|
||||
selected = isChecked
|
||||
baseValue = isChecked
|
||||
}
|
||||
|
||||
@ -107,52 +68,9 @@
|
||||
//--------------------------------------------------
|
||||
|
||||
required public init(from decoder: Decoder) throws {
|
||||
try super.init(from: decoder)
|
||||
|
||||
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString
|
||||
|
||||
accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier)
|
||||
|
||||
if let borderWidth = try typeContainer.decodeIfPresent(CGFloat.self, forKey: .borderWidth) {
|
||||
self.borderWidth = borderWidth
|
||||
}
|
||||
|
||||
if let borderColor = try typeContainer.decodeIfPresent(Color.self, forKey: .borderColor) {
|
||||
self.borderColor = borderColor
|
||||
}
|
||||
|
||||
if let checkColor = try typeContainer.decodeIfPresent(Color.self, forKey: .checkColor) {
|
||||
self.checkColor = checkColor
|
||||
}
|
||||
|
||||
if let unCheckedBackgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .unCheckedBackgroundColor) {
|
||||
self.unCheckedBackgroundColor = unCheckedBackgroundColor
|
||||
}
|
||||
|
||||
if let checkedBackgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .checkedBackgroundColor) {
|
||||
self.checkedBackgroundColor = checkedBackgroundColor
|
||||
}
|
||||
|
||||
if let disabledBackgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .disabledBackgroundColor) {
|
||||
self.disabledBackgroundColor = disabledBackgroundColor
|
||||
}
|
||||
|
||||
if let disabledBorderColor = try typeContainer.decodeIfPresent(Color.self, forKey: .disabledBorderColor) {
|
||||
self.disabledBorderColor = disabledBorderColor
|
||||
}
|
||||
|
||||
if let disabledCheckColor = try typeContainer.decodeIfPresent(Color.self, forKey: .disabledCheckColor) {
|
||||
self.disabledCheckColor = disabledCheckColor
|
||||
}
|
||||
|
||||
if let invertedColor = try typeContainer.decodeIfPresent(Color.self, forKey: .invertedColor) {
|
||||
self.invertedColor = invertedColor
|
||||
}
|
||||
|
||||
if let invertedBackgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .invertedBackgroundColor) {
|
||||
self.invertedBackgroundColor = invertedBackgroundColor
|
||||
}
|
||||
|
||||
if let checked = try typeContainer.decodeIfPresent(Bool.self, forKey: .checked) {
|
||||
self.selected = checked
|
||||
}
|
||||
@ -162,51 +80,26 @@
|
||||
if let animated = try typeContainer.decodeIfPresent(Bool.self, forKey: .animated) {
|
||||
self.animated = animated
|
||||
}
|
||||
|
||||
if let round = try typeContainer.decodeIfPresent(Bool.self, forKey: .round) {
|
||||
self.round = round
|
||||
}
|
||||
|
||||
if let inverted = try typeContainer.decodeIfPresent(Bool.self, forKey: .inverted) {
|
||||
self.inverted = inverted
|
||||
}
|
||||
|
||||
enabled = try typeContainer.decodeIfPresent(Bool.self, forKey: .enabled) ?? true
|
||||
readOnly = try typeContainer.decodeIfPresent(Bool.self, forKey: .readOnly) ?? false
|
||||
|
||||
action = try typeContainer.decodeModelIfPresent(codingKey: .action)
|
||||
fieldKey = try typeContainer.decodeIfPresent(String.self, forKey: .fieldKey)
|
||||
|
||||
if let groupName = try typeContainer.decodeIfPresent(String.self, forKey: .groupName) {
|
||||
self.groupName = groupName
|
||||
}
|
||||
offAction = try typeContainer.decodeModelIfPresent(codingKey: .offAction)
|
||||
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
public override func encode(to encoder: Encoder) throws {
|
||||
try super.encode(to: encoder)
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(id, forKey: .id)
|
||||
try container.encode(moleculeName, forKey: .moleculeName)
|
||||
try container.encodeIfPresent(groupName, forKey: .groupName)
|
||||
try container.encodeIfPresent(fieldKey, forKey: .fieldKey)
|
||||
try container.encodeIfPresent(borderColor, forKey: .borderColor)
|
||||
try container.encode(borderWidth, forKey: .borderWidth)
|
||||
try container.encode(selected, forKey: .checked)
|
||||
try container.encode(inverted, forKey: .inverted)
|
||||
try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier)
|
||||
try container.encodeIfPresent(checkColor, forKey: .checkColor)
|
||||
try container.encodeIfPresent(invertedColor, forKey: .invertedColor)
|
||||
try container.encodeIfPresent(invertedBackgroundColor, forKey: .invertedBackgroundColor)
|
||||
try container.encodeIfPresent(unCheckedBackgroundColor, forKey: .unCheckedBackgroundColor)
|
||||
try container.encodeIfPresent(checkedBackgroundColor, forKey: .checkedBackgroundColor)
|
||||
try container.encodeIfPresent(disabledBorderColor, forKey: .disabledBorderColor)
|
||||
try container.encodeIfPresent(disabledBackgroundColor, forKey: .disabledBackgroundColor)
|
||||
try container.encodeIfPresent(disabledCheckColor, forKey: .disabledCheckColor)
|
||||
try container.encodeIfPresent(animated, forKey: .animated)
|
||||
try container.encodeIfPresent(round, forKey: .round)
|
||||
try container.encode(enabled, forKey: .enabled)
|
||||
try container.encode(readOnly, forKey: .readOnly)
|
||||
try container.encodeModelIfPresent(action, forKey: .action)
|
||||
try container.encodeIfPresent(groupName, forKey: .groupName)
|
||||
try container.encodeModelIfPresent(offAction, forKey: .offAction)
|
||||
}
|
||||
|
||||
open override func isEqual(to model: any ModelComparisonProtocol) -> Bool {
|
||||
guard super.isEqual(to: model), let model = model as? Self else { return false }
|
||||
return selected == model.selected
|
||||
&& animated == model.animated
|
||||
&& offAction.isEqual(to: model.offAction)
|
||||
&& action.isEqual(to: model.action)
|
||||
}
|
||||
}
|
||||
|
||||
58
MVMCoreUI/Atomic/Atoms/Selectors/Checkboxes.swift
Normal file
58
MVMCoreUI/Atomic/Atoms/Selectors/Checkboxes.swift
Normal file
@ -0,0 +1,58 @@
|
||||
//
|
||||
// Checkboxes.swift
|
||||
// MVMCoreUI
|
||||
//
|
||||
// Created by Matt Bruce on 8/19/24.
|
||||
// Copyright © 2024 Verizon Wireless. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import VDS
|
||||
|
||||
open class Checkboxes: VDS.CheckboxGroup, VDSMoleculeViewProtocol {
|
||||
|
||||
//------------------------------------------------------
|
||||
// MARK: - Properties
|
||||
//------------------------------------------------------
|
||||
open var viewModel: CheckboxesModel!
|
||||
open var delegateObject: MVMCoreUIDelegateObject?
|
||||
open var additionalData: [AnyHashable : Any]?
|
||||
|
||||
// Form Validation
|
||||
var fieldKey: String?
|
||||
var fieldValue: JSONValue?
|
||||
var groupName: String?
|
||||
|
||||
/// The models for the molecules.
|
||||
public var checkboxes: [CheckboxLabelModel]?
|
||||
|
||||
// MARK: - MoleculeViewProtocol
|
||||
public func viewModelDidUpdate() {
|
||||
if let accessibilityIdentifier = viewModel.accessibilityIdentifier {
|
||||
self.accessibilityIdentifier = accessibilityIdentifier
|
||||
}
|
||||
surface = viewModel.surface
|
||||
showError = viewModel.showError
|
||||
isEnabled = viewModel.enabled && !viewModel.readOnly
|
||||
checkboxes = viewModel.checkboxes
|
||||
checkboxes?.forEach {
|
||||
FormValidator.setupValidation(for: $0.checkbox, delegate: delegateObject?.formHolderDelegate)
|
||||
}
|
||||
|
||||
selectorModels = viewModel.checkboxes.convertToVDSCheckboxItemModel(surface: surface,
|
||||
delegateObject: delegateObject,
|
||||
additionalData: additionalData)
|
||||
}
|
||||
|
||||
open func updateView(_ size: CGFloat) {}
|
||||
|
||||
open override func didSelect(_ selectedControl: CheckboxItem) {
|
||||
super.didSelect(selectedControl)
|
||||
|
||||
// since the checkboxes has the state being tracked, we need to update the values here.
|
||||
if let index = items.firstIndex(where: {$0 === selectedControl}), let selected = checkboxes?[index] {
|
||||
selected.checkbox.selected = true
|
||||
}
|
||||
_ = FormValidator.validate(delegate: delegateObject?.formHolderDelegate)
|
||||
}
|
||||
}
|
||||
101
MVMCoreUI/Atomic/Atoms/Selectors/CheckboxesModel.swift
Normal file
101
MVMCoreUI/Atomic/Atoms/Selectors/CheckboxesModel.swift
Normal file
@ -0,0 +1,101 @@
|
||||
//
|
||||
// CheckboxesModel.swift
|
||||
// MVMCoreUI
|
||||
//
|
||||
// Created by Matt Bruce on 8/19/24.
|
||||
// Copyright © 2024 Verizon Wireless. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MVMCore
|
||||
import VDS
|
||||
|
||||
public class CheckboxesModel: MoleculeModelProtocol, ParentMoleculeModelProtocol {
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Properties
|
||||
//--------------------------------------------------
|
||||
|
||||
public static var identifier: String { "checkboxes" }
|
||||
public var id: String = UUID().uuidString
|
||||
|
||||
public var backgroundColor: Color?
|
||||
public var accessibilityIdentifier: String?
|
||||
|
||||
public var enabled: Bool = true
|
||||
public var required: Bool = true
|
||||
public var readOnly: Bool = false
|
||||
public var showError: Bool = false
|
||||
public var inverted: Bool = false
|
||||
public var surface: Surface { inverted ? .dark : .light }
|
||||
|
||||
public var checkboxes: [CheckboxLabelModel]
|
||||
|
||||
public var children: [any MoleculeModelProtocol] { checkboxes }
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Keys
|
||||
//--------------------------------------------------
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case moleculeName
|
||||
case accessibilityIdentifier
|
||||
case inverted
|
||||
case enabled
|
||||
case readOnly
|
||||
case showError
|
||||
case checkboxes
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Initializer
|
||||
//--------------------------------------------------
|
||||
|
||||
public init(with checkboxes: [CheckboxLabelModel]){
|
||||
self.checkboxes = checkboxes
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Codec
|
||||
//--------------------------------------------------
|
||||
|
||||
required public init(from decoder: Decoder) throws {
|
||||
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString
|
||||
accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier)
|
||||
showError = try typeContainer.decodeIfPresent(Bool.self, forKey: .showError) ?? false
|
||||
enabled = try typeContainer.decodeIfPresent(Bool.self, forKey: .enabled) ?? true
|
||||
readOnly = try typeContainer.decodeIfPresent(Bool.self, forKey: .readOnly) ?? false
|
||||
|
||||
if let inverted = try typeContainer.decodeIfPresent(Bool.self, forKey: .inverted) {
|
||||
self.inverted = inverted
|
||||
}
|
||||
checkboxes = try typeContainer.decode([CheckboxLabelModel].self, forKey: .checkboxes)
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(id, forKey: .id)
|
||||
try container.encodeIfPresent(moleculeName, forKey: .moleculeName)
|
||||
try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier)
|
||||
try container.encode(readOnly, forKey: .readOnly)
|
||||
try container.encode(enabled, forKey: .enabled)
|
||||
try container.encode(inverted, forKey: .inverted)
|
||||
try container.encode(checkboxes, forKey: .checkboxes)
|
||||
}
|
||||
|
||||
open func isEqual(to model: any ModelComparisonProtocol) -> Bool {
|
||||
guard let model = model as? Self else { return false }
|
||||
return moleculeName == model.moleculeName
|
||||
&& enabled == model.enabled
|
||||
&& showError == model.showError
|
||||
&& readOnly == model.readOnly
|
||||
&& required == model.required
|
||||
&& inverted == model.inverted
|
||||
&& accessibilityText == model.accessibilityText
|
||||
&& accessibilityIdentifier == model.accessibilityIdentifier
|
||||
&& accessibilityTraits == model.accessibilityTraits
|
||||
}
|
||||
}
|
||||
@ -5,243 +5,66 @@
|
||||
// Created by Scott Pfeil on 4/9/20.
|
||||
// Copyright © 2020 Verizon Wireless. All rights reserved.
|
||||
//
|
||||
import VDS
|
||||
|
||||
|
||||
open class RadioBox: Control, MFButtonProtocol {
|
||||
//--------------------------------------------------
|
||||
@objcMembers open class RadioBox: VDS.RadioBoxItem, VDSMoleculeViewProtocol, MFButtonProtocol {
|
||||
//------------------------------------------------------
|
||||
// MARK: - Properties
|
||||
//------------------------------------------------------
|
||||
open var viewModel: RadioBoxModel!
|
||||
open var delegateObject: MVMCoreUIDelegateObject?
|
||||
open var additionalData: [AnyHashable : Any]?
|
||||
|
||||
open var isOutOfStock: Bool {
|
||||
get { strikethrough }
|
||||
set {
|
||||
strikethrough = newValue
|
||||
viewModel?.strikethrough = newValue
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Lifecycle
|
||||
//--------------------------------------------------
|
||||
|
||||
public let label = Label(fontStyle: .RegularBodySmall)
|
||||
public let subTextLabel = Label(fontStyle: .RegularMicro)
|
||||
public var isOutOfStock = false
|
||||
public var accentColor = UIColor.mvmRed
|
||||
|
||||
public let innerPadding: CGFloat = 12.0
|
||||
|
||||
private var borderLayer: CALayer?
|
||||
private var strikeLayer: CALayer?
|
||||
private var maskLayer: CALayer?
|
||||
|
||||
public var subTextLabelHeightConstraint: NSLayoutConstraint?
|
||||
|
||||
private var delegateObject: MVMCoreUIDelegateObject?
|
||||
var additionalData: [AnyHashable: Any]?
|
||||
|
||||
public var radioBoxModel: RadioBoxModel? {
|
||||
model as? RadioBoxModel
|
||||
}
|
||||
|
||||
public override var isSelected: Bool {
|
||||
didSet { updateAccessibility() }
|
||||
}
|
||||
|
||||
public override var isEnabled: Bool {
|
||||
didSet { updateAccessibility() }
|
||||
}
|
||||
public func viewModelDidUpdate() {
|
||||
if let accessibilityIdentifier = viewModel.accessibilityIdentifier {
|
||||
self.accessibilityIdentifier = accessibilityIdentifier
|
||||
}
|
||||
text = viewModel.text
|
||||
subText = viewModel.subText
|
||||
subTextRight = viewModel.subTextRight
|
||||
strikethrough = viewModel.strikethrough
|
||||
isSelected = viewModel.selected
|
||||
isEnabled = viewModel.enabled && !viewModel.readOnly
|
||||
|
||||
onChange = { [weak self] _ in
|
||||
if let radioBoxModel = self?.viewModel, let actionModel = radioBoxModel.action {
|
||||
Task(priority: .userInitiated) { [weak self] in
|
||||
guard let self else { return }
|
||||
try await Button.performButtonAction(with: actionModel, button: self, delegateObject: delegateObject, additionalData: additionalData, sourceModel: radioBoxModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Functions
|
||||
//--------------------------------------------------
|
||||
|
||||
@objc open func selectBox() {
|
||||
toggle()
|
||||
}
|
||||
|
||||
@objc open func deselectBox() {
|
||||
toggle()
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - MVMCoreViewProtocol
|
||||
//--------------------------------------------------
|
||||
|
||||
open override func updateView(_ size: CGFloat) {
|
||||
super.updateView(size)
|
||||
label.updateView(size)
|
||||
subTextLabel.updateView(size)
|
||||
layer.setNeedsDisplay()
|
||||
}
|
||||
|
||||
open override func setupView() {
|
||||
super.setupView()
|
||||
|
||||
layer.delegate = self
|
||||
layer.borderColor = UIColor.mvmCoolGray6.cgColor
|
||||
layer.borderWidth = 1
|
||||
open func updateView(_ size: CGFloat) {}
|
||||
|
||||
label.numberOfLines = 1
|
||||
addSubview(label)
|
||||
NSLayoutConstraint.constraintPinSubview(label, pinTop: true, topConstant: innerPadding, pinBottom: false, bottomConstant: 0, pinLeft: true, leftConstant: innerPadding, pinRight: true, rightConstant: innerPadding)
|
||||
|
||||
subTextLabel.textColor = .mvmCoolGray6
|
||||
subTextLabel.numberOfLines = 1
|
||||
addSubview(subTextLabel)
|
||||
NSLayoutConstraint.constraintPinSubview(subTextLabel, pinTop: false, topConstant:0, pinBottom: false, bottomConstant: 0, pinLeft: true, leftConstant: innerPadding, pinRight: true, rightConstant: innerPadding)
|
||||
bottomAnchor.constraint(greaterThanOrEqualTo: subTextLabel.bottomAnchor, constant: innerPadding).isActive = true
|
||||
subTextLabel.topAnchor.constraint(equalTo: label.bottomAnchor, constant: 2).isActive = true
|
||||
subTextLabelHeightConstraint = subTextLabel.heightAnchor.constraint(equalToConstant: 0)
|
||||
subTextLabelHeightConstraint?.isActive = true
|
||||
|
||||
addTarget(self, action: #selector(selectBox), for: .touchUpInside)
|
||||
|
||||
isAccessibilityElement = true
|
||||
}
|
||||
|
||||
open override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
|
||||
super.set(with: model, delegateObject, additionalData)
|
||||
guard let model = model as? RadioBoxModel else { return }
|
||||
self.delegateObject = delegateObject
|
||||
self.additionalData = additionalData
|
||||
label.text = model.text
|
||||
subTextLabel.text = model.subText
|
||||
isOutOfStock = model.strikethrough
|
||||
subTextLabelHeightConstraint?.isActive = (subTextLabel.text?.count ?? 0) == 0
|
||||
if let color = model.selectedAccentColor?.uiColor {
|
||||
accentColor = color
|
||||
}
|
||||
isSelected = model.selected
|
||||
isEnabled = model.enabled && !model.readOnly
|
||||
}
|
||||
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
backgroundColor = .white
|
||||
accentColor = .mvmRed
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - State Handling
|
||||
//--------------------------------------------------
|
||||
|
||||
open override func draw(_ layer: CALayer, in ctx: CGContext) {
|
||||
// Draw the strikethrough
|
||||
strikeLayer?.removeFromSuperlayer()
|
||||
if isOutOfStock {
|
||||
let line = getStrikeThrough(color: isSelected ? .black : .mvmCoolGray6, thickness: 1)
|
||||
layer.addSublayer(line)
|
||||
strikeLayer = line
|
||||
}
|
||||
|
||||
// Draw the border
|
||||
borderLayer?.removeFromSuperlayer()
|
||||
if isSelected {
|
||||
layer.borderWidth = 0
|
||||
let border = getSelectedBorder()
|
||||
layer.addSublayer(border)
|
||||
borderLayer = border
|
||||
} else {
|
||||
layer.borderWidth = 1
|
||||
}
|
||||
|
||||
// Handle Mask
|
||||
maskLayer?.removeFromSuperlayer()
|
||||
if !isEnabled {
|
||||
let mask = getMaskLayer()
|
||||
layer.mask = mask
|
||||
maskLayer = mask
|
||||
}
|
||||
}
|
||||
|
||||
open override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
// Accounts for any size changes
|
||||
layer.setNeedsDisplay()
|
||||
}
|
||||
|
||||
@objc open func selectBox() {
|
||||
guard isEnabled, !isSelected else { return }
|
||||
isSelected = true
|
||||
radioBoxModel?.selected = isSelected
|
||||
if let radioBoxModel = radioBoxModel, let actionModel = radioBoxModel.action {
|
||||
Task(priority: .userInitiated) {
|
||||
try await Button.performButtonAction(with: actionModel, button: self, delegateObject: delegateObject, additionalData: additionalData, sourceModel: radioBoxModel)
|
||||
}
|
||||
}
|
||||
layer.setNeedsDisplay()
|
||||
}
|
||||
|
||||
@objc open func deselectBox() {
|
||||
isSelected = false
|
||||
radioBoxModel?.selected = isSelected
|
||||
layer.setNeedsDisplay()
|
||||
}
|
||||
|
||||
/// Gets the selected state border
|
||||
func getSelectedBorder() -> CAShapeLayer {
|
||||
let layer = CAShapeLayer()
|
||||
|
||||
let topLineWidth: CGFloat = 4
|
||||
let topLinePath = UIBezierPath()
|
||||
topLinePath.lineWidth = topLineWidth
|
||||
topLinePath.move(to: CGPoint(x: 0, y: topLineWidth / 2.0))
|
||||
topLinePath.addLine(to: CGPoint(x: bounds.width, y: topLineWidth / 2.0))
|
||||
|
||||
let topLineLayer = CAShapeLayer()
|
||||
topLineLayer.fillColor = nil
|
||||
topLineLayer.strokeColor = accentColor.cgColor
|
||||
topLineLayer.lineWidth = 4
|
||||
topLineLayer.path = topLinePath.cgPath
|
||||
layer.addSublayer(topLineLayer)
|
||||
|
||||
let lineWidth: CGFloat = 1
|
||||
let halfLineWidth: CGFloat = 0.5
|
||||
let linePath = UIBezierPath()
|
||||
linePath.move(to: CGPoint(x: halfLineWidth, y: topLineWidth))
|
||||
linePath.addLine(to: CGPoint(x: halfLineWidth, y: bounds.height))
|
||||
linePath.move(to: CGPoint(x: 0, y: bounds.height - halfLineWidth))
|
||||
linePath.addLine(to: CGPoint(x: bounds.width, y: bounds.height - halfLineWidth))
|
||||
linePath.move(to: CGPoint(x: bounds.width - halfLineWidth, y: bounds.height))
|
||||
linePath.addLine(to: CGPoint(x: bounds.width - halfLineWidth, y: topLineWidth))
|
||||
|
||||
let borderLayer = CAShapeLayer()
|
||||
borderLayer.fillColor = nil
|
||||
borderLayer.strokeColor = UIColor.black.cgColor
|
||||
borderLayer.lineWidth = lineWidth
|
||||
borderLayer.path = linePath.cgPath
|
||||
layer.addSublayer(borderLayer)
|
||||
|
||||
return layer
|
||||
}
|
||||
|
||||
/// Adds a border to edge
|
||||
func getStrikeThrough(color: UIColor, thickness: CGFloat) -> CAShapeLayer {
|
||||
let border = CAShapeLayer()
|
||||
border.name = "strikethrough"
|
||||
border.fillColor = nil
|
||||
border.opacity = 1.0
|
||||
border.lineWidth = thickness
|
||||
border.strokeColor = color.cgColor
|
||||
|
||||
let linePath = UIBezierPath()
|
||||
linePath.move(to: CGPoint(x: 0, y: bounds.height))
|
||||
linePath.addLine(to: CGPoint(x: bounds.width, y: 0))
|
||||
border.path = linePath.cgPath
|
||||
return border
|
||||
}
|
||||
|
||||
func getMaskLayer() -> CALayer {
|
||||
let mask = CALayer()
|
||||
mask.backgroundColor = UIColor.white.cgColor
|
||||
mask.opacity = 0.3
|
||||
mask.frame = bounds
|
||||
return mask
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Accessibility
|
||||
//--------------------------------------------------
|
||||
|
||||
public func updateAccessibility() {
|
||||
|
||||
var message = ""
|
||||
|
||||
if let labelText = label.text, label.hasText {
|
||||
message += labelText + ", "
|
||||
}
|
||||
|
||||
if let subLabelText = subTextLabel.text, subTextLabel.hasText {
|
||||
message += subLabelText + ", "
|
||||
}
|
||||
|
||||
accessibilityLabel = message
|
||||
accessibilityTraits = .button
|
||||
|
||||
if isSelected {
|
||||
accessibilityTraits.insert(.selected)
|
||||
}
|
||||
|
||||
if !isEnabled {
|
||||
accessibilityTraits.insert(.notEnabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -6,45 +6,35 @@
|
||||
// Copyright © 2020 Verizon Wireless. All rights reserved.
|
||||
//
|
||||
import MVMCore
|
||||
import VDS
|
||||
|
||||
@objcMembers public class RadioBoxModel: MoleculeModelProtocol, EnableableModelProtocol {
|
||||
public class RadioBoxModel: FormFieldModel {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Properties
|
||||
//--------------------------------------------------
|
||||
|
||||
public static var identifier: String = "radioBox"
|
||||
public var id: String = UUID().uuidString
|
||||
public override static var identifier: String { "radioBox" }
|
||||
|
||||
public var text: String
|
||||
public var subText: String?
|
||||
public var backgroundColor: Color?
|
||||
public var accessibilityIdentifier: String?
|
||||
public var selectedAccentColor: Color?
|
||||
public var subTextRight: String?
|
||||
public var selected: Bool = false
|
||||
public var enabled: Bool = true
|
||||
public var readOnly: Bool = false
|
||||
public var strikethrough: Bool = false
|
||||
public var fieldValue: String?
|
||||
public var action: ActionModelProtocol?
|
||||
|
||||
public var fieldValue: String?
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Keys
|
||||
//--------------------------------------------------
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case moleculeName
|
||||
case text
|
||||
case subText
|
||||
case selectedAccentColor
|
||||
case backgroundColor
|
||||
case accessibilityIdentifier
|
||||
case subTextRight
|
||||
case selected
|
||||
case enabled
|
||||
case strikethrough
|
||||
case fieldValue
|
||||
case action
|
||||
case readOnly
|
||||
case fieldValue
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
@ -53,8 +43,19 @@ import MVMCore
|
||||
|
||||
public init(text: String) {
|
||||
self.text = text
|
||||
super.init()
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Form Validation
|
||||
//--------------------------------------------------
|
||||
|
||||
/// Returns the fieldValue of the selected box, otherwise the text of the selected box.
|
||||
public override func formFieldValue() -> AnyHashable? {
|
||||
guard enabled else { return nil }
|
||||
return fieldValue
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Codec
|
||||
//--------------------------------------------------
|
||||
@ -62,42 +63,42 @@ import MVMCore
|
||||
required public init(from decoder: Decoder) throws {
|
||||
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString
|
||||
text = try typeContainer.decode(String.self, forKey: .text)
|
||||
subText = try typeContainer.decodeIfPresent(String.self, forKey: .subText)
|
||||
selectedAccentColor = try typeContainer.decodeIfPresent(Color.self, forKey: .selectedAccentColor)
|
||||
backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor)
|
||||
accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier)
|
||||
subTextRight = try typeContainer.decodeIfPresent(String.self, forKey: .subTextRight)
|
||||
|
||||
if let isSelected = try typeContainer.decodeIfPresent(Bool.self, forKey: .selected) {
|
||||
selected = isSelected
|
||||
}
|
||||
|
||||
enabled = try typeContainer.decodeIfPresent(Bool.self, forKey: .enabled) ?? true
|
||||
readOnly = try typeContainer.decodeIfPresent(Bool.self, forKey: .readOnly) ?? false
|
||||
|
||||
if let isStrikeTrough = try typeContainer.decodeIfPresent(Bool.self, forKey: .strikethrough) {
|
||||
strikethrough = isStrikeTrough
|
||||
}
|
||||
|
||||
fieldValue = try typeContainer.decodeIfPresent(String.self, forKey: .fieldValue)
|
||||
action = try typeContainer.decodeModelIfPresent(codingKey: .action)
|
||||
try super.init(from: decoder)
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
public override func encode(to encoder: Encoder) throws {
|
||||
try super.encode(to: encoder)
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(id, forKey: .id)
|
||||
try container.encode(moleculeName, forKey: .moleculeName)
|
||||
try container.encode(text, forKey: .text)
|
||||
try container.encodeIfPresent(subText, forKey: .subText)
|
||||
try container.encodeIfPresent(selectedAccentColor, forKey: .selectedAccentColor)
|
||||
try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor)
|
||||
try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier)
|
||||
try container.encodeIfPresent(subTextRight, forKey: .subTextRight)
|
||||
try container.encode(selected, forKey: .selected)
|
||||
try container.encode(enabled, forKey: .enabled)
|
||||
try container.encode(readOnly, forKey: .readOnly)
|
||||
try container.encode(strikethrough, forKey: .strikethrough)
|
||||
try container.encodeIfPresent(fieldValue, forKey: .fieldValue)
|
||||
try container.encodeModelIfPresent(action, forKey: .action)
|
||||
}
|
||||
|
||||
open override func isEqual(to model: any ModelComparisonProtocol) -> Bool {
|
||||
guard super.isEqual(to: model), let model = model as? Self else { return false }
|
||||
return text == model.text
|
||||
&& subText == model.subText
|
||||
&& subTextRight == model.subTextRight
|
||||
&& selected == model.selected
|
||||
&& strikethrough == model.strikethrough
|
||||
&& fieldValue == model.fieldValue
|
||||
&& action.isEqual(to: model.action)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -7,172 +7,49 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import VDS
|
||||
|
||||
public protocol RadioBoxSelectionDelegate: AnyObject {
|
||||
func selected(radioBox: RadioBoxModel)
|
||||
}
|
||||
open class RadioBoxes: VDS.RadioBoxGroup, VDSMoleculeViewProtocol {
|
||||
|
||||
//------------------------------------------------------
|
||||
// MARK: - Properties
|
||||
//------------------------------------------------------
|
||||
open var viewModel: RadioBoxesModel!
|
||||
open var delegateObject: MVMCoreUIDelegateObject?
|
||||
open var additionalData: [AnyHashable : Any]?
|
||||
|
||||
// Form Validation
|
||||
var fieldKey: String?
|
||||
var fieldValue: JSONValue?
|
||||
var groupName: String?
|
||||
|
||||
open class RadioBoxes: View {
|
||||
|
||||
public var collectionView: CollectionView!
|
||||
public var collectionViewHeight: NSLayoutConstraint!
|
||||
private let boxWidth: CGFloat = 151.0
|
||||
private let boxHeight: CGFloat = 64.0
|
||||
private var itemSpacing: CGFloat = 12.0
|
||||
private var numberOfColumns: CGFloat = 2.0
|
||||
private var radioBoxesModel: RadioBoxesModel? {
|
||||
return model as? RadioBoxesModel
|
||||
}
|
||||
public weak var radioDelegate: RadioBoxSelectionDelegate?
|
||||
private var delegateObject: MVMCoreUIDelegateObject?
|
||||
|
||||
/// The models for the molecules.
|
||||
public var boxes: [RadioBoxModel]?
|
||||
|
||||
private var size: CGFloat?
|
||||
|
||||
open override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
// Accounts for any collection size changes
|
||||
DispatchQueue.main.async {
|
||||
self.collectionView.collectionViewLayout.invalidateLayout()
|
||||
}
|
||||
}
|
||||
|
||||
open func updateAccessibilityValue(collectionView: UICollectionView, cell: RadioBoxCollectionViewCell, indexPath: IndexPath) {
|
||||
guard let format = MVMCoreUIUtility.hardcodedString(withKey: "index_string_of_total"),
|
||||
let indexString = MVMCoreUIUtility.getOrdinalString(forIndex: NSNumber(value: indexPath.row + 1)) else { return }
|
||||
let total = self.collectionView(collectionView, numberOfItemsInSection: indexPath.section)
|
||||
cell.accessibilityValue = String(format: format, indexString, total)
|
||||
}
|
||||
|
||||
// MARK: - MVMCoreViewProtocol
|
||||
open override func setupView() {
|
||||
super.setupView()
|
||||
collectionView = createCollectionView()
|
||||
addSubview(collectionView)
|
||||
NSLayoutConstraint.constraintPinSubview(toSuperview: collectionView)
|
||||
collectionViewHeight = collectionView.heightAnchor.constraint(equalToConstant: 300)
|
||||
collectionViewHeight?.isActive = true
|
||||
}
|
||||
|
||||
|
||||
// MARK: - MoleculeViewProtocol
|
||||
open override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
|
||||
super.set(with: model, delegateObject, additionalData)
|
||||
self.delegateObject = delegateObject
|
||||
|
||||
guard let model = model as? RadioBoxesModel else { return }
|
||||
boxes = model.boxes
|
||||
FormValidator.setupValidation(for: model, delegate: delegateObject?.formHolderDelegate)
|
||||
|
||||
backgroundColor = model.backgroundColor?.uiColor
|
||||
|
||||
registerCells()
|
||||
setHeight()
|
||||
collectionView.reloadData()
|
||||
}
|
||||
|
||||
@objc override open func updateView(_ size: CGFloat) {
|
||||
super.updateView(size)
|
||||
self.size = size
|
||||
itemSpacing = Padding.Component.gutterFor(size: size)
|
||||
collectionView.updateView(size)
|
||||
}
|
||||
|
||||
// MARK: - Creation
|
||||
|
||||
/// Creates the layout for the collection.
|
||||
open func createCollectionViewLayout() -> UICollectionViewLayout {
|
||||
let layout = UICollectionViewFlowLayout()
|
||||
layout.scrollDirection = .vertical
|
||||
layout.minimumLineSpacing = itemSpacing
|
||||
layout.minimumInteritemSpacing = itemSpacing
|
||||
return layout
|
||||
}
|
||||
|
||||
/// Creates the collection view.
|
||||
open func createCollectionView() -> CollectionView {
|
||||
let collection = CollectionView(frame: .zero, collectionViewLayout: createCollectionViewLayout())
|
||||
collection.dataSource = self
|
||||
collection.delegate = self
|
||||
return collection
|
||||
}
|
||||
|
||||
/// Registers the cells with the collection view
|
||||
open func registerCells() {
|
||||
collectionView.register(RadioBoxCollectionViewCell.self, forCellWithReuseIdentifier: "RadioBoxCollectionViewCell")
|
||||
}
|
||||
|
||||
// MARK: - JSON Setters
|
||||
open func setHeight() {
|
||||
guard let boxes = boxes, boxes.count > 0 else {
|
||||
collectionViewHeight.constant = 0
|
||||
return
|
||||
public func viewModelDidUpdate() {
|
||||
if let accessibilityIdentifier = viewModel.accessibilityIdentifier {
|
||||
self.accessibilityIdentifier = accessibilityIdentifier
|
||||
}
|
||||
boxes = viewModel.boxes
|
||||
surface = viewModel.surface
|
||||
selectorModels = viewModel.boxes.convertToVDSRadioBoxModel(surface: surface)
|
||||
FormValidator.setupValidation(for: viewModel, delegate: delegateObject?.formHolderDelegate)
|
||||
|
||||
// Calculate the height
|
||||
let rows = ceil(CGFloat(boxes.count) / numberOfColumns)
|
||||
let height = (rows * boxHeight) + ((rows - 1) * itemSpacing)
|
||||
collectionViewHeight?.constant = height
|
||||
}
|
||||
|
||||
open func updateView(_ size: CGFloat) {}
|
||||
|
||||
open override func didSelect(_ selectedControl: RadioBoxItem) {
|
||||
super.didSelect(selectedControl)
|
||||
|
||||
// since the boxes has the state being tracked, we need to update the values here.
|
||||
if let index = items.firstIndex(where: {$0 === selectedControl}), let selectedBox = boxes?[index] {
|
||||
boxes?.forEach { $0.selected = false }
|
||||
selectedBox.selected = true
|
||||
_ = FormValidator.validate(delegate: delegateObject?.formHolderDelegate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension RadioBoxes: UICollectionViewDelegateFlowLayout {
|
||||
open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
|
||||
let itemWidth: CGFloat = (collectionView.bounds.width - itemSpacing) / numberOfColumns
|
||||
return CGSize(width: itemWidth, height: boxHeight)
|
||||
}
|
||||
}
|
||||
|
||||
extension RadioBoxes: UICollectionViewDataSource {
|
||||
open func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||
return boxes?.count ?? 0
|
||||
}
|
||||
|
||||
open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||
guard let molecule = boxes?[indexPath.row],
|
||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "RadioBoxCollectionViewCell", for: indexPath) as? RadioBoxCollectionViewCell else {
|
||||
fatalError()
|
||||
}
|
||||
cell.reset()
|
||||
cell.radioBox.isUserInteractionEnabled = false
|
||||
|
||||
if let color = radioBoxesModel?.boxesColor {
|
||||
cell.radioBox.backgroundColor = color.uiColor
|
||||
}
|
||||
if let color = radioBoxesModel?.selectedAccentColor {
|
||||
cell.radioBox.accentColor = color.uiColor
|
||||
}
|
||||
|
||||
cell.set(with: molecule, delegateObject, nil)
|
||||
cell.updateView(size ?? collectionView.bounds.width)
|
||||
if molecule.selected {
|
||||
collectionView.selectItem(at: indexPath, animated: false, scrollPosition: .centeredVertically)
|
||||
}
|
||||
updateAccessibilityValue(collectionView: collectionView, cell: cell, indexPath: indexPath)
|
||||
cell.layoutIfNeeded()
|
||||
return cell
|
||||
}
|
||||
}
|
||||
|
||||
extension RadioBoxes: UICollectionViewDelegate {
|
||||
open func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
|
||||
guard let molecule = boxes?[indexPath.row] else { return false }
|
||||
return molecule.enabled
|
||||
}
|
||||
|
||||
open func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||
guard let cell = collectionView.cellForItem(at: indexPath) as? RadioBoxCollectionViewCell else { return }
|
||||
cell.radioBox.selectBox()
|
||||
_ = FormValidator.validate(delegate: delegateObject?.formHolderDelegate)
|
||||
cell.updateAccessibility()
|
||||
guard let radioBox = boxes?[indexPath.row] else { return }
|
||||
radioDelegate?.selected(radioBox: radioBox)
|
||||
}
|
||||
|
||||
open func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {
|
||||
guard let cell = collectionView.cellForItem(at: indexPath) as? RadioBoxCollectionViewCell else { return }
|
||||
cell.radioBox.deselectBox()
|
||||
cell.updateAccessibility()
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,43 +6,37 @@
|
||||
// Copyright © 2020 Verizon Wireless. All rights reserved.
|
||||
//
|
||||
import MVMCore
|
||||
import VDS
|
||||
|
||||
@objcMembers public class RadioBoxesModel: MoleculeModelProtocol, FormFieldProtocol {
|
||||
public class RadioBoxesModel: FormFieldModel, ParentMoleculeModelProtocol {
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Properties
|
||||
//--------------------------------------------------
|
||||
|
||||
public static var identifier: String = "radioBoxes"
|
||||
public var id: String = UUID().uuidString
|
||||
public override static var identifier: String { "radioBoxes" }
|
||||
|
||||
public var boxes: [RadioBoxModel]
|
||||
public var backgroundColor: Color?
|
||||
public var accessibilityIdentifier: String?
|
||||
public var selectedAccentColor: Color?
|
||||
public var boxesColor: Color?
|
||||
public var fieldKey: String?
|
||||
public var groupName: String = FormValidator.defaultGroupName
|
||||
public var baseValue: AnyHashable?
|
||||
public var enabled: Bool = true
|
||||
public var readOnly: Bool = false
|
||||
|
||||
public var children: [any MoleculeModelProtocol] { boxes }
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Form Validation
|
||||
//--------------------------------------------------
|
||||
|
||||
/// Returns the fieldValue of the selected box, otherwise the text of the selected box.
|
||||
public func formFieldValue() -> AnyHashable? {
|
||||
public override func formFieldValue() -> AnyHashable? {
|
||||
guard enabled else { return nil }
|
||||
let selectedBox = boxes.first { (box) -> Bool in
|
||||
return box.selected
|
||||
}
|
||||
return selectedBox?.fieldValue ?? selectedBox?.text
|
||||
return selectedBox?.formFieldValue() ?? selectedBox?.text
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Server Value
|
||||
//--------------------------------------------------
|
||||
open func formFieldServerValue() -> AnyHashable? {
|
||||
open override func formFieldServerValue() -> AnyHashable? {
|
||||
return formFieldValue()
|
||||
}
|
||||
|
||||
@ -51,17 +45,7 @@ import MVMCore
|
||||
//--------------------------------------------------
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case moleculeName
|
||||
case enabled
|
||||
case readOnly
|
||||
case selectedAccentColor
|
||||
case backgroundColor
|
||||
case accessibilityIdentifier
|
||||
case boxesColor
|
||||
case boxes
|
||||
case fieldKey
|
||||
case groupName
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
@ -69,7 +53,8 @@ import MVMCore
|
||||
//--------------------------------------------------
|
||||
|
||||
public init(with boxes: [RadioBoxModel]){
|
||||
self.boxes = boxes
|
||||
self.boxes = boxes
|
||||
super.init()
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
@ -78,32 +63,29 @@ import MVMCore
|
||||
|
||||
required public init(from decoder: Decoder) throws {
|
||||
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
|
||||
id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString
|
||||
selectedAccentColor = try typeContainer.decodeIfPresent(Color.self, forKey: .selectedAccentColor)
|
||||
backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor)
|
||||
accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier)
|
||||
boxesColor = try typeContainer.decodeIfPresent(Color.self, forKey: .boxesColor)
|
||||
boxes = try typeContainer.decode([RadioBoxModel].self, forKey: .boxes)
|
||||
fieldKey = try typeContainer.decodeIfPresent(String.self, forKey: .fieldKey)
|
||||
if let groupName = try typeContainer.decodeIfPresent(String.self, forKey: .groupName) {
|
||||
self.groupName = groupName
|
||||
}
|
||||
enabled = try typeContainer.decodeIfPresent(Bool.self, forKey: .enabled) ?? true
|
||||
readOnly = try typeContainer.decodeIfPresent(Bool.self, forKey: .readOnly) ?? false
|
||||
baseValue = formFieldValue()
|
||||
try super.init(from: decoder)
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
public override func encode(to encoder: Encoder) throws {
|
||||
try super.encode(to: encoder)
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(id, forKey: .id)
|
||||
try container.encode(moleculeName, forKey: .moleculeName)
|
||||
try container.encode(boxes, forKey: .boxes)
|
||||
try container.encodeIfPresent(selectedAccentColor, forKey: .selectedAccentColor)
|
||||
try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor)
|
||||
try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier)
|
||||
try container.encodeIfPresent(fieldKey, forKey: .fieldKey)
|
||||
try container.encode(groupName, forKey: .groupName)
|
||||
try container.encode(enabled, forKey: .enabled)
|
||||
try container.encode(readOnly, forKey: .readOnly)
|
||||
}
|
||||
}
|
||||
|
||||
extension Array where Element == RadioBoxModel {
|
||||
internal func convertToVDSRadioBoxModel(surface: Surface) -> [RadioBoxGroup.RadioBoxItemModel] {
|
||||
compactMap({ item in
|
||||
var radioBox = RadioBoxGroup.RadioBoxItemModel()
|
||||
radioBox.text = item.text
|
||||
radioBox.subText = item.subText
|
||||
radioBox.subTextRight = item.subTextRight
|
||||
radioBox.surface = surface
|
||||
radioBox.selected = item.selected
|
||||
radioBox.strikethrough = item.strikethrough
|
||||
radioBox.enabled = item.isEnabled
|
||||
return radioBox
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,37 +7,26 @@
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import VDSCoreTokens
|
||||
import VDS
|
||||
|
||||
@objcMembers open class RadioButton: Control, MFButtonProtocol {
|
||||
//--------------------------------------------------
|
||||
@objcMembers open class RadioButton: VDS.RadioButton, RadioButtonSelectionHelperProtocol, VDSMoleculeViewProtocol, MFButtonProtocol, MVMCoreUIViewConstrainingProtocol {
|
||||
//------------------------------------------------------
|
||||
// MARK: - Properties
|
||||
//--------------------------------------------------
|
||||
|
||||
public var diameter: CGFloat = 20 {
|
||||
didSet { widthConstraint?.constant = diameter }
|
||||
//------------------------------------------------------
|
||||
open var viewModel: RadioButtonModel!
|
||||
open var delegateObject: MVMCoreUIDelegateObject?
|
||||
open var additionalData: [AnyHashable : Any]?
|
||||
|
||||
open var radioButtonModel: RadioButtonModel {
|
||||
viewModel
|
||||
}
|
||||
|
||||
@objc public override var isSelected: Bool {
|
||||
didSet {
|
||||
radioModel?.state = isSelected
|
||||
updateAccessibilityLabel()
|
||||
}
|
||||
}
|
||||
public var enabledColor: UIColor {
|
||||
return radioModel?.inverted ?? false ? VDSColor.elementsPrimaryOndark : VDSColor.elementsPrimaryOnlight
|
||||
}
|
||||
public var disabledColor: UIColor {
|
||||
return radioModel?.inverted ?? false ? VDSColor.interactiveDisabledOndark : VDSColor.interactiveDisabledOnlight
|
||||
}
|
||||
public var delegateObject: MVMCoreUIDelegateObject?
|
||||
var additionalData: [AnyHashable: Any]?
|
||||
|
||||
public var radioModel: RadioButtonModel? {
|
||||
model as? RadioButtonModel
|
||||
}
|
||||
|
||||
lazy public var radioGroupName: String? = { radioModel?.fieldKey }()
|
||||
// Form Validation
|
||||
open var fieldKey: String?
|
||||
open var fieldValue: JSONValue?
|
||||
open var groupName: String?
|
||||
|
||||
lazy public var radioGroupName: String? = { viewModel.fieldKey }()
|
||||
|
||||
lazy public var radioButtonSelectionHelper: RadioButtonSelectionHelper? = {
|
||||
|
||||
@ -48,132 +37,120 @@ import VDSCoreTokens
|
||||
return radioButtonModel
|
||||
}()
|
||||
|
||||
public override var isEnabled: Bool {
|
||||
didSet {
|
||||
isUserInteractionEnabled = isEnabled
|
||||
setNeedsDisplay()
|
||||
}
|
||||
//--------------------------------------------------
|
||||
// MARK: - Initializers
|
||||
//--------------------------------------------------
|
||||
|
||||
override public init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Constraints
|
||||
//--------------------------------------------------
|
||||
|
||||
public var widthConstraint: NSLayoutConstraint?
|
||||
public var heightConstraint: NSLayoutConstraint?
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Lifecycle
|
||||
//--------------------------------------------------
|
||||
|
||||
open override func draw(_ rect: CGRect) {
|
||||
guard let context = UIGraphicsGetCurrentContext() else { return }
|
||||
|
||||
let color = isEnabled == false ? disabledColor.cgColor : enabledColor.cgColor
|
||||
layer.cornerRadius = bounds.width * 0.5
|
||||
layer.borderColor = color
|
||||
layer.borderWidth = bounds.width * 0.0333
|
||||
|
||||
if isSelected {
|
||||
// Space around inner circle is 1/5 the size
|
||||
context.addEllipse(in: CGRect(x: bounds.width * 0.2,
|
||||
y: bounds.height * 0.2,
|
||||
width: bounds.width * 0.6,
|
||||
height: bounds.height * 0.6))
|
||||
context.setFillColor(color)
|
||||
context.fillPath()
|
||||
}
|
||||
/// There is currently no intention on using xib files.
|
||||
required public init?(coder aDecoder: NSCoder) {
|
||||
super.init(coder: aDecoder)
|
||||
fatalError("xib file is not implemented for Checkbox.")
|
||||
}
|
||||
|
||||
public convenience required init() {
|
||||
self.init(frame:.zero)
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Validation
|
||||
//--------------------------------------------------
|
||||
|
||||
/// The action performed when tapped.
|
||||
func tapAction() {
|
||||
if !isEnabled {
|
||||
return
|
||||
}
|
||||
|
||||
let wasPreviouslySelected = isSelected
|
||||
if let radioButtonModel = radioButtonSelectionHelper {
|
||||
radioButtonModel.selected(self)
|
||||
} else {
|
||||
isSelected = !isSelected
|
||||
}
|
||||
|
||||
if let radioModel = radioModel, let actionModel = radioModel.action, isSelected, !wasPreviouslySelected {
|
||||
Task(priority: .userInitiated) {
|
||||
try await Button.performButtonAction(with: actionModel, button: self, delegateObject: delegateObject, additionalData: additionalData, sourceModel: radioModel)
|
||||
}
|
||||
}
|
||||
|
||||
_ = FormValidator.validate(delegate: delegateObject?.formHolderDelegate)
|
||||
setNeedsDisplay()
|
||||
}
|
||||
|
||||
public func isValidField() -> Bool { isSelected }
|
||||
|
||||
public func formFieldName() -> String? {
|
||||
radioModel?.fieldKey
|
||||
viewModel.fieldKey
|
||||
}
|
||||
|
||||
public func formFieldGroupName() -> String? {
|
||||
radioModel?.fieldKey
|
||||
viewModel.fieldKey
|
||||
}
|
||||
|
||||
public func formFieldValue() -> AnyHashable? {
|
||||
guard let radioModel = radioModel, radioModel.enabled else { return nil }
|
||||
guard let radioModel = viewModel, radioModel.enabled else { return nil }
|
||||
return radioModel.fieldValue
|
||||
}
|
||||
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Methods
|
||||
//--------------------------------------------------
|
||||
|
||||
/// Adjust accessibility label based on state of RadioButton.
|
||||
func updateAccessibilityLabel() {
|
||||
if let message = MVMCoreUIUtility.hardcodedString(withKey: "radio_button"),
|
||||
let selectedState = MVMCoreUIUtility.hardcodedString(withKey: isSelected ? "radio_selected_state" : "radio_not_selected_state") {
|
||||
accessibilityLabel = message + selectedState
|
||||
// MARK: - Lifecycle
|
||||
//--------------------------------------------------
|
||||
open override func setup() {
|
||||
super.setup()
|
||||
|
||||
// Radio button should never be smaller that its content size.
|
||||
setContentCompressionResistancePriority(.required, for: .vertical)
|
||||
setContentCompressionResistancePriority(.required, for: .horizontal)
|
||||
|
||||
publisher(for: .valueChanged)
|
||||
.sink { [weak self] control in
|
||||
guard let self, isEnabled else { return }
|
||||
viewModel?.state = isSelected
|
||||
_ = FormValidator.validate(delegate: delegateObject?.formHolderDelegate)
|
||||
}.store(in: &subscribers)
|
||||
}
|
||||
|
||||
open func viewModelDidUpdate() {
|
||||
if let accessibilityIdentifier = viewModel.accessibilityIdentifier {
|
||||
self.accessibilityIdentifier = accessibilityIdentifier
|
||||
}
|
||||
//events
|
||||
viewModel.updateUI = {
|
||||
MVMCoreDispatchUtility.performBlock(onMainThread: { [weak self] in
|
||||
guard let self = self else { return }
|
||||
let isValid = viewModel.isValid ?? true
|
||||
showError = !isValid
|
||||
isEnabled = viewModel.enabled
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
isSelected = viewModel.state
|
||||
isEnabled = viewModel.isEnabled
|
||||
RadioButtonSelectionHelper.setupForRadioButtonGroup(viewModel, self, delegateObject: delegateObject)
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Overrides
|
||||
//--------------------------------------------------
|
||||
open override func toggle() {
|
||||
guard !isSelected, isEnabled else { return }
|
||||
|
||||
//removed error
|
||||
if showError && isSelected == false {
|
||||
showError.toggle()
|
||||
}
|
||||
|
||||
let wasPreviouslySelected = isSelected
|
||||
if let radioButtonSelectionHelper {
|
||||
radioButtonSelectionHelper.selected(self)
|
||||
} else {
|
||||
isSelected.toggle()
|
||||
}
|
||||
|
||||
if let actionModel = viewModel.action, isSelected, !wasPreviouslySelected {
|
||||
Task(priority: .userInitiated) {
|
||||
try await Button.performButtonAction(with: actionModel, button: self, delegateObject: delegateObject, additionalData: additionalData, sourceModel: viewModel)
|
||||
}
|
||||
}
|
||||
sendActions(for: .valueChanged)
|
||||
setNeedsUpdate()
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - MVMViewProtocol
|
||||
// MARK: - Actions
|
||||
//--------------------------------------------------
|
||||
|
||||
open override func setupView() {
|
||||
super.setupView()
|
||||
|
||||
backgroundColor = .clear
|
||||
clipsToBounds = true
|
||||
widthConstraint = widthAnchor.constraint(equalToConstant: 20)
|
||||
widthConstraint?.isActive = true
|
||||
heightConstraint = heightAnchor.constraint(equalTo: widthAnchor, multiplier: 1)
|
||||
heightConstraint?.isActive = true
|
||||
|
||||
addTarget(self, action: #selector(tapAction), for: .touchUpInside)
|
||||
isAccessibilityElement = true
|
||||
accessibilityHint = MVMCoreUIUtility.hardcodedString(withKey: "radio_action_hint")
|
||||
accessibilityTraits = .button
|
||||
updateAccessibilityLabel()
|
||||
/// This will toggle the state of the Checkbox and execute the actionBlock if provided.
|
||||
public func tapAction() {
|
||||
toggle()
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - MoleculeViewProtocol
|
||||
//--------------------------------------------------
|
||||
public func updateView(_ size: CGFloat) {}
|
||||
|
||||
public override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
|
||||
super.set(with: model, delegateObject, additionalData)
|
||||
self.delegateObject = delegateObject
|
||||
self.additionalData = additionalData
|
||||
|
||||
guard let model = model as? RadioButtonModel else { return }
|
||||
|
||||
isSelected = model.state
|
||||
isEnabled = model.enabled && !model.readOnly
|
||||
RadioButtonSelectionHelper.setupForRadioButtonGroup(model, self, delegateObject: delegateObject)
|
||||
}
|
||||
|
||||
public override func reset() {
|
||||
super.reset()
|
||||
backgroundColor = .clear
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,48 +7,29 @@
|
||||
//
|
||||
|
||||
import MVMCore
|
||||
import VDS
|
||||
|
||||
|
||||
open class RadioButtonModel: MoleculeModelProtocol, FormFieldProtocol {
|
||||
open class RadioButtonModel: FormFieldModel {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Properties
|
||||
//--------------------------------------------------
|
||||
|
||||
public static var identifier: String = "radioButton"
|
||||
public var id: String = UUID().uuidString
|
||||
|
||||
public var backgroundColor: Color?
|
||||
public var accessibilityIdentifier: String?
|
||||
public static override var identifier: String { "radioButton" }
|
||||
public var state: Bool = false
|
||||
public var enabled: Bool = true
|
||||
public var readOnly: Bool = false
|
||||
|
||||
/// The specific value to send to server. TODO: update this to be more generic.
|
||||
public var fieldValue: String?
|
||||
|
||||
public var baseValue: AnyHashable?
|
||||
public var groupName: String = FormValidator.defaultGroupName
|
||||
public var fieldKey: String?
|
||||
public var action: ActionModelProtocol?
|
||||
public var inverted: Bool = false
|
||||
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Keys
|
||||
//--------------------------------------------------
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case moleculeName
|
||||
case backgroundColor
|
||||
case accessibilityIdentifier
|
||||
case state
|
||||
case enabled
|
||||
case fieldValue
|
||||
case fieldKey
|
||||
case groupName
|
||||
case action
|
||||
case readOnly
|
||||
case inverted
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
@ -56,69 +37,57 @@ open class RadioButtonModel: MoleculeModelProtocol, FormFieldProtocol {
|
||||
//--------------------------------------------------
|
||||
|
||||
public init(_ state: Bool) {
|
||||
super.init()
|
||||
self.state = state
|
||||
baseValue = state
|
||||
self.baseValue = state
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Validation
|
||||
//--------------------------------------------------
|
||||
|
||||
public func formFieldValue() -> AnyHashable? {
|
||||
public override func formFieldValue() -> AnyHashable? {
|
||||
guard enabled else { return nil }
|
||||
return fieldValue
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Server Value
|
||||
//--------------------------------------------------
|
||||
open func formFieldServerValue() -> AnyHashable? {
|
||||
return formFieldValue()
|
||||
open override func setValidity(_ valid: Bool, errorMessage: String?) {
|
||||
if let ruleErrorMessage = errorMessage, fieldKey != nil {
|
||||
self.errorMessage = ruleErrorMessage
|
||||
}
|
||||
isValid = valid
|
||||
updateUI?()
|
||||
}
|
||||
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Codec
|
||||
//--------------------------------------------------
|
||||
|
||||
required public init(from decoder: Decoder) throws {
|
||||
try super.init(from: decoder)
|
||||
|
||||
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString
|
||||
|
||||
if let state = try typeContainer.decodeIfPresent(Bool.self, forKey: .state) {
|
||||
self.state = state
|
||||
}
|
||||
|
||||
enabled = try typeContainer.decodeIfPresent(Bool.self, forKey: .enabled) ?? true
|
||||
readOnly = try typeContainer.decodeIfPresent(Bool.self, forKey: .readOnly) ?? false
|
||||
backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor)
|
||||
accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier)
|
||||
|
||||
baseValue = state
|
||||
fieldKey = try typeContainer.decodeIfPresent(String.self, forKey: .fieldKey)
|
||||
if let groupName = try typeContainer.decodeIfPresent(String.self, forKey: .groupName) {
|
||||
self.groupName = groupName
|
||||
}
|
||||
fieldValue = try typeContainer.decodeIfPresent(String.self, forKey: .fieldValue)
|
||||
action = try typeContainer.decodeModelIfPresent(codingKey: .action)
|
||||
if let inverted = try typeContainer.decodeIfPresent(Bool.self, forKey: .inverted) {
|
||||
self.inverted = inverted
|
||||
}
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
public override func encode(to encoder: Encoder) throws {
|
||||
try super.encode(to: encoder)
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor)
|
||||
try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier)
|
||||
try container.encode(id, forKey: .id)
|
||||
try container.encode(moleculeName, forKey: .moleculeName)
|
||||
try container.encode(state, forKey: .state)
|
||||
try container.encode(enabled, forKey: .enabled)
|
||||
try container.encode(readOnly, forKey: .readOnly)
|
||||
try container.encodeIfPresent(fieldKey, forKey: .fieldKey)
|
||||
try container.encodeIfPresent(groupName, forKey: .groupName)
|
||||
try container.encodeIfPresent(fieldValue, forKey: .fieldValue)
|
||||
try container.encodeModelIfPresent(action, forKey: .action)
|
||||
try container.encodeIfPresent(inverted, forKey: .inverted)
|
||||
}
|
||||
|
||||
open override func isEqual(to model: any ModelComparisonProtocol) -> Bool {
|
||||
guard super.isEqual(to: model), let model = model as? Self else { return false }
|
||||
return state == model.state
|
||||
&& fieldValue == model.fieldValue
|
||||
&& action.isEqual(to: model.action)
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,6 +6,10 @@
|
||||
// Copyright © 2019 Verizon Wireless. All rights reserved.
|
||||
//
|
||||
|
||||
public protocol RadioButtonSelectionHelperProtocol: AnyObject {
|
||||
var isSelected: Bool { get set }
|
||||
var radioButtonModel: RadioButtonModel { get }
|
||||
}
|
||||
|
||||
@objcMembers public class RadioButtonSelectionHelper: FormFieldProtocol {
|
||||
//--------------------------------------------------
|
||||
@ -14,7 +18,7 @@
|
||||
|
||||
public var fieldKey: String?
|
||||
public var groupName: String = FormValidator.defaultGroupName
|
||||
private var selectedRadioButton: RadioButton?
|
||||
private var selectedRadioButton: RadioButtonSelectionHelperProtocol?
|
||||
private var selectedRadioButtonModel: RadioButtonModel?
|
||||
public var baseValue: AnyHashable?
|
||||
public var enabled: Bool = true
|
||||
@ -24,7 +28,7 @@
|
||||
// MARK: - Initializer
|
||||
//--------------------------------------------------
|
||||
|
||||
public func set(_ radioButtonModel: RadioButtonModel, _ radioButton: RadioButton) {
|
||||
public func set(_ radioButtonModel: RadioButtonModel, _ radioButton: RadioButtonSelectionHelperProtocol) {
|
||||
self.fieldKey = radioButtonModel.fieldKey
|
||||
self.groupName = radioButtonModel.groupName
|
||||
|
||||
@ -49,7 +53,7 @@
|
||||
// MARK: - Functions
|
||||
//--------------------------------------------------
|
||||
|
||||
public static func setupForRadioButtonGroup(_ radioButtonModel: RadioButtonModel, _ radioButton: RadioButton, delegateObject: MVMCoreUIDelegateObject?) {
|
||||
public static func setupForRadioButtonGroup(_ radioButtonModel: RadioButtonModel, _ radioButton: RadioButtonSelectionHelperProtocol, delegateObject: MVMCoreUIDelegateObject?) {
|
||||
|
||||
guard let groupName = radioButtonModel.fieldKey,
|
||||
let formValidator = delegateObject?.formHolderDelegate?.formValidator
|
||||
@ -61,10 +65,10 @@
|
||||
FormValidator.setupValidation(for: radioButtonSelectionHelper, delegate: delegateObject?.formHolderDelegate)
|
||||
}
|
||||
|
||||
public func selected(_ radioButton: RadioButton) {
|
||||
public func selected(_ radioButton: RadioButtonSelectionHelperProtocol) {
|
||||
|
||||
// Checks because the view could be reused
|
||||
if selectedRadioButton?.radioModel === selectedRadioButtonModel {
|
||||
if selectedRadioButton?.radioButtonModel === selectedRadioButtonModel {
|
||||
selectedRadioButton?.isSelected = false
|
||||
} else {
|
||||
selectedRadioButtonModel?.state = false
|
||||
@ -72,7 +76,7 @@
|
||||
|
||||
selectedRadioButton = radioButton
|
||||
selectedRadioButton?.isSelected = true
|
||||
selectedRadioButtonModel = selectedRadioButton?.radioModel
|
||||
selectedRadioButtonModel = selectedRadioButton?.radioButtonModel
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
57
MVMCoreUI/Atomic/Atoms/Selectors/RadioButtons.swift
Normal file
57
MVMCoreUI/Atomic/Atoms/Selectors/RadioButtons.swift
Normal file
@ -0,0 +1,57 @@
|
||||
//
|
||||
// RadioButtons.swift
|
||||
// MVMCoreUI
|
||||
//
|
||||
// Created by Matt Bruce on 8/20/24.
|
||||
// Copyright © 2024 Verizon Wireless. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import VDS
|
||||
|
||||
open class RadioButtons: VDS.RadioButtonGroup, VDSMoleculeViewProtocol {
|
||||
|
||||
//------------------------------------------------------
|
||||
// MARK: - Properties
|
||||
//------------------------------------------------------
|
||||
open var viewModel: RadioButtonsModel!
|
||||
open var delegateObject: MVMCoreUIDelegateObject?
|
||||
open var additionalData: [AnyHashable : Any]?
|
||||
|
||||
// Form Validation
|
||||
var fieldKey: String?
|
||||
var fieldValue: JSONValue?
|
||||
var groupName: String?
|
||||
|
||||
/// The models for the molecules.
|
||||
public var radioButtons: [RadioButtonLabelModel]?
|
||||
|
||||
// MARK: - MoleculeViewProtocol
|
||||
public func viewModelDidUpdate() {
|
||||
if let accessibilityIdentifier = viewModel.accessibilityIdentifier {
|
||||
self.accessibilityIdentifier = accessibilityIdentifier
|
||||
}
|
||||
showError = viewModel.showError
|
||||
isEnabled = viewModel.isEnabled
|
||||
surface = viewModel.surface
|
||||
|
||||
radioButtons = viewModel.radioButtons
|
||||
selectorModels = viewModel.radioButtons.convertToVDSRadioButtonItemModel(surface: surface,
|
||||
delegateObject: delegateObject,
|
||||
additionalData: additionalData)
|
||||
FormValidator.setupValidation(for: viewModel, delegate: delegateObject?.formHolderDelegate)
|
||||
}
|
||||
|
||||
open func updateView(_ size: CGFloat) {}
|
||||
|
||||
open override func didSelect(_ selectedControl: RadioButtonItem) {
|
||||
super.didSelect(selectedControl)
|
||||
|
||||
// since the radiobutton has the state being tracked, we need to update the values here.
|
||||
if let index = items.firstIndex(where: {$0 === selectedControl}), let selected = radioButtons?[index] {
|
||||
radioButtons?.forEach { $0.radioButton.state = false }
|
||||
selected.radioButton.state = true
|
||||
}
|
||||
_ = FormValidator.validate(delegate: delegateObject?.formHolderDelegate)
|
||||
}
|
||||
}
|
||||
74
MVMCoreUI/Atomic/Atoms/Selectors/RadioButtonsModel.swift
Normal file
74
MVMCoreUI/Atomic/Atoms/Selectors/RadioButtonsModel.swift
Normal file
@ -0,0 +1,74 @@
|
||||
//
|
||||
// RadioButtonsModel.swift
|
||||
// MVMCoreUI
|
||||
//
|
||||
// Created by Matt Bruce on 8/20/24.
|
||||
// Copyright © 2024 Verizon Wireless. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MVMCore
|
||||
import VDS
|
||||
|
||||
public class RadioButtonsModel: FormFieldModel, ParentMoleculeModelProtocol {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Properties
|
||||
//--------------------------------------------------
|
||||
|
||||
public override static var identifier: String { "radioButtons" }
|
||||
|
||||
public var radioButtons: [RadioButtonLabelModel]
|
||||
|
||||
public var children: [any MoleculeModelProtocol] { radioButtons }
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Form Validation
|
||||
//--------------------------------------------------
|
||||
|
||||
/// Returns the fieldValue of the selected RadioButton.
|
||||
public override func formFieldValue() -> AnyHashable? {
|
||||
guard enabled else { return nil }
|
||||
let selected = radioButtons.first { $0.radioButton.state }
|
||||
return selected?.radioButton.formFieldValue()
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Server Value
|
||||
//--------------------------------------------------
|
||||
open override func formFieldServerValue() -> AnyHashable? {
|
||||
return formFieldValue()
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Keys
|
||||
//--------------------------------------------------
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case radioButtons
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Initializer
|
||||
//--------------------------------------------------
|
||||
|
||||
public init(with radioButtons: [RadioButtonLabelModel]){
|
||||
self.radioButtons = radioButtons
|
||||
super.init()
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Codec
|
||||
//--------------------------------------------------
|
||||
|
||||
required public init(from decoder: Decoder) throws {
|
||||
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
|
||||
radioButtons = try typeContainer.decode([RadioButtonLabelModel].self, forKey: .radioButtons)
|
||||
try super.init(from: decoder)
|
||||
}
|
||||
|
||||
public override func encode(to encoder: Encoder) throws {
|
||||
try super.encode(to: encoder)
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(radioButtons, forKey: .radioButtons)
|
||||
}
|
||||
}
|
||||
@ -8,6 +8,7 @@
|
||||
|
||||
import MVMCore
|
||||
import UIKit
|
||||
import VDS
|
||||
|
||||
public typealias ActionBlockConfirmation = () -> (Bool)
|
||||
|
||||
@ -19,137 +20,40 @@ public typealias ActionBlockConfirmation = () -> (Bool)
|
||||
Container: The background of the toggle control.
|
||||
Knob: The circular indicator that slides on the container.
|
||||
*/
|
||||
@objcMembers open class Toggle: Control, MVMCoreUIViewConstrainingProtocol {
|
||||
//--------------------------------------------------
|
||||
@objcMembers open class Toggle: VDS.Toggle, VDSMoleculeViewProtocol, MVMCoreUIViewConstrainingProtocol {
|
||||
//------------------------------------------------------
|
||||
// MARK: - Properties
|
||||
//--------------------------------------------------
|
||||
//------------------------------------------------------
|
||||
open var viewModel: ToggleModel!
|
||||
open var delegateObject: MVMCoreUIDelegateObject?
|
||||
open var additionalData: [AnyHashable : Any]?
|
||||
|
||||
/// Holds the on and off colors for the container.
|
||||
public var containerTintColor: (on: UIColor, off: UIColor) = (on: .mvmGreen, off: .mvmBlack)
|
||||
|
||||
/// Holds the on and off colors for the knob.
|
||||
public var knobTintColor: (on: UIColor, off: UIColor) = (on: .mvmWhite, off: .mvmWhite)
|
||||
|
||||
/// Holds the on and off colors for the disabled state..
|
||||
public var disabledTintColor: (container: UIColor, knob: UIColor) = (container: .mvmCoolGray3, knob: .mvmWhite)
|
||||
|
||||
/// Set this flag to false if you do not want to animate state changes.
|
||||
public var isAnimated = true
|
||||
|
||||
public var didToggleAction: ActionBlock?
|
||||
public var didToggleAction: ActionBlock? {
|
||||
didSet {
|
||||
if let didToggleAction {
|
||||
onChange = { _ in
|
||||
didToggleAction()
|
||||
}
|
||||
} else {
|
||||
onChange = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Executes logic before state change. If false, then toggle state will not change and the didToggleAction will not execute.
|
||||
public var shouldToggleAction: ActionBlockConfirmation? = {
|
||||
return { true }
|
||||
}()
|
||||
|
||||
// Sizes are from InVision design specs.
|
||||
static let containerSize = CGSize(width: 51, height: 31)
|
||||
static let knobSize = CGSize(width: 28, height: 28)
|
||||
|
||||
private var knobView: View = {
|
||||
let view = View()
|
||||
view.backgroundColor = .white
|
||||
view.layer.cornerRadius = Toggle.getKnobHeight() / 2.0
|
||||
return view
|
||||
}()
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Computed Properties
|
||||
//--------------------------------------------------
|
||||
|
||||
open override var isEnabled: Bool {
|
||||
didSet {
|
||||
isUserInteractionEnabled = isEnabled
|
||||
changeStateNoAnimation(isEnabled ? isOn : false)
|
||||
setToggleAppearanceFromState()
|
||||
accessibilityHint = MVMCoreUIUtility.hardcodedString(withKey: isEnabled ? "AccToggleHint" : "AccDisabled")
|
||||
}
|
||||
}
|
||||
|
||||
/// Simple means to prevent user interaction with the toggle.
|
||||
public var isLocked: Bool = false {
|
||||
didSet { isUserInteractionEnabled = !isLocked }
|
||||
}
|
||||
|
||||
/// The state on the toggle. Default value: false.
|
||||
open var isOn: Bool = false {
|
||||
didSet {
|
||||
if isAnimated {
|
||||
UIView.animate(withDuration: 0.2, delay: 0.0, options: .curveEaseIn, animations: {
|
||||
if self.isOn {
|
||||
self.knobView.backgroundColor = self.knobTintColor.on
|
||||
self.backgroundColor = self.containerTintColor.on
|
||||
|
||||
} else {
|
||||
self.knobView.backgroundColor = self.knobTintColor.off
|
||||
self.backgroundColor = self.containerTintColor.off
|
||||
}
|
||||
}, completion: nil)
|
||||
|
||||
UIView.animate(withDuration: 0.33, delay: 0, usingSpringWithDamping: 0.6, initialSpringVelocity: 0.2, options: [], animations: {
|
||||
self.constrainKnob()
|
||||
self.knobWidthConstraint?.constant = Self.getKnobWidth()
|
||||
self.layoutIfNeeded()
|
||||
}, completion: nil)
|
||||
|
||||
} else {
|
||||
setToggleAppearanceFromState()
|
||||
self.constrainKnob()
|
||||
}
|
||||
|
||||
toggleModel?.selected = isOn
|
||||
_ = FormValidator.validate(delegate: delegateObject?.formHolderDelegate)
|
||||
accessibilityValue = isOn ? MVMCoreUIUtility.hardcodedString(withKey: "AccOn") : MVMCoreUIUtility.hardcodedString(withKey: "AccOff")
|
||||
setNeedsLayout()
|
||||
layoutIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
public var toggleModel: ToggleModel? {
|
||||
model as? ToggleModel
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Delegate
|
||||
//--------------------------------------------------
|
||||
|
||||
private var delegateObject: MVMCoreUIDelegateObject?
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Constraints
|
||||
//--------------------------------------------------
|
||||
|
||||
private var knobLeadingConstraint: NSLayoutConstraint?
|
||||
private var knobTrailingConstraint: NSLayoutConstraint?
|
||||
private var knobHeightConstraint: NSLayoutConstraint?
|
||||
private var knobWidthConstraint: NSLayoutConstraint?
|
||||
private var heightConstraint: NSLayoutConstraint?
|
||||
private var widthConstraint: NSLayoutConstraint?
|
||||
|
||||
private func constrainKnob() {
|
||||
|
||||
knobLeadingConstraint?.isActive = false
|
||||
knobTrailingConstraint?.isActive = false
|
||||
|
||||
_ = isOn ? constrainKnobOn() : constrainKnobOff()
|
||||
|
||||
knobTrailingConstraint?.isActive = true
|
||||
knobLeadingConstraint?.isActive = true
|
||||
}
|
||||
|
||||
private func constrainKnobOn() {
|
||||
|
||||
knobTrailingConstraint = trailingAnchor.constraint(equalTo: knobView.trailingAnchor, constant: 2)
|
||||
knobLeadingConstraint = knobView.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor)
|
||||
}
|
||||
|
||||
private func constrainKnobOff() {
|
||||
|
||||
knobTrailingConstraint = trailingAnchor.constraint(greaterThanOrEqualTo: knobView.trailingAnchor)
|
||||
knobLeadingConstraint = knobView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 2)
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Initializers
|
||||
//--------------------------------------------------
|
||||
@ -158,7 +62,7 @@ public typealias ActionBlockConfirmation = () -> (Bool)
|
||||
super.init(frame: frame)
|
||||
}
|
||||
|
||||
public convenience override init() {
|
||||
public convenience required init() {
|
||||
self.init(frame: .zero)
|
||||
}
|
||||
|
||||
@ -171,7 +75,7 @@ public typealias ActionBlockConfirmation = () -> (Bool)
|
||||
/// - parameter didToggleAction: A closure which is executed after the toggle changes states.
|
||||
public convenience init(isOn: Bool = false, didToggleAction: ActionBlock?) {
|
||||
self.init(frame: .zero)
|
||||
changeStateNoAnimation(isOn)
|
||||
self.isOn = isOn
|
||||
self.didToggleAction = didToggleAction
|
||||
}
|
||||
|
||||
@ -191,223 +95,78 @@ public typealias ActionBlockConfirmation = () -> (Bool)
|
||||
//--------------------------------------------------
|
||||
// MARK: - Lifecycle
|
||||
//--------------------------------------------------
|
||||
|
||||
public override func updateView(_ size: CGFloat) {
|
||||
super.updateView(size)
|
||||
|
||||
heightConstraint?.constant = Self.getContainerHeight()
|
||||
widthConstraint?.constant = Self.getContainerWidth()
|
||||
|
||||
knobHeightConstraint?.constant = Self.getKnobHeight()
|
||||
knobWidthConstraint?.constant = Self.getKnobWidth()
|
||||
|
||||
layer.cornerRadius = Self.getContainerHeight() / 2.0
|
||||
knobView.layer.cornerRadius = Self.getKnobHeight() / 2.0
|
||||
|
||||
changeStateNoAnimation(isOn)
|
||||
}
|
||||
|
||||
public override func setupView() {
|
||||
super.setupView()
|
||||
|
||||
isAccessibilityElement = true
|
||||
accessibilityHint = MVMCoreUIUtility.hardcodedString(withKey: "AccToggleHint")
|
||||
accessibilityLabel = MVMCoreUIUtility.hardcodedString(withKey: "Toggle_buttonlabel")
|
||||
accessibilityTraits = .button
|
||||
|
||||
heightConstraint = heightAnchor.constraint(equalToConstant: Self.containerSize.height)
|
||||
heightConstraint?.isActive = true
|
||||
|
||||
widthConstraint = widthAnchor.constraint(equalToConstant: Self.containerSize.width)
|
||||
widthConstraint?.isActive = true
|
||||
|
||||
layer.cornerRadius = Self.getContainerHeight() / 2.0
|
||||
backgroundColor = containerTintColor.off
|
||||
|
||||
addSubview(knobView)
|
||||
|
||||
knobHeightConstraint = knobView.heightAnchor.constraint(equalToConstant: Self.knobSize.height)
|
||||
knobHeightConstraint?.isActive = true
|
||||
knobWidthConstraint = knobView.widthAnchor.constraint(equalToConstant: Self.knobSize.width)
|
||||
knobWidthConstraint?.isActive = true
|
||||
knobView.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
|
||||
knobView.topAnchor.constraint(greaterThanOrEqualTo: topAnchor).isActive = true
|
||||
bottomAnchor.constraint(greaterThanOrEqualTo: knobView.bottomAnchor).isActive = true
|
||||
|
||||
constrainKnobOff()
|
||||
}
|
||||
|
||||
public override func reset() {
|
||||
super.reset()
|
||||
|
||||
backgroundColor = containerTintColor.off
|
||||
knobView.backgroundColor = knobTintColor.off
|
||||
accessibilityLabel = MVMCoreUIUtility.hardcodedString(withKey: "Toggle_buttonlabel")
|
||||
isAnimated = true
|
||||
didToggleAction = nil
|
||||
shouldToggleAction = { return true }
|
||||
}
|
||||
|
||||
class func getContainerWidth() -> CGFloat {
|
||||
let containerWidth = Self.containerSize.width
|
||||
return (MFSizeObject(standardSize: containerWidth, standardiPadPortraitSize: CGFloat(Self.containerSize.width * 1.5)))?.getValueBasedOnApplicationWidth() ?? containerWidth
|
||||
}
|
||||
|
||||
class func getContainerHeight() -> CGFloat {
|
||||
let containerHeight = Self.containerSize.height
|
||||
return (MFSizeObject(standardSize: containerHeight, standardiPadPortraitSize: CGFloat(Self.containerSize.height * 1.5)))?.getValueBasedOnApplicationWidth() ?? containerHeight
|
||||
}
|
||||
|
||||
class func getKnobWidth() -> CGFloat {
|
||||
let knobWidth = Self.knobSize.width
|
||||
return (MFSizeObject(standardSize: knobWidth, standardiPadPortraitSize: CGFloat(Self.knobSize.width * 1.5)))?.getValueBasedOnApplicationWidth() ?? knobWidth
|
||||
}
|
||||
|
||||
class func getKnobHeight() -> CGFloat {
|
||||
let knobHeight = Self.knobSize.width
|
||||
return (MFSizeObject(standardSize: knobHeight, standardiPadPortraitSize: CGFloat(Self.knobSize.height * 1.5)))?.getValueBasedOnApplicationWidth() ?? knobHeight
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Actions
|
||||
//--------------------------------------------------
|
||||
|
||||
open override func sendAction(_ action: Selector, to target: Any?, for event: UIEvent?) {
|
||||
super.sendAction(action, to: target, for: event)
|
||||
toggleAndAction()
|
||||
}
|
||||
|
||||
open override func sendActions(for controlEvents: UIControl.Event) {
|
||||
super.sendActions(for: controlEvents)
|
||||
toggleAndAction()
|
||||
}
|
||||
|
||||
/// This will toggle the state of the Toggle and execute the actionBlock if provided.
|
||||
public func toggleAndAction() {
|
||||
|
||||
if let result = shouldToggleAction?(), result {
|
||||
isOn.toggle()
|
||||
didToggleAction?()
|
||||
public func viewModelDidUpdate() {
|
||||
if let accessibilityIdentifier = viewModel.accessibilityIdentifier {
|
||||
self.accessibilityIdentifier = accessibilityIdentifier
|
||||
}
|
||||
}
|
||||
|
||||
private func changeStateNoAnimation(_ state: Bool) {
|
||||
FormValidator.setupValidation(for: viewModel, delegate: delegateObject?.formHolderDelegate)
|
||||
|
||||
// Hold state in case User wanted isAnimated to remain off.
|
||||
let isAnimatedState = isAnimated
|
||||
|
||||
isAnimated = false
|
||||
isOn = state
|
||||
isAnimated = isAnimatedState
|
||||
}
|
||||
|
||||
override open func accessibilityActivate() -> Bool {
|
||||
// Hold state in case User wanted isAnimated to remain off.
|
||||
guard isUserInteractionEnabled else { return false }
|
||||
let isAnimatedState = isAnimated
|
||||
isAnimated = false
|
||||
sendActions(for: .touchUpInside)
|
||||
isAnimated = isAnimatedState
|
||||
return true
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - UIResponder
|
||||
//--------------------------------------------------
|
||||
|
||||
open override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
|
||||
UIView.animate(withDuration: 0.1, animations: {
|
||||
self.knobWidthConstraint?.constant += PaddingOne
|
||||
self.layoutIfNeeded()
|
||||
})
|
||||
}
|
||||
|
||||
public override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
|
||||
knobReformAnimation()
|
||||
|
||||
// Action only occurs of the user lifts up from withing acceptable region of the toggle.
|
||||
guard let coordinates = touches.first?.location(in: self),
|
||||
coordinates.x > -20,
|
||||
coordinates.x < bounds.width + 20,
|
||||
coordinates.y > -20,
|
||||
coordinates.y < bounds.height + 20
|
||||
else { return }
|
||||
|
||||
sendActions(for: .touchUpInside)
|
||||
}
|
||||
|
||||
public func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
|
||||
knobReformAnimation()
|
||||
sendActions(for: .touchCancel)
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Animations
|
||||
//--------------------------------------------------
|
||||
|
||||
public func setToggleAppearanceFromState() {
|
||||
|
||||
backgroundColor = isEnabled ? isOn ? containerTintColor.on : containerTintColor.off : disabledTintColor.container
|
||||
knobView.backgroundColor = isEnabled ? isOn ? knobTintColor.on : knobTintColor.off : disabledTintColor.knob
|
||||
}
|
||||
|
||||
public func knobReformAnimation() {
|
||||
|
||||
if isAnimated {
|
||||
UIView.animate(withDuration: 0.1, animations: {
|
||||
self.knobWidthConstraint?.constant = Self.getKnobWidth()
|
||||
self.layoutIfNeeded()
|
||||
}, completion: nil)
|
||||
|
||||
} else {
|
||||
knobWidthConstraint?.constant = Self.getKnobWidth()
|
||||
layoutIfNeeded()
|
||||
isOn = viewModel.selected
|
||||
surface = viewModel.surface
|
||||
isAnimated = viewModel.animated
|
||||
isEnabled = viewModel.isEnabled
|
||||
showText = viewModel.showText
|
||||
if let onText = viewModel.onText {
|
||||
self.onText = onText
|
||||
}
|
||||
}
|
||||
|
||||
// MARK:- MoleculeViewProtocol
|
||||
public override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
|
||||
super.set(with: model, delegateObject, additionalData)
|
||||
self.delegateObject = delegateObject
|
||||
if let offText = viewModel.offText {
|
||||
self.offText = offText
|
||||
}
|
||||
textSize = viewModel.textSize
|
||||
textWeight = viewModel.textWeight
|
||||
textPosition = viewModel.textPosition
|
||||
|
||||
guard let model = model as? ToggleModel else { return }
|
||||
|
||||
FormValidator.setupValidation(for: model, delegate: delegateObject?.formHolderDelegate)
|
||||
|
||||
containerTintColor.on = model.onTintColor.uiColor
|
||||
containerTintColor.off = model.offTintColor.uiColor
|
||||
knobTintColor.on = model.onKnobTintColor.uiColor
|
||||
knobTintColor.off = model.offKnobTintColor.uiColor
|
||||
isOn = model.selected
|
||||
changeStateNoAnimation(isOn)
|
||||
isAnimated = model.animated
|
||||
isEnabled = model.enabled && !model.readOnly
|
||||
|
||||
if let accessibileString = model.accessibilityText {
|
||||
if let accessibileString = viewModel.accessibilityText {
|
||||
accessibilityLabel = accessibileString
|
||||
}
|
||||
|
||||
if model.action != nil || model.alternateAction != nil {
|
||||
if viewModel.action != nil || viewModel.alternateAction != nil {
|
||||
didToggleAction = { [weak self] in
|
||||
guard let self = self else { return }
|
||||
if self.isOn {
|
||||
if let action = model.action {
|
||||
if let action = viewModel.action {
|
||||
MVMCoreUIActionHandler.performActionUnstructured(with: action, sourceModel: model, additionalData: additionalData, delegateObject: delegateObject)
|
||||
}
|
||||
} else {
|
||||
if let action = model.alternateAction ?? model.action {
|
||||
if let action = viewModel.alternateAction ?? viewModel.action {
|
||||
MVMCoreUIActionHandler.performActionUnstructured(with: action, sourceModel: model, additionalData: additionalData, delegateObject: delegateObject)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Actions
|
||||
//--------------------------------------------------
|
||||
/// This will toggle the state of the Toggle and execute the actionBlock if provided.
|
||||
public func toggleAndAction() {
|
||||
toggle()
|
||||
}
|
||||
|
||||
public override class func estimatedHeight(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? {
|
||||
Self.getContainerHeight()
|
||||
open override func toggle() {
|
||||
if let result = shouldToggleAction?(), result {
|
||||
super.toggle()
|
||||
viewModel?.selected = isOn
|
||||
_ = FormValidator.validate(delegate: delegateObject?.formHolderDelegate)
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK:- MoleculeViewProtocol
|
||||
//--------------------------------------------------
|
||||
|
||||
public func updateView(_ size: CGFloat) {}
|
||||
|
||||
public class func estimatedHeight(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? {
|
||||
return Self().intrinsicContentSize.height
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -5,80 +5,62 @@
|
||||
// Created by Scott Pfeil on 1/14/20.
|
||||
// Copyright © 2020 Verizon Wireless. All rights reserved.
|
||||
//
|
||||
import VDS
|
||||
|
||||
|
||||
public class ToggleModel: MoleculeModelProtocol, FormFieldProtocol {
|
||||
public class ToggleModel: FormFieldModel {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Properties
|
||||
//--------------------------------------------------
|
||||
|
||||
public static var identifier: String = "toggle"
|
||||
public var id: String = UUID().uuidString
|
||||
|
||||
public var accessibilityIdentifier: String?
|
||||
public var backgroundColor: Color?
|
||||
public override static var identifier: String { "toggle" }
|
||||
public var selected: Bool = false
|
||||
public var animated: Bool = true
|
||||
public var enabled: Bool = true
|
||||
public var readOnly: Bool = false
|
||||
public var action: ActionModelProtocol?
|
||||
public var alternateAction: ActionModelProtocol?
|
||||
public var accessibilityText: String?
|
||||
public var onTintColor: Color = Color(uiColor: .mvmGreen)
|
||||
public var offTintColor: Color = Color(uiColor: .mvmBlack)
|
||||
public var onKnobTintColor: Color = Color(uiColor: .mvmWhite)
|
||||
public var offKnobTintColor: Color = Color(uiColor: .mvmWhite)
|
||||
|
||||
public var fieldKey: String?
|
||||
public var groupName: String = FormValidator.defaultGroupName
|
||||
public var baseValue: AnyHashable?
|
||||
|
||||
public var showText: Bool = false
|
||||
public var onText: String?
|
||||
public var offText: String?
|
||||
public var textSize: VDS.Toggle.TextSize = .small
|
||||
public var textWeight: VDS.Toggle.TextWeight = .regular
|
||||
public var textPosition: VDS.Toggle.TextPosition = .left
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Keys
|
||||
//--------------------------------------------------
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case moleculeName
|
||||
case state
|
||||
case animated
|
||||
case enabled
|
||||
case readOnly
|
||||
case action
|
||||
case backgroundColor
|
||||
case accessibilityIdentifier
|
||||
case alternateAction
|
||||
case accessibilityText
|
||||
case onTintColor
|
||||
case offTintColor
|
||||
case onKnobTintColor
|
||||
case offKnobTintColor
|
||||
case fieldKey
|
||||
case groupName
|
||||
|
||||
case showText
|
||||
case onText
|
||||
case offText
|
||||
case textSize
|
||||
case textWeight
|
||||
case textPosition
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Form Valdiation
|
||||
//--------------------------------------------------
|
||||
|
||||
public func formFieldValue() -> AnyHashable? {
|
||||
public override func formFieldValue() -> AnyHashable? {
|
||||
guard enabled else { return nil }
|
||||
return selected
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Server Value
|
||||
//--------------------------------------------------
|
||||
open func formFieldServerValue() -> AnyHashable? {
|
||||
return formFieldValue()
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Initializer
|
||||
//--------------------------------------------------
|
||||
|
||||
public init(_ state: Bool, id: String = UUID().uuidString) {
|
||||
self.selected = state
|
||||
selected = state
|
||||
super.init()
|
||||
baseValue = state
|
||||
self.id = id
|
||||
}
|
||||
@ -90,8 +72,6 @@ public class ToggleModel: MoleculeModelProtocol, FormFieldProtocol {
|
||||
required public init(from decoder: Decoder) throws {
|
||||
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString
|
||||
|
||||
if let state = try typeContainer.decodeIfPresent(Bool.self, forKey: .state) {
|
||||
self.selected = state
|
||||
}
|
||||
@ -102,54 +82,48 @@ public class ToggleModel: MoleculeModelProtocol, FormFieldProtocol {
|
||||
|
||||
action = try typeContainer.decodeModelIfPresent(codingKey: .action)
|
||||
alternateAction = try typeContainer.decodeModelIfPresent(codingKey: .alternateAction)
|
||||
backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor)
|
||||
accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier)
|
||||
|
||||
if let onTintColor = try typeContainer.decodeIfPresent(Color.self, forKey: .onTintColor) {
|
||||
self.onTintColor = onTintColor
|
||||
}
|
||||
|
||||
if let offTintColor = try typeContainer.decodeIfPresent(Color.self, forKey: .offTintColor) {
|
||||
self.offTintColor = offTintColor
|
||||
}
|
||||
|
||||
if let onKnobTintColor = try typeContainer.decodeIfPresent(Color.self, forKey: .onKnobTintColor) {
|
||||
self.onKnobTintColor = onKnobTintColor
|
||||
}
|
||||
|
||||
if let offKnobTintColor = try typeContainer.decodeIfPresent(Color.self, forKey: .offKnobTintColor) {
|
||||
self.offKnobTintColor = offKnobTintColor
|
||||
}
|
||||
|
||||
accessibilityText = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityText)
|
||||
|
||||
showText = try typeContainer.decodeIfPresent(Bool.self, forKey: .showText) ?? false
|
||||
onText = try typeContainer.decodeIfPresent(String.self, forKey: .onText)
|
||||
offText = try typeContainer.decodeIfPresent(String.self, forKey: .offText)
|
||||
textSize = try typeContainer.decodeIfPresent(VDS.Toggle.TextSize.self, forKey: .textSize) ?? .small
|
||||
textWeight = try typeContainer.decodeIfPresent(VDS.Toggle.TextWeight.self, forKey: .textWeight) ?? .regular
|
||||
textPosition = try typeContainer.decodeIfPresent(VDS.Toggle.TextPosition.self, forKey: .textPosition) ?? .left
|
||||
|
||||
try super.init(from: decoder)
|
||||
baseValue = selected
|
||||
fieldKey = try typeContainer.decodeIfPresent(String.self, forKey: .fieldKey)
|
||||
if let groupName = try typeContainer.decodeIfPresent(String.self, forKey: .groupName) {
|
||||
self.groupName = groupName
|
||||
}
|
||||
enabled = try typeContainer.decodeIfPresent(Bool.self, forKey: .enabled) ?? true
|
||||
readOnly = try typeContainer.decodeIfPresent(Bool.self, forKey: .readOnly) ?? false
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
public override func encode(to encoder: Encoder) throws {
|
||||
try super.encode(to: encoder)
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(id, forKey: .id)
|
||||
try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor)
|
||||
try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier)
|
||||
try container.encodeModelIfPresent(action, forKey: .action)
|
||||
try container.encodeModelIfPresent(alternateAction, forKey: .alternateAction)
|
||||
try container.encode(moleculeName, forKey: .moleculeName)
|
||||
try container.encode(selected, forKey: .state)
|
||||
try container.encode(animated, forKey: .animated)
|
||||
try container.encode(enabled, forKey: .enabled)
|
||||
try container.encode(onTintColor, forKey: .onTintColor)
|
||||
try container.encode(onKnobTintColor, forKey: .onKnobTintColor)
|
||||
try container.encode(onKnobTintColor, forKey: .onKnobTintColor)
|
||||
try container.encode(offKnobTintColor, forKey: .offKnobTintColor)
|
||||
try container.encodeIfPresent(accessibilityText, forKey: .accessibilityText)
|
||||
try container.encodeIfPresent(fieldKey, forKey: .fieldKey)
|
||||
try container.encodeIfPresent(groupName, forKey: .groupName)
|
||||
try container.encode(readOnly, forKey: .readOnly)
|
||||
try container.encode(showText, forKey: .showText)
|
||||
try container.encodeIfPresent(onText, forKey: .onText)
|
||||
try container.encodeIfPresent(offText, forKey: .offText)
|
||||
try container.encode(textSize, forKey: .textSize)
|
||||
try container.encode(textWeight, forKey: .textWeight)
|
||||
try container.encode(textPosition, forKey: .textPosition)
|
||||
}
|
||||
|
||||
open override func isEqual(to model: any ModelComparisonProtocol) -> Bool {
|
||||
guard super.isEqual(to: model), let model = model as? Self else { return false }
|
||||
return selected == model.selected
|
||||
&& animated == model.animated
|
||||
&& action.isEqual(to: model.action)
|
||||
&& alternateAction.isEqual(to: model.alternateAction)
|
||||
&& accessibilityText == model.accessibilityText
|
||||
&& showText == model.showText
|
||||
&& onText == model.onText
|
||||
&& offText == model.offText
|
||||
&& textSize == model.textSize
|
||||
&& textWeight == model.textWeight
|
||||
&& textPosition == model.textPosition
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,7 +19,8 @@ open class ArrowModel: MoleculeModelProtocol, EnableableModelProtocol {
|
||||
}
|
||||
public var moleculeName: String?
|
||||
public var id: String = UUID().uuidString
|
||||
|
||||
public var accessibilityIdentifier: String?
|
||||
|
||||
public var backgroundColor: Color?
|
||||
public var disabledColor: Color = Color(uiColor: .mvmCoolGray3)
|
||||
public var color: Color = Color(uiColor: .mvmBlack)
|
||||
@ -59,6 +60,7 @@ open class ArrowModel: MoleculeModelProtocol, EnableableModelProtocol {
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case accessibilityIdentifier
|
||||
case moleculeName
|
||||
case backgroundColor
|
||||
case disabledColor
|
||||
@ -79,7 +81,8 @@ open class ArrowModel: MoleculeModelProtocol, EnableableModelProtocol {
|
||||
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
|
||||
id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString
|
||||
backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor)
|
||||
|
||||
accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier)
|
||||
|
||||
if let disabledColor = try typeContainer.decodeIfPresent(Color.self, forKey: .disabledColor) {
|
||||
self.disabledColor = disabledColor
|
||||
}
|
||||
@ -116,6 +119,7 @@ open class ArrowModel: MoleculeModelProtocol, EnableableModelProtocol {
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(id, forKey: .id)
|
||||
try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier)
|
||||
try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor)
|
||||
try container.encode(moleculeName, forKey: .moleculeName)
|
||||
try container.encode(disabledColor, forKey: .disabledColor)
|
||||
|
||||
@ -27,10 +27,14 @@ open class Badge: VDS.Badge, VDSMoleculeViewProtocol {
|
||||
//--------------------------------------------------
|
||||
|
||||
public func viewModelDidUpdate() {
|
||||
if let accessibilityIdentifier = viewModel.accessibilityIdentifier {
|
||||
self.accessibilityIdentifier = accessibilityIdentifier
|
||||
}
|
||||
text = viewModel.text
|
||||
textColor = viewModel.textColorStyle
|
||||
maxWidth = viewModel.maxWidth
|
||||
numberOfLines = viewModel.numberOfLines
|
||||
fillColor = viewModel.fillColor
|
||||
fillColor = viewModel.fillColorStyle
|
||||
surface = viewModel.surface
|
||||
}
|
||||
|
||||
|
||||
@ -25,6 +25,9 @@ open class BadgeIndicator: VDS.BadgeIndicator, VDSMoleculeViewProtocol {
|
||||
//--------------------------------------------------
|
||||
|
||||
public func viewModelDidUpdate() {
|
||||
if let accessibilityIdentifier = viewModel.accessibilityIdentifier {
|
||||
self.accessibilityIdentifier = accessibilityIdentifier
|
||||
}
|
||||
surface = viewModel.surface
|
||||
number = viewModel.number
|
||||
fillColor = viewModel.fillColor
|
||||
|
||||
@ -17,7 +17,8 @@ open class BadgeIndicatorModel: MoleculeModelProtocol {
|
||||
public static var identifier: String { "badgeIndicator" }
|
||||
public var id: String = UUID().uuidString
|
||||
public var backgroundColor: Color?
|
||||
|
||||
public var accessibilityIdentifier: String?
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - VDS Properties
|
||||
//--------------------------------------------------
|
||||
@ -43,6 +44,7 @@ open class BadgeIndicatorModel: MoleculeModelProtocol {
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case accessibilityIdentifier
|
||||
case inverted
|
||||
case accessibilityText
|
||||
case number
|
||||
@ -67,6 +69,7 @@ open class BadgeIndicatorModel: MoleculeModelProtocol {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
self.init()
|
||||
id = try container.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString
|
||||
accessibilityIdentifier = try container.decodeIfPresent(String.self, forKey: .accessibilityIdentifier)
|
||||
inverted = try container.decodeIfPresent(Bool.self, forKey: .inverted) ?? false
|
||||
accessibilityText = try container.decodeIfPresent(String.self, forKey: .accessibilityText)
|
||||
number = try container.decodeIfPresent(Int.self, forKey: .number)
|
||||
@ -91,6 +94,7 @@ open class BadgeIndicatorModel: MoleculeModelProtocol {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(id, forKey: .id)
|
||||
try container.encode(inverted, forKey: .inverted)
|
||||
try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier)
|
||||
try container.encodeIfPresent(accessibilityText, forKey: .accessibilityText)
|
||||
try container.encodeIfPresent(number, forKey: .number)
|
||||
try container.encodeIfPresent(fillColor, forKey: .fillColor)
|
||||
|
||||
@ -16,28 +16,46 @@ open class BadgeModel: MoleculeModelProtocol {
|
||||
public static var identifier: String = "badge"
|
||||
public var id: String = UUID().uuidString
|
||||
public var backgroundColor: Color?
|
||||
|
||||
public var accessibilityIdentifier: String?
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - VDS Properties
|
||||
//--------------------------------------------------
|
||||
public var text: String = ""
|
||||
public var textColorStyle: Badge.TextColor? = nil
|
||||
public var accessibilityText: String?
|
||||
public var maxWidth: CGFloat?
|
||||
public var numberOfLines: Int = 1
|
||||
public var fillColor = Badge.FillColor.red
|
||||
public var fillColorStyle = Badge.FillColor.red
|
||||
public var surface: Surface = .light
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id, text, accessibilityText, fillColor, surface, numberOfLines, maxWidth
|
||||
case id, accessibilityIdentifier, accessibilityText
|
||||
case surface, numberOfLines, maxWidth
|
||||
case text, textColor
|
||||
case fillColor, fillColorStyle
|
||||
}
|
||||
|
||||
required public convenience init(from decoder: Decoder) throws {
|
||||
self.init()
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
id = try container.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString
|
||||
accessibilityIdentifier = try container.decodeIfPresent(String.self, forKey: .accessibilityIdentifier)
|
||||
text = try container.decode(String.self, forKey: .text)
|
||||
accessibilityText = try container.decodeIfPresent(String.self, forKey: .accessibilityText)
|
||||
fillColor = try container.decodeIfPresent(Badge.FillColor.self, forKey: .fillColor) ?? .red
|
||||
|
||||
//look for a textColor
|
||||
if let textColor = try container.decodeIfPresent(Color.self, forKey: .textColor) {
|
||||
textColorStyle = .custom(textColor.uiColor)
|
||||
}
|
||||
|
||||
//look for a style
|
||||
fillColorStyle = try container.decodeIfPresent(Badge.FillColor.self, forKey: .fillColorStyle) ?? .red
|
||||
|
||||
//look for a color and set the style
|
||||
if let fillColor = try container.decodeIfPresent(Color.self, forKey: .fillColor) {
|
||||
fillColorStyle = .custom(fillColor.uiColor)
|
||||
}
|
||||
surface = try container.decodeIfPresent(Surface.self, forKey: .surface) ?? .light
|
||||
numberOfLines = try container.decodeIfPresent(Int.self, forKey: .numberOfLines) ?? 1
|
||||
maxWidth = try container.decodeIfPresent(CGFloat.self, forKey: .maxWidth)
|
||||
@ -48,9 +66,28 @@ open class BadgeModel: MoleculeModelProtocol {
|
||||
try container.encode(id, forKey: .id)
|
||||
try container.encode(text, forKey: .text)
|
||||
try container.encode(accessibilityText, forKey: .accessibilityText)
|
||||
try container.encode(fillColor, forKey: .fillColor)
|
||||
try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier)
|
||||
try container.encode(surface, forKey: .surface)
|
||||
try container.encode(numberOfLines, forKey: .numberOfLines)
|
||||
try container.encodeIfPresent(maxWidth, forKey: .maxWidth)
|
||||
try container.encode(fillColorStyle, forKey: .fillColorStyle)
|
||||
switch textColorStyle {
|
||||
case .custom(let color):
|
||||
try container.encode(Color(uiColor: color), forKey: .textColor)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
public func isEqual(to model: any ModelComparisonProtocol) -> Bool {
|
||||
guard let model = model as? BadgeModel else { return false }
|
||||
return self.backgroundColor == model.backgroundColor
|
||||
&& self.fillColorStyle == model.fillColorStyle
|
||||
&& self.textColorStyle == model.textColorStyle
|
||||
&& self.numberOfLines == model.numberOfLines
|
||||
&& self.text == model.text
|
||||
&& self.surface == model.surface
|
||||
&& self.accessibilityText == model.accessibilityText
|
||||
&& self.maxWidth == model.maxWidth
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,6 +25,9 @@ open class ButtonIcon: VDS.ButtonIcon, VDSMoleculeViewProtocol {
|
||||
//--------------------------------------------------
|
||||
|
||||
public func viewModelDidUpdate() {
|
||||
if let accessibilityIdentifier = viewModel.accessibilityIdentifier {
|
||||
self.accessibilityIdentifier = accessibilityIdentifier
|
||||
}
|
||||
surface = viewModel.surface
|
||||
|
||||
onClick = { [weak self] control in
|
||||
|
||||
@ -16,7 +16,8 @@ open class ButtonIconModel: ButtonModelProtocol, MoleculeModelProtocol {
|
||||
public static var identifier: String = "buttonIcon"
|
||||
public var id: String = UUID().uuidString
|
||||
public var backgroundColor: Color?
|
||||
|
||||
public var accessibilityIdentifier: String?
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - VDS Properties
|
||||
//--------------------------------------------------
|
||||
@ -77,6 +78,7 @@ open class ButtonIconModel: ButtonModelProtocol, MoleculeModelProtocol {
|
||||
//--------------------------------------------------
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case accessibilityIdentifier
|
||||
case inverted
|
||||
case accessibilityText
|
||||
case action
|
||||
@ -105,6 +107,7 @@ open class ButtonIconModel: ButtonModelProtocol, MoleculeModelProtocol {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
action = try container.decodeModel(codingKey: .action)
|
||||
id = try container.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString
|
||||
accessibilityIdentifier = try container.decodeIfPresent(String.self, forKey: .accessibilityIdentifier)
|
||||
inverted = try container.decodeIfPresent(Bool.self, forKey: .inverted) ?? false
|
||||
accessibilityText = try container.decodeIfPresent(String.self, forKey: .accessibilityText)
|
||||
badgeIndicator = try container.decodeIfPresent(BadgeIndicatorModel.self, forKey: .badgeIndicator)
|
||||
@ -128,6 +131,7 @@ open class ButtonIconModel: ButtonModelProtocol, MoleculeModelProtocol {
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(id, forKey: .id)
|
||||
try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier)
|
||||
try container.encode(inverted, forKey: .inverted)
|
||||
try container.encodeIfPresent(accessibilityText, forKey: .accessibilityText)
|
||||
try container.encodeIfPresent(badgeIndicator, forKey: .badgeIndicator)
|
||||
|
||||
12
MVMCoreUI/Atomic/Atoms/Views/Calendar.swift
Normal file
12
MVMCoreUI/Atomic/Atoms/Views/Calendar.swift
Normal file
@ -0,0 +1,12 @@
|
||||
//
|
||||
// Calendar.swift
|
||||
// MVMCoreUI
|
||||
//
|
||||
// Created by Matt Bruce on 8/20/24.
|
||||
// Copyright © 2024 Verizon Wireless. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import VDS
|
||||
|
||||
open class CalendarView: VDS.CalendarBase, VDSMoleculeViewProtocol
|
||||
65
MVMCoreUI/Atomic/Atoms/Views/CalendarView.swift
Normal file
65
MVMCoreUI/Atomic/Atoms/Views/CalendarView.swift
Normal file
@ -0,0 +1,65 @@
|
||||
//
|
||||
// Calendar.swift
|
||||
// MVMCoreUI
|
||||
//
|
||||
// Created by Matt Bruce on 8/20/24.
|
||||
// Copyright © 2024 Verizon Wireless. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import VDS
|
||||
|
||||
open class CalendarView: VDS.CalendarBase, VDSMoleculeViewProtocol {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Public Properties
|
||||
//--------------------------------------------------
|
||||
public var viewModel: CalendarViewModel!
|
||||
public var delegateObject: MVMCoreUIDelegateObject?
|
||||
public var additionalData: [AnyHashable : Any]?
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Public Methods
|
||||
//--------------------------------------------------
|
||||
|
||||
public func viewModelDidUpdate() {
|
||||
if let accessibilityIdentifier = viewModel.accessibilityIdentifier {
|
||||
self.accessibilityIdentifier = accessibilityIdentifier
|
||||
}
|
||||
if let _selectedDate = viewModel.selectedDate {
|
||||
selectedDate = _selectedDate
|
||||
}
|
||||
|
||||
if let _activeDates = viewModel.activeDates {
|
||||
activeDates = _activeDates
|
||||
}
|
||||
|
||||
if let _hideContainerBorder = viewModel.hideContainerBorder {
|
||||
hideContainerBorder = _hideContainerBorder
|
||||
}
|
||||
|
||||
if let _hideCurrentDateIndicator = viewModel.hideCurrentDateIndicator {
|
||||
hideCurrentDateIndicator = _hideCurrentDateIndicator
|
||||
}
|
||||
|
||||
if let _inactiveDates = viewModel.inactiveDates {
|
||||
inactiveDates = _inactiveDates
|
||||
}
|
||||
|
||||
if let _indicators = viewModel.indicators {
|
||||
indicators = _indicators
|
||||
}
|
||||
|
||||
if let _maxDate = viewModel.maxDate {
|
||||
maxDate = _maxDate
|
||||
}
|
||||
|
||||
if let _minDate = viewModel.minDate {
|
||||
minDate = _minDate
|
||||
}
|
||||
|
||||
surface = viewModel.surface
|
||||
}
|
||||
|
||||
public func updateView(_ size: CGFloat) {}
|
||||
|
||||
}
|
||||
159
MVMCoreUI/Atomic/Atoms/Views/CalendarViewModel.swift
Normal file
159
MVMCoreUI/Atomic/Atoms/Views/CalendarViewModel.swift
Normal file
@ -0,0 +1,159 @@
|
||||
//
|
||||
// CalendarModel.swift
|
||||
// MVMCoreUI
|
||||
//
|
||||
// Created by Matt Bruce on 8/20/24.
|
||||
// Copyright © 2024 Verizon Wireless. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import VDS
|
||||
|
||||
open class CalendarViewModel: MoleculeModelProtocol {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Properties
|
||||
//--------------------------------------------------
|
||||
public static var identifier: String = "calendar"
|
||||
public var id: String = UUID().uuidString
|
||||
public var backgroundColor: Color?
|
||||
public var accessibilityIdentifier: String?
|
||||
|
||||
public var dateFormatter: DateFormatter = {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateStyle = .medium
|
||||
formatter.timeZone = NSTimeZone.system
|
||||
formatter.locale = .current
|
||||
formatter.formatterBehavior = .default
|
||||
return formatter
|
||||
}()
|
||||
|
||||
/// Update the property value to alter the format of how the date is presented.
|
||||
public var dateFormat: String = "MMM d, y" {
|
||||
didSet { dateFormatter.dateFormat = dateFormat }
|
||||
}
|
||||
|
||||
public var hideContainerBorder: Bool?
|
||||
public var hideCurrentDateIndicator: Bool?
|
||||
public var activeDates: [Date]?
|
||||
public var inactiveDates: [Date]?
|
||||
public var selectedDate: Date?
|
||||
public var minDate: Date?
|
||||
public var maxDate: Date?
|
||||
public var indicators: [CalendarBase.CalendarIndicatorModel]?
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - VDS Properties
|
||||
//--------------------------------------------------
|
||||
public var surface: Surface { inverted ? .dark : .light }
|
||||
public var inverted: Bool = false
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Keys
|
||||
//--------------------------------------------------
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case accessibilityIdentifier
|
||||
case moleculeName
|
||||
case inverted
|
||||
case dateFormat
|
||||
case hideContainerBorder
|
||||
case hideCurrentDateIndicator
|
||||
case activeDates
|
||||
case inactiveDates
|
||||
case selectedDate
|
||||
case minDate
|
||||
case maxDate
|
||||
case indicators
|
||||
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Initializers
|
||||
//--------------------------------------------------
|
||||
public init() {}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Codec
|
||||
//--------------------------------------------------
|
||||
required public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
id = try container.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString
|
||||
inverted = try container.decodeIfPresent(Bool.self, forKey: .inverted) ?? false
|
||||
accessibilityIdentifier = try container.decodeIfPresent(String.self, forKey: .accessibilityIdentifier)
|
||||
|
||||
hideContainerBorder = try container.decodeIfPresent(Bool.self, forKey: .hideContainerBorder)
|
||||
hideCurrentDateIndicator = try container.decodeIfPresent(Bool.self, forKey: .hideCurrentDateIndicator)
|
||||
|
||||
if let dateFormat = try container.decodeIfPresent(String.self, forKey: .dateFormat) {
|
||||
self.dateFormat = dateFormat
|
||||
dateFormatter.dateFormat = dateFormat
|
||||
}
|
||||
|
||||
if let dates = try container.decodeIfPresent([String].self, forKey: .activeDates) {
|
||||
activeDates = dates.compactMap { dateFormatter.date(from: $0) }
|
||||
}
|
||||
|
||||
if let dates = try container.decodeIfPresent([String].self, forKey: .inactiveDates) {
|
||||
inactiveDates = dates.compactMap { dateFormatter.date(from: $0) }
|
||||
}
|
||||
|
||||
if let date = try container.decodeIfPresent(String.self, forKey: .selectedDate) {
|
||||
selectedDate = dateFormatter.date(from: date)
|
||||
}
|
||||
|
||||
if let date = try container.decodeIfPresent(String.self, forKey: .minDate) {
|
||||
minDate = dateFormatter.date(from: date)
|
||||
}
|
||||
|
||||
if let date = try container.decodeIfPresent(String.self, forKey: .maxDate) {
|
||||
maxDate = dateFormatter.date(from: date)
|
||||
}
|
||||
|
||||
indicators = try container.decodeIfPresent([CalendarBase.CalendarIndicatorModel].self, forKey: .indicators)
|
||||
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(moleculeName, forKey: .moleculeName)
|
||||
try container.encode(id, forKey: .id)
|
||||
try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier)
|
||||
try container.encode(inverted, forKey: .inverted)
|
||||
try container.encode(dateFormat, forKey: .dateFormat)
|
||||
try container.encode(hideContainerBorder, forKey: .hideContainerBorder)
|
||||
try container.encode(hideCurrentDateIndicator, forKey: .hideCurrentDateIndicator)
|
||||
try container.encode(activeDates, forKey: .activeDates)
|
||||
try container.encode(selectedDate, forKey: .selectedDate)
|
||||
try container.encode(minDate, forKey: .minDate)
|
||||
try container.encode(maxDate, forKey: .maxDate)
|
||||
try container.encode(indicators, forKey: .indicators)
|
||||
}
|
||||
|
||||
open func isEqual(to model: any ModelComparisonProtocol) -> Bool {
|
||||
guard let model = model as? Self else { return false }
|
||||
return inverted == model.inverted
|
||||
&& dateFormat == model.dateFormat
|
||||
&& hideContainerBorder == model.hideContainerBorder
|
||||
&& hideCurrentDateIndicator == model.hideCurrentDateIndicator
|
||||
&& activeDates == model.activeDates
|
||||
&& inactiveDates == model.inactiveDates
|
||||
&& selectedDate == model.selectedDate
|
||||
&& minDate == model.minDate
|
||||
&& maxDate == model.maxDate
|
||||
&& indicators == model.indicators
|
||||
}
|
||||
}
|
||||
|
||||
extension CalendarViewModel {
|
||||
public func convertToVDSCalendarModel() -> DatePicker.CalendarModel {
|
||||
let defaults = DatePicker.CalendarModel()
|
||||
return .init(hideContainerBorder: hideContainerBorder ?? defaults.hideContainerBorder ,
|
||||
hideCurrentDateIndicator: hideCurrentDateIndicator ?? defaults.hideCurrentDateIndicator,
|
||||
activeDates: activeDates ?? defaults.activeDates,
|
||||
inactiveDates: inactiveDates ?? defaults.inactiveDates,
|
||||
selectedDate: selectedDate ?? defaults.selectedDate,
|
||||
minDate: minDate ?? defaults.minDate,
|
||||
maxDate: maxDate ?? defaults.maxDate,
|
||||
indicators: indicators ?? defaults.indicators)
|
||||
}
|
||||
}
|
||||
@ -16,6 +16,7 @@ import MVMCore
|
||||
|
||||
public static var identifier: String = "caretView"
|
||||
public var id: String = UUID().uuidString
|
||||
public var accessibilityIdentifier: String?
|
||||
public var backgroundColor: Color?
|
||||
public var strokeColor: Color = Color(uiColor: .mvmBlack)
|
||||
public var strokeColor_inverted: Color = Color(uiColor: .mvmWhite)
|
||||
@ -30,6 +31,7 @@ import MVMCore
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case accessibilityIdentifier
|
||||
case moleculeName
|
||||
case backgroundColor
|
||||
case strokeColor
|
||||
@ -54,7 +56,8 @@ import MVMCore
|
||||
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString
|
||||
|
||||
accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier)
|
||||
|
||||
if let strokeColor = try typeContainer.decodeIfPresent(Color.self, forKey: .strokeColor) {
|
||||
self.strokeColor = strokeColor
|
||||
}
|
||||
@ -84,6 +87,7 @@ import MVMCore
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(id, forKey: .id)
|
||||
try container.encode(moleculeName, forKey: .moleculeName)
|
||||
try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier)
|
||||
try container.encode(strokeColor, forKey: .strokeColor)
|
||||
try container.encode(strokeColor_inverted, forKey: .strokeColor_inverted)
|
||||
try container.encode(inverted, forKey: .inverted)
|
||||
|
||||
@ -19,6 +19,7 @@ open class CarouselIndicatorModel: CarouselPagingModelProtocol, MoleculeModelPro
|
||||
}
|
||||
|
||||
public var id: String = UUID().uuidString
|
||||
public var accessibilityIdentifier: String?
|
||||
public var backgroundColor: Color?
|
||||
public var moleculeName: String?
|
||||
|
||||
@ -48,6 +49,7 @@ open class CarouselIndicatorModel: CarouselPagingModelProtocol, MoleculeModelPro
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case accessibilityIdentifier
|
||||
case moleculeName
|
||||
case backgroundColor
|
||||
case currentIndex
|
||||
@ -70,6 +72,7 @@ open class CarouselIndicatorModel: CarouselPagingModelProtocol, MoleculeModelPro
|
||||
required public init(from decoder: Decoder) throws {
|
||||
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
|
||||
id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString
|
||||
accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier)
|
||||
moleculeName = try typeContainer.decodeIfPresent(String.self, forKey: .moleculeName)
|
||||
backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor)
|
||||
|
||||
@ -118,6 +121,7 @@ open class CarouselIndicatorModel: CarouselPagingModelProtocol, MoleculeModelPro
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(id, forKey: .id)
|
||||
try container.encodeIfPresent(moleculeName, forKey: .moleculeName)
|
||||
try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier)
|
||||
try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor)
|
||||
try container.encode(currentIndex, forKey: .currentIndex)
|
||||
try container.encode(alwaysSendAction, forKey: .alwaysSendAction)
|
||||
|
||||
@ -5,141 +5,99 @@
|
||||
// Created by Kevin Christiano on 9/13/19.
|
||||
// Copyright © 2019 Verizon Wireless. All rights reserved.
|
||||
//
|
||||
import VDS
|
||||
|
||||
|
||||
@objcMembers open class CheckboxLabel: View {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Outlets
|
||||
//--------------------------------------------------
|
||||
|
||||
public let checkbox = Checkbox()
|
||||
public let label = Label(fontStyle: .RegularBodySmall)
|
||||
private var observation: NSKeyValueObservation? = nil
|
||||
|
||||
//--------------------------------------------------
|
||||
@objcMembers open class CheckboxLabel: VDS.CheckboxItem, VDSMoleculeViewProtocol {
|
||||
//------------------------------------------------------
|
||||
// MARK: - Properties
|
||||
//--------------------------------------------------
|
||||
|
||||
public var checkboxPosition: CheckboxPosition = .center
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Constraints
|
||||
//--------------------------------------------------
|
||||
//------------------------------------------------------
|
||||
open var viewModel: CheckboxLabelModel!
|
||||
open var delegateObject: MVMCoreUIDelegateObject?
|
||||
open var additionalData: [AnyHashable : Any]?
|
||||
|
||||
public var checkboxTopConstraint: NSLayoutConstraint?
|
||||
public var checkboxBottomConstraint: NSLayoutConstraint?
|
||||
public var checkboxCenterYConstraint: NSLayoutConstraint?
|
||||
// Form Validation
|
||||
var fieldKey: String?
|
||||
var fieldValue: JSONValue?
|
||||
var groupName: String?
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Life Cycle
|
||||
//--------------------------------------------------
|
||||
|
||||
override open func setupView() {
|
||||
super.setupView()
|
||||
|
||||
guard subviews.isEmpty else { return }
|
||||
|
||||
addSubview(checkbox)
|
||||
addSubview(label)
|
||||
|
||||
label.text = ""
|
||||
|
||||
checkbox.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor).isActive = true
|
||||
|
||||
checkboxBottomConstraint = layoutMarginsGuide.bottomAnchor.constraint(equalTo: checkbox.bottomAnchor)
|
||||
checkboxBottomConstraint?.isActive = true
|
||||
|
||||
checkboxTopConstraint = checkbox.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor)
|
||||
checkboxTopConstraint?.isActive = true
|
||||
|
||||
checkboxCenterYConstraint = checkbox.centerYAnchor.constraint(equalTo: centerYAnchor)
|
||||
|
||||
label.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor).isActive = true
|
||||
layoutMarginsGuide.trailingAnchor.constraint(equalTo: label.trailingAnchor).isActive = true
|
||||
label.leadingAnchor.constraint(equalTo: checkbox.trailingAnchor, constant: PaddingTwo).isActive = true
|
||||
|
||||
layoutMarginsGuide.bottomAnchor.constraint(greaterThanOrEqualTo: label.bottomAnchor).isActive = true
|
||||
let bottomLabelConstraint = layoutMarginsGuide.bottomAnchor.constraint(equalTo: label.bottomAnchor)
|
||||
bottomLabelConstraint.priority = .defaultLow
|
||||
bottomLabelConstraint.isActive = true
|
||||
|
||||
alignCheckbox(.center)
|
||||
isAccessibilityElement = false
|
||||
accessibilityElements = [checkbox, label]
|
||||
observation = observe(\.checkbox.isSelected, options: [.new]) { [weak self] _, _ in
|
||||
self?.updateAccessibilityLabel()
|
||||
override open var isSelected: Bool {
|
||||
didSet {
|
||||
viewModel?.checkbox.selected = isSelected
|
||||
_ = FormValidator.validate(delegate: delegateObject?.formHolderDelegate)
|
||||
}
|
||||
}
|
||||
|
||||
@objc override open func updateView(_ size: CGFloat) {
|
||||
super.updateView(size)
|
||||
|
||||
label.updateView(size)
|
||||
checkbox.updateView(size)
|
||||
layoutIfNeeded()
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Methods
|
||||
//--------------------------------------------------
|
||||
|
||||
/// Aligns Checkbox and Label relative to the desired position of the Checkbox.
|
||||
private func alignCheckbox(_ position: CheckboxPosition) {
|
||||
checkboxPosition = position
|
||||
|
||||
switch position {
|
||||
case .center:
|
||||
checkboxBottomConstraint?.isActive = false
|
||||
checkboxTopConstraint?.isActive = false
|
||||
checkboxCenterYConstraint?.isActive = true
|
||||
|
||||
case .top:
|
||||
checkboxBottomConstraint?.isActive = false
|
||||
checkboxTopConstraint?.isActive = true
|
||||
checkboxCenterYConstraint?.isActive = false
|
||||
|
||||
case .bottom:
|
||||
checkboxBottomConstraint?.isActive = true
|
||||
checkboxTopConstraint?.isActive = false
|
||||
checkboxCenterYConstraint?.isActive = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Atomic
|
||||
//--------------------------------------------------
|
||||
open override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
|
||||
guard let checkBoxWithLabelModel = model as? CheckboxLabelModel else { return }
|
||||
|
||||
if let checkboxAlignment = checkBoxWithLabelModel.checkboxAlignment {
|
||||
alignCheckbox(checkboxAlignment)
|
||||
open func viewModelDidUpdate() {
|
||||
if let accessibilityIdentifier = viewModel.accessibilityIdentifier {
|
||||
self.accessibilityIdentifier = accessibilityIdentifier
|
||||
}
|
||||
surface = viewModel.surface
|
||||
|
||||
checkbox.set(with: checkBoxWithLabelModel.checkbox, delegateObject, additionalData)
|
||||
label.set(with: checkBoxWithLabelModel.label, delegateObject, additionalData)
|
||||
updateAccessibilityLabel()
|
||||
updateCheckbox()
|
||||
|
||||
//primary label
|
||||
labelText = viewModel.label?.text
|
||||
labelTextAttributes = viewModel.label?.attributes?.toVDSLabelAttributeModel(delegateObject: delegateObject, additionalData: additionalData)
|
||||
|
||||
//secondary label
|
||||
childText = viewModel.subTitle?.text
|
||||
childTextAttributes = viewModel.subTitle?.attributes?.toVDSLabelAttributeModel(delegateObject: delegateObject, additionalData: additionalData)
|
||||
}
|
||||
|
||||
open override class func estimatedHeight(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? {
|
||||
private func performCheckboxAction(with actionModel: ActionModelProtocol, delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) {
|
||||
MVMCoreUIActionHandler.performActionUnstructured(with: actionModel, sourceModel: viewModel.checkbox, additionalData: additionalData, delegateObject: delegateObject)
|
||||
}
|
||||
|
||||
open func updateCheckbox() {
|
||||
//forms
|
||||
FormValidator.setupValidation(for: viewModel.checkbox, delegate: delegateObject?.formHolderDelegate)
|
||||
groupName = viewModel.checkbox.groupName
|
||||
if let fieldKey = viewModel.checkbox.fieldKey {
|
||||
self.fieldKey = fieldKey
|
||||
}
|
||||
|
||||
//properties
|
||||
isAnimated = viewModel.checkbox.animated
|
||||
isEnabled = viewModel.checkbox.isEnabled
|
||||
|
||||
//call super here to go around the didSet
|
||||
//in this class
|
||||
super.isSelected = viewModel.checkbox.selected
|
||||
|
||||
//events
|
||||
viewModel.checkbox.updateUI = {
|
||||
MVMCoreDispatchUtility.performBlock(onMainThread: { [weak self] in
|
||||
guard let self = self else { return }
|
||||
//let isValid = viewModel.checkbox.isValid ?? true
|
||||
//TODO: Fix issue with default state
|
||||
//showError = !isValid
|
||||
errorText = viewModel.checkbox.errorMessage
|
||||
isEnabled = viewModel.checkbox.isEnabled
|
||||
})
|
||||
}
|
||||
|
||||
//onChange
|
||||
if (viewModel.checkbox.action != nil || viewModel.checkbox.offAction != nil) {
|
||||
onChange = { [weak self] control in
|
||||
guard let self = self else { return }
|
||||
|
||||
if let offAction = viewModel.checkbox.offAction, !isSelected {
|
||||
performCheckboxAction(with: offAction, delegateObject: delegateObject, additionalData: additionalData)
|
||||
|
||||
} else if let action = viewModel.checkbox.action {
|
||||
performCheckboxAction(with: action, delegateObject: delegateObject, additionalData: additionalData)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@objc open func updateView(_ size: CGFloat) {}
|
||||
|
||||
open class func estimatedHeight(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? {
|
||||
return 200
|
||||
}
|
||||
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
|
||||
label.text = ""
|
||||
checkbox.reset()
|
||||
alignCheckbox(.center)
|
||||
}
|
||||
|
||||
override open func accessibilityActivate() -> Bool {
|
||||
checkbox.accessibilityActivate()
|
||||
}
|
||||
|
||||
open func updateAccessibilityLabel() {
|
||||
checkbox.updateAccessibilityLabel()
|
||||
if let text = label.text {
|
||||
checkbox.accessibilityLabel?.append(", \(text)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,31 +8,67 @@
|
||||
|
||||
import Foundation
|
||||
import MVMCore
|
||||
|
||||
public enum CheckboxPosition: String, Codable {
|
||||
case center
|
||||
case top
|
||||
case bottom
|
||||
}
|
||||
import VDS
|
||||
|
||||
@objcMembers open class CheckboxLabelModel: MoleculeModelProtocol, ParentMoleculeModelProtocol {
|
||||
open class var identifier: String { "checkboxLabel" }
|
||||
public var moleculeName: String = CheckboxLabelModel.identifier
|
||||
@DecodableDefault.UUIDString public var id: String
|
||||
|
||||
public var accessibilityIdentifier: String?
|
||||
|
||||
public var backgroundColor: Color?
|
||||
public var checkboxAlignment: CheckboxPosition?
|
||||
public var checkbox: CheckboxModel
|
||||
public var label: LabelModel
|
||||
public var label: LabelModel?
|
||||
public var subTitle: LabelModel?
|
||||
public var inverted: Bool? = false
|
||||
public var surface: Surface { inverted ?? false ? .dark : .light }
|
||||
|
||||
public var children: [MoleculeModelProtocol] {
|
||||
var values: [MoleculeModelProtocol] = [checkbox]
|
||||
if let label { values.append(label) }
|
||||
if let subTitle { values.append(subTitle) }
|
||||
return values
|
||||
}
|
||||
|
||||
public var children: [MoleculeModelProtocol] { [checkbox, label] }
|
||||
//--------------------------------------------------
|
||||
// MARK: - Initializer
|
||||
//--------------------------------------------------
|
||||
|
||||
public init(checkbox: CheckboxModel, label: LabelModel) {
|
||||
public init(checkbox: CheckboxModel, label: LabelModel, subTitle: LabelModel?) {
|
||||
self.checkbox = checkbox
|
||||
self.label = label
|
||||
self.subTitle = subTitle
|
||||
}
|
||||
|
||||
open func isEqual(to model: any ModelComparisonProtocol) -> Bool {
|
||||
guard let model = model as? Self else { return false }
|
||||
return moleculeName == model.moleculeName
|
||||
&& inverted == model.inverted
|
||||
&& accessibilityText == model.accessibilityText
|
||||
&& accessibilityIdentifier == model.accessibilityIdentifier
|
||||
&& accessibilityTraits == model.accessibilityTraits
|
||||
}
|
||||
}
|
||||
|
||||
extension Array where Element == CheckboxLabelModel {
|
||||
internal func convertToVDSCheckboxItemModel(surface: Surface,
|
||||
delegateObject: MVMCoreUIDelegateObject?,
|
||||
additionalData: [AnyHashable: Any]?) -> [CheckboxGroup.CheckboxItemModel] {
|
||||
return compactMap({ model in
|
||||
var item = CheckboxGroup.CheckboxItemModel()
|
||||
item.inputId = model.checkbox.fieldKey
|
||||
item.labelText = model.label?.text
|
||||
if let attributes = model.label?.attributes?.toVDSLabelAttributeModel(delegateObject: delegateObject, additionalData: additionalData) {
|
||||
item.labelTextAttributes = attributes
|
||||
}
|
||||
item.childText = model.subTitle?.text
|
||||
if let attributes = model.subTitle?.attributes?.toVDSLabelAttributeModel(delegateObject: delegateObject, additionalData: additionalData) {
|
||||
item.childTextAttributes = attributes
|
||||
}
|
||||
item.surface = surface
|
||||
item.selected = model.checkbox.selected
|
||||
item.enabled = model.checkbox.isEnabled
|
||||
return item
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
128
MVMCoreUI/Atomic/Atoms/Views/CircularProgressBar.swift
Normal file
128
MVMCoreUI/Atomic/Atoms/Views/CircularProgressBar.swift
Normal file
@ -0,0 +1,128 @@
|
||||
//
|
||||
// CircularProgressBar.swift
|
||||
// MVMCoreUI
|
||||
//
|
||||
// Created by Xi Zhang on 7/5/24.
|
||||
// Copyright © 2024 Verizon Wireless. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
@objcMembers open class CircularProgressBar: View, MVMCoreUIViewConstrainingProtocol {
|
||||
|
||||
var heightConstraint: NSLayoutConstraint?
|
||||
var graphModel: CircularProgressBarModel? {
|
||||
return model as? CircularProgressBarModel
|
||||
}
|
||||
|
||||
var viewWidth: CGFloat {
|
||||
graphModel?.diameter ?? CGFloat(64)
|
||||
}
|
||||
|
||||
private var progressLayer = CAShapeLayer()
|
||||
private var tracklayer = CAShapeLayer()
|
||||
private var labelLayer = CATextLayer()
|
||||
|
||||
var progressColor: UIColor = UIColor.red
|
||||
var trackColor: UIColor = UIColor.lightGray
|
||||
|
||||
// A path with which CAShapeLayer will be drawn on the screen
|
||||
private var viewCGPath: CGPath? {
|
||||
|
||||
let width = viewWidth
|
||||
let height = width
|
||||
|
||||
return UIBezierPath(arcCenter: CGPoint(x: width / 2.0, y: height / 2.0),
|
||||
radius: (width - 1.5)/2,
|
||||
startAngle: CGFloat(-0.5 * Double.pi),
|
||||
endAngle: CGFloat(1.5 * Double.pi), clockwise: true).cgPath
|
||||
}
|
||||
|
||||
// MARK: setup
|
||||
override open func setupView() {
|
||||
super.setupView()
|
||||
heightConstraint = heightAnchor.constraint(equalToConstant: 0)
|
||||
heightConstraint?.isActive = true
|
||||
widthAnchor.constraint(equalTo: heightAnchor).isActive = true
|
||||
}
|
||||
|
||||
override open func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
|
||||
|
||||
super.set(with: model, delegateObject, additionalData)
|
||||
guard let model = model as? CircularProgressBarModel else { return }
|
||||
|
||||
// set background color
|
||||
backgroundColor = model.backgroundColor?.uiColor ?? UIColor.clear
|
||||
|
||||
configureProgressViewToBeCircular()
|
||||
|
||||
// set progress color
|
||||
progressColor = model.color?.uiColor ?? .red
|
||||
progressLayer.strokeColor = progressColor.cgColor
|
||||
|
||||
// set track color
|
||||
trackColor = model.trackColor?.uiColor ?? .lightGray
|
||||
tracklayer.strokeColor = trackColor.cgColor
|
||||
|
||||
// show circular progress view with animation.
|
||||
showProgressWithAnimation(duration: graphModel?.duration ?? 0, value: Float(graphModel?.percent ?? 0) / 100)
|
||||
|
||||
// show progress percentage label.
|
||||
if let drawText = model.drawText, drawText {
|
||||
showProgressPercentage()
|
||||
}
|
||||
}
|
||||
|
||||
private func configureProgressViewToBeCircular() {
|
||||
let lineWidth = graphModel?.lineWidth ?? 4.0
|
||||
|
||||
self.drawShape(using: tracklayer, lineWidth: lineWidth)
|
||||
self.drawShape(using: progressLayer, lineWidth: lineWidth)
|
||||
}
|
||||
|
||||
private func drawShape(using shape: CAShapeLayer, lineWidth: CGFloat) {
|
||||
shape.path = self.viewCGPath
|
||||
shape.fillColor = UIColor.clear.cgColor
|
||||
shape.lineWidth = lineWidth
|
||||
self.layer.addSublayer(shape)
|
||||
}
|
||||
|
||||
// value range is [0,1]
|
||||
private func showProgressWithAnimation(duration: TimeInterval, value: Float) {
|
||||
let animation = CABasicAnimation(keyPath: "strokeEnd")
|
||||
animation.duration = duration
|
||||
|
||||
animation.fromValue = 0 //start animation at point 0
|
||||
animation.toValue = value //end animation at point specified
|
||||
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
|
||||
progressLayer.strokeEnd = CGFloat(value)
|
||||
progressLayer.add(animation, forKey: "animateCircle")
|
||||
}
|
||||
|
||||
private func showProgressPercentage() {
|
||||
|
||||
let percent = graphModel?.percent ?? 0
|
||||
let percentLen = String(percent).count
|
||||
|
||||
// configure attributed string for progress percentage.
|
||||
let attributedString = NSMutableAttributedString(string: String(percent) + "%")
|
||||
// percent value
|
||||
attributedString.setAttributes([NSAttributedString.Key.font: Styler.Font.BoldTitleXLarge], range: NSMakeRange(0, percentLen))
|
||||
// % symbol
|
||||
attributedString.setAttributes([NSAttributedString.Key.font: Styler.Font.RegularMicro], range: NSMakeRange(percentLen, 1))
|
||||
|
||||
// show progress percentage in a text layer
|
||||
let width = viewWidth
|
||||
let height = width
|
||||
labelLayer.string = attributedString
|
||||
labelLayer.frame = CGRectMake((width - CGFloat(percentLen * 20))/2, (height - 40)/2, 80, 40)
|
||||
self.layer.addSublayer(labelLayer)
|
||||
}
|
||||
|
||||
|
||||
//MARK: MVMCoreUIViewConstrainingProtocol
|
||||
public func needsToBeConstrained() -> Bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
123
MVMCoreUI/Atomic/Atoms/Views/CircularProgressBarModel.swift
Normal file
123
MVMCoreUI/Atomic/Atoms/Views/CircularProgressBarModel.swift
Normal file
@ -0,0 +1,123 @@
|
||||
//
|
||||
// CircularProgressBarModel.swift
|
||||
// MVMCoreUI
|
||||
//
|
||||
// https://oneconfluence.verizon.com/display/MFD/Circular+Progress+Tracker
|
||||
//
|
||||
// Created by Xi Zhang on 7/5/24.
|
||||
// Copyright © 2024 Verizon Wireless. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public class CircularProgressBarModel: GraphSizeBase, MoleculeModelProtocol {
|
||||
|
||||
public static var identifier: String = "circularProgress"
|
||||
public var id: String = UUID().uuidString
|
||||
public var accessibilityIdentifier: String?
|
||||
|
||||
public var percent: Int = 0
|
||||
public var diameter: CGFloat? = 64
|
||||
public var lineWidth: CGFloat? = 4
|
||||
public var duration : Double? = 0
|
||||
public var color: Color? = Color(uiColor: UIColor.mfGet(forHex: "#007AB8"))
|
||||
public var trackColor: Color? = Color(uiColor: .mvmCoolGray3)
|
||||
public var drawText: Bool? = true
|
||||
public var backgroundColor: Color? = Color(uiColor: UIColor.clear)
|
||||
|
||||
public override init() {
|
||||
super.init()
|
||||
updateSize()
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case accessibilityIdentifier
|
||||
case moleculeName
|
||||
case percent
|
||||
case size
|
||||
case diameter
|
||||
case lineWidth
|
||||
case duration
|
||||
case color
|
||||
case trackColor
|
||||
case drawText
|
||||
case backgroundColor
|
||||
}
|
||||
|
||||
required public init(from decoder: Decoder) throws {
|
||||
|
||||
super.init()
|
||||
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
|
||||
id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString
|
||||
accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier)
|
||||
|
||||
percent = try typeContainer.decode(Int.self, forKey: .percent)
|
||||
|
||||
if let size = try typeContainer.decodeIfPresent(GraphSize.self, forKey: .size) {
|
||||
self.size = size
|
||||
}
|
||||
updateSize()
|
||||
|
||||
if let diameter = try typeContainer.decodeIfPresent(CGFloat.self, forKey: .diameter) {
|
||||
self.diameter = diameter
|
||||
}
|
||||
|
||||
if let lineWidth = try typeContainer.decodeIfPresent(CGFloat.self, forKey: .lineWidth) {
|
||||
self.lineWidth = lineWidth
|
||||
}
|
||||
|
||||
if let duration = try typeContainer.decodeIfPresent(Double.self, forKey: .duration) {
|
||||
self.duration = duration
|
||||
}
|
||||
|
||||
if let drawText = try typeContainer.decodeIfPresent(Bool.self, forKey: .drawText) {
|
||||
self.drawText = drawText
|
||||
}
|
||||
|
||||
if let color = try typeContainer.decodeIfPresent(Color.self, forKey: .color) {
|
||||
self.color = color
|
||||
}
|
||||
|
||||
if let trackColor = try typeContainer.decodeIfPresent(Color.self, forKey: .trackColor) {
|
||||
self.trackColor = trackColor
|
||||
}
|
||||
|
||||
if let backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor) {
|
||||
self.backgroundColor = backgroundColor
|
||||
}
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(id, forKey: .id)
|
||||
try container.encode(moleculeName, forKey: .moleculeName)
|
||||
try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier)
|
||||
try container.encode(percent, forKey: .percent)
|
||||
try container.encodeIfPresent(size, forKey: .size)
|
||||
try container.encodeIfPresent(diameter, forKey: .diameter)
|
||||
try container.encodeIfPresent(lineWidth, forKey: .lineWidth)
|
||||
try container.encodeIfPresent(duration, forKey: .duration)
|
||||
try container.encodeIfPresent(drawText, forKey: .drawText)
|
||||
try container.encodeIfPresent(trackColor, forKey: .trackColor)
|
||||
try container.encodeIfPresent(color, forKey: .color)
|
||||
try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor)
|
||||
}
|
||||
|
||||
public override func updateSize() {
|
||||
switch size {
|
||||
case .small:
|
||||
diameter = MFSizeObject(standardSize: 64)?.getValueBasedOnApplicationWidth() ?? 64
|
||||
lineWidth = MFSizeObject(standardSize: 4)?.getValueBasedOnApplicationWidth() ?? 4
|
||||
break
|
||||
case .medium:
|
||||
diameter = MFSizeObject(standardSize: 84)?.getValueBasedOnApplicationWidth() ?? 84
|
||||
lineWidth = MFSizeObject(standardSize: 4)?.getValueBasedOnApplicationWidth() ?? 4
|
||||
break
|
||||
case .large:
|
||||
diameter = MFSizeObject(standardSize: 124)?.getValueBasedOnApplicationWidth() ?? 124
|
||||
lineWidth = MFSizeObject(standardSize: 4)?.getValueBasedOnApplicationWidth() ?? 4
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
29
MVMCoreUI/Atomic/Atoms/Views/GraphSizeProtocol.swift
Normal file
29
MVMCoreUI/Atomic/Atoms/Views/GraphSizeProtocol.swift
Normal file
@ -0,0 +1,29 @@
|
||||
//
|
||||
// GraphSizeProtocol.swift
|
||||
// MVMCoreUI
|
||||
//
|
||||
// Created by Xi Zhang on 7/15/24.
|
||||
// Copyright © 2024 Verizon Wireless. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public enum GraphSize: String, Codable {
|
||||
case small, medium, large
|
||||
}
|
||||
|
||||
public protocol GraphSizeProtocol {
|
||||
var size: GraphSize { get set }
|
||||
func updateSize()
|
||||
}
|
||||
|
||||
public class GraphSizeBase: GraphSizeProtocol {
|
||||
public var size: GraphSize = .small {
|
||||
didSet {
|
||||
updateSize()
|
||||
}
|
||||
}
|
||||
|
||||
public func updateSize() {
|
||||
}
|
||||
}
|
||||
@ -31,11 +31,15 @@ open class Icon: VDS.Icon, VDSMoleculeViewProtocol{
|
||||
// MARK: - Public
|
||||
//--------------------------------------------------
|
||||
public func viewModelDidUpdate() {
|
||||
if let accessibilityIdentifier = viewModel.accessibilityIdentifier {
|
||||
self.accessibilityIdentifier = accessibilityIdentifier
|
||||
}
|
||||
surface = viewModel.surface
|
||||
color = viewModel.color.uiColor
|
||||
size = viewModel.size
|
||||
customSize = viewModel.customSize
|
||||
name = viewModel.name
|
||||
isAccessibilityElement = viewModel.isAccessibilityElement ?? true
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
|
||||
@ -21,6 +21,8 @@ open class IconModel: MoleculeModelProtocol {
|
||||
|
||||
public var backgroundColor: Color?
|
||||
|
||||
public var accessibilityIdentifier: String?
|
||||
|
||||
/// A representation that will be used to render the icon with corresponding name.
|
||||
public var name: Icon.Name
|
||||
|
||||
@ -35,4 +37,6 @@ open class IconModel: MoleculeModelProtocol {
|
||||
|
||||
/// A custom size of the icon.
|
||||
public var customSize: Int?
|
||||
|
||||
public var isAccessibilityElement: Bool?
|
||||
}
|
||||
|
||||
@ -18,6 +18,7 @@
|
||||
public var backgroundColor: Color?
|
||||
public var moleculeName: String = ImageViewModel.identifier
|
||||
public var image: String
|
||||
public var accessibilityIdentifier: String?
|
||||
public var accessibilityText: String?
|
||||
public var fallbackImage: String?
|
||||
public var imageFormat: String?
|
||||
@ -47,6 +48,7 @@
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case accessibilityIdentifier
|
||||
case moleculeName
|
||||
case backgroundColor
|
||||
case image
|
||||
|
||||
@ -16,13 +16,15 @@ public class FormLabel: Label {
|
||||
//public properties
|
||||
public override var isEnabled: Bool {
|
||||
didSet{
|
||||
self.formModel.enabled = isEnabled
|
||||
guard let formModel else { return }
|
||||
formModel.enabled = isEnabled
|
||||
self.set(with: isRequired ? formModel.model : formModel.requiredModel, delegateObject, additionalData)
|
||||
}
|
||||
}
|
||||
|
||||
public var isRequired: Bool = true {
|
||||
didSet{
|
||||
guard let formModel else { return }
|
||||
self.set(with: isRequired ? formModel.model : formModel.requiredModel, delegateObject, additionalData)
|
||||
}
|
||||
}
|
||||
@ -50,6 +52,7 @@ public class FormLabel: Label {
|
||||
/// Text change that will update both enabledModel and disabledModel text values
|
||||
/// - Parameter text: text you want to see
|
||||
public func set(text: String?){
|
||||
self.formModel.set(text: text ?? "")
|
||||
guard let formModel else { return }
|
||||
formModel.set(text: text ?? "")
|
||||
}
|
||||
}
|
||||
|
||||
@ -34,11 +34,7 @@ public typealias ActionBlock = () -> ()
|
||||
|
||||
/// A specific text index to use as a unique marker.
|
||||
public var hero: Int?
|
||||
|
||||
public var getRange: NSRange {
|
||||
NSRange(location: 0, length: text?.count ?? 0)
|
||||
}
|
||||
|
||||
|
||||
public var shouldMaskWhileRecording: Bool = false
|
||||
|
||||
public var hasText: Bool {
|
||||
@ -186,6 +182,9 @@ public typealias ActionBlock = () -> ()
|
||||
}
|
||||
|
||||
public func viewModelDidUpdate() {
|
||||
if let accessibilityIdentifier = viewModel.accessibilityIdentifier {
|
||||
self.accessibilityIdentifier = accessibilityIdentifier
|
||||
}
|
||||
shouldMaskWhileRecording = viewModel.shouldMaskRecordedView ?? false
|
||||
text = viewModel.text
|
||||
hero = viewModel.hero
|
||||
@ -364,8 +363,8 @@ extension Label {
|
||||
public static func boundingRect(forCharacterRange range: NSRange, in label: Label) -> CGRect {
|
||||
|
||||
guard let abstractContainer = label.abstractTextContainer() else { return CGRect() }
|
||||
let textContainer = abstractContainer.0
|
||||
let layoutManager = abstractContainer.1
|
||||
let textContainer = abstractContainer.textContainer
|
||||
let layoutManager = abstractContainer.layoutManager
|
||||
|
||||
var glyphRange = NSRange()
|
||||
|
||||
@ -374,53 +373,28 @@ extension Label {
|
||||
|
||||
return layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)
|
||||
}
|
||||
|
||||
/**
|
||||
Provides a text container and layout manager of how the text would appear on screen.
|
||||
They are used in tandem to derive low-level TextKit results of the label.
|
||||
*/
|
||||
public func abstractTextContainer() -> (NSTextContainer, NSLayoutManager, NSTextStorage)? {
|
||||
|
||||
// Must configure the attributed string to translate what would appear on screen to accurately analyze.
|
||||
guard let attributedText = attributedText else { return nil }
|
||||
|
||||
let paragraph = NSMutableParagraphStyle()
|
||||
paragraph.alignment = textAlignment
|
||||
|
||||
let stagedAttributedString = NSMutableAttributedString(attributedString: attributedText)
|
||||
stagedAttributedString.addAttributes([NSAttributedString.Key.paragraphStyle: paragraph], range: NSRange(location: 0, length: attributedText.string.count))
|
||||
|
||||
let textStorage = NSTextStorage(attributedString: stagedAttributedString)
|
||||
let layoutManager = NSLayoutManager()
|
||||
let textContainer = NSTextContainer(size: .zero)
|
||||
|
||||
layoutManager.addTextContainer(textContainer)
|
||||
textStorage.addLayoutManager(layoutManager)
|
||||
|
||||
textContainer.lineFragmentPadding = 0.0
|
||||
textContainer.lineBreakMode = lineBreakMode
|
||||
textContainer.maximumNumberOfLines = numberOfLines
|
||||
textContainer.size = bounds.size
|
||||
|
||||
return (textContainer, layoutManager, textStorage)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Atomization
|
||||
extension Label {
|
||||
|
||||
|
||||
public func needsToBeConstrained() -> Bool { true }
|
||||
|
||||
public func horizontalAlignment() -> UIStackView.Alignment { .leading }
|
||||
|
||||
public func copyBackgroundColor() -> Bool { true }
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Multi-Link Functionality
|
||||
extension Label {
|
||||
extension VDS.Label {
|
||||
|
||||
public var getRange: NSRange {
|
||||
NSRange(location: 0, length: text?.count ?? 0)
|
||||
}
|
||||
|
||||
/// Underlines the tappable region and stores the tap logic for interation.
|
||||
private func setTextLinkState(range: NSRange, actionBlock: @escaping ActionBlock) {
|
||||
internal func setTextLinkState(range: NSRange, actionBlock: @escaping ActionBlock) {
|
||||
guard let text, text.isValid(range: range) else { return }
|
||||
|
||||
var textLink = ActionLabelAttribute(location: range.location, length: range.length)
|
||||
@ -447,8 +421,16 @@ extension Label {
|
||||
return { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
if (delegateObject as? MVMCoreUIDelegateObject)?.buttonDelegate?.button?(self, shouldPerformActionWithMap: actionMap, additionalData: additionalData) ?? true {
|
||||
MVMCoreActionHandler.shared()?.handleAction(with: actionMap, additionalData: additionalData, delegateObject: delegateObject)
|
||||
if let button = self as? MFButtonProtocol {
|
||||
if (delegateObject as? MVMCoreUIDelegateObject)?.buttonDelegate?.button?(button, shouldPerformActionWithMap: actionMap, additionalData: additionalData) ?? true {
|
||||
MVMCoreActionHandler.shared()?.handleAction(with: actionMap,
|
||||
additionalData: additionalData,
|
||||
delegateObject: delegateObject)
|
||||
}
|
||||
} else {
|
||||
MVMCoreActionHandler.shared()?.handleAction(with: actionMap,
|
||||
additionalData: additionalData,
|
||||
delegateObject: delegateObject)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,7 +14,8 @@ import VDS
|
||||
|
||||
open class var identifier: String { "label" }
|
||||
public var id: String
|
||||
|
||||
public var accessibilityIdentifier: String?
|
||||
|
||||
public var backgroundColor: Color?
|
||||
public var text: String
|
||||
public var accessibilityText: String?
|
||||
@ -38,6 +39,7 @@ import VDS
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case accessibilityIdentifier
|
||||
case moleculeName
|
||||
case text
|
||||
case accessibilityText
|
||||
@ -88,6 +90,7 @@ import VDS
|
||||
required public init(from decoder: Decoder) throws {
|
||||
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
|
||||
id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString
|
||||
accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier)
|
||||
text = try typeContainer.decode(String.self, forKey: .text)
|
||||
accessibilityText = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityText)
|
||||
textColor = try typeContainer.decodeIfPresent(Color.self, forKey: .textColor)
|
||||
@ -115,6 +118,7 @@ import VDS
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encodeIfPresent(moleculeName, forKey: .moleculeName)
|
||||
try container.encode(id, forKey: .id)
|
||||
try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier)
|
||||
try container.encode(text, forKey: .text)
|
||||
try container.encodeIfPresent(accessibilityText, forKey: .accessibilityText)
|
||||
try container.encodeIfPresent(textColor, forKey: .textColor)
|
||||
|
||||
@ -16,7 +16,8 @@ import UIKit
|
||||
public static var identifier: String = "leftRightLabelView"
|
||||
public var moleculeName: String = LeftRightLabelModel.identifier
|
||||
@DecodableDefault.UUIDString public var id: String
|
||||
|
||||
public var accessibilityIdentifier: String?
|
||||
|
||||
public var backgroundColor: Color?
|
||||
public var leftText: LabelModel
|
||||
public var rightText: LabelModel?
|
||||
|
||||
@ -66,7 +66,7 @@ import VDS
|
||||
}
|
||||
|
||||
open func setStyle(_ style: LineModel.Style) {
|
||||
viewModel.type = style
|
||||
viewModel?.type = style
|
||||
update(viewModel: viewModel)
|
||||
}
|
||||
|
||||
@ -85,7 +85,7 @@ import VDS
|
||||
}
|
||||
|
||||
open override func draw(_ rect: CGRect) {
|
||||
guard viewModel.type != .none else { return }
|
||||
guard let viewModel, viewModel.type != .none else { return }
|
||||
super.draw(rect)
|
||||
}
|
||||
|
||||
@ -93,6 +93,9 @@ import VDS
|
||||
// MARK: - VDSMoleculeViewProtocol
|
||||
//--------------------------------------------------
|
||||
open func viewModelDidUpdate() {
|
||||
if let accessibilityIdentifier = viewModel.accessibilityIdentifier {
|
||||
self.accessibilityIdentifier = accessibilityIdentifier
|
||||
}
|
||||
surface = viewModel.surface
|
||||
style = VDS.Line.Style(rawValue: viewModel.type.rawValue) ?? .primary
|
||||
orientation = viewModel.orientation
|
||||
|
||||
@ -55,6 +55,7 @@ public class LineModel: MoleculeModelProtocol, Invertable {
|
||||
|
||||
public static var identifier: String = "line"
|
||||
public var id: String = UUID().uuidString
|
||||
public var accessibilityIdentifier: String?
|
||||
public var backgroundColor: Color?
|
||||
|
||||
public var type: Style = .secondary
|
||||
@ -83,6 +84,7 @@ public class LineModel: MoleculeModelProtocol, Invertable {
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case accessibilityIdentifier
|
||||
case moleculeName
|
||||
case type
|
||||
case frequency
|
||||
@ -99,7 +101,8 @@ public class LineModel: MoleculeModelProtocol, Invertable {
|
||||
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString
|
||||
|
||||
accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier)
|
||||
|
||||
if let type = try typeContainer.decodeIfPresent(Style.self, forKey: .type) {
|
||||
self.type = type
|
||||
}
|
||||
@ -124,6 +127,7 @@ public class LineModel: MoleculeModelProtocol, Invertable {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(moleculeName, forKey: .moleculeName)
|
||||
try container.encode(id, forKey: .id)
|
||||
try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier)
|
||||
try container.encode(type, forKey: .type)
|
||||
try container.encode(inverted, forKey: .inverted)
|
||||
try container.encodeIfPresent(frequency, forKey: .frequency)
|
||||
|
||||
@ -22,6 +22,9 @@ open class LoadingSpinner: VDS.Loader, VDSMoleculeViewProtocol {
|
||||
// MARK: - Public Functions
|
||||
//--------------------------------------------------
|
||||
open func viewModelDidUpdate() {
|
||||
if let accessibilityIdentifier = viewModel.accessibilityIdentifier {
|
||||
self.accessibilityIdentifier = accessibilityIdentifier
|
||||
}
|
||||
size = Int(viewModel.diameter)
|
||||
surface = viewModel.surface
|
||||
}
|
||||
|
||||
@ -16,7 +16,8 @@ open class LoadingSpinnerModel: MoleculeModelProtocol {
|
||||
//--------------------------------------------------
|
||||
public static var identifier: String = "loadingSpinner"
|
||||
public var id: String = UUID().uuidString
|
||||
|
||||
public var accessibilityIdentifier: String?
|
||||
|
||||
public var backgroundColor: Color?
|
||||
public var inverted: Bool = false
|
||||
public var diameter: CGFloat = 40
|
||||
@ -27,6 +28,7 @@ open class LoadingSpinnerModel: MoleculeModelProtocol {
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case accessibilityIdentifier
|
||||
case moleculeName
|
||||
case strokeColor
|
||||
case diameter
|
||||
@ -47,7 +49,8 @@ open class LoadingSpinnerModel: MoleculeModelProtocol {
|
||||
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString
|
||||
|
||||
accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier)
|
||||
|
||||
if let diameter = try typeContainer.decodeIfPresent(CGFloat.self, forKey: .diameter) {
|
||||
self.diameter = diameter
|
||||
}
|
||||
@ -65,6 +68,7 @@ open class LoadingSpinnerModel: MoleculeModelProtocol {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(id, forKey: .id)
|
||||
try container.encode(moleculeName, forKey: .moleculeName)
|
||||
try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier)
|
||||
try container.encodeIfPresent(diameter, forKey: .diameter)
|
||||
try container.encodeIfPresent(inverted, forKey: .inverted)
|
||||
}
|
||||
|
||||
@ -22,7 +22,8 @@ import Foundation
|
||||
|
||||
public static var identifier: String = "multiProgressBar"
|
||||
public var id: String = UUID().uuidString
|
||||
|
||||
public var accessibilityIdentifier: String?
|
||||
|
||||
public var progressList: [SingleProgressBarModel]
|
||||
public var backgroundColor: Color?
|
||||
public var thickness: CGFloat?
|
||||
@ -30,6 +31,7 @@ import Foundation
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case accessibilityIdentifier
|
||||
case moleculeName
|
||||
case progressList
|
||||
case thickness
|
||||
@ -44,6 +46,7 @@ import Foundation
|
||||
required public init(from decoder: Decoder) throws {
|
||||
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
|
||||
id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString
|
||||
accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier)
|
||||
progressList = try typeContainer.decode([SingleProgressBarModel].self, forKey: .progressList)
|
||||
backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor)
|
||||
thickness = try typeContainer.decodeIfPresent(CGFloat.self, forKey: .thickness)
|
||||
@ -54,6 +57,7 @@ import Foundation
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(id, forKey: .id)
|
||||
try container.encode(moleculeName, forKey: .moleculeName)
|
||||
try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier)
|
||||
try container.encode(progressList, forKey: .progressList)
|
||||
try container.encodeIfPresent(thickness, forKey: .thickness)
|
||||
try container.encodeIfPresent(roundedCorners, forKey: .roundedCorners)
|
||||
|
||||
60
MVMCoreUI/Atomic/Atoms/Views/Pagination.swift
Normal file
60
MVMCoreUI/Atomic/Atoms/Views/Pagination.swift
Normal file
@ -0,0 +1,60 @@
|
||||
//
|
||||
// Pagination.swift
|
||||
// MVMCoreUI
|
||||
//
|
||||
// Created by Matt Bruce on 8/27/24.
|
||||
// Copyright © 2024 Verizon Wireless. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import VDS
|
||||
|
||||
@objcMembers open class Pagination: VDS.Pagination, VDSMoleculeViewProtocol {
|
||||
//------------------------------------------------------
|
||||
// MARK: - Properties
|
||||
//------------------------------------------------------
|
||||
open var viewModel: PaginationModel!
|
||||
open var delegateObject: MVMCoreUIDelegateObject?
|
||||
open var additionalData: [AnyHashable : Any]?
|
||||
|
||||
// Form Validation
|
||||
open var fieldKey: String?
|
||||
open var fieldValue: JSONValue?
|
||||
open var groupName: String?
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Initializers
|
||||
//--------------------------------------------------
|
||||
public convenience required init() {
|
||||
self.init(frame:.zero)
|
||||
}
|
||||
|
||||
open override func setup() {
|
||||
super.setup()
|
||||
pageChangedPublisher
|
||||
.sink { [weak self] control in
|
||||
guard let self else { return }
|
||||
viewModel?.selectedPage = control.selectedPage
|
||||
}.store(in: &subscribers)
|
||||
}
|
||||
|
||||
open func viewModelDidUpdate() {
|
||||
if let accessibilityIdentifier = viewModel.accessibilityIdentifier {
|
||||
self.accessibilityIdentifier = accessibilityIdentifier
|
||||
}
|
||||
isEnabled = viewModel.enabled
|
||||
surface = viewModel.surface
|
||||
total = viewModel.totalPages
|
||||
selectedPage = viewModel.selectedPage
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Actions
|
||||
//--------------------------------------------------
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - MoleculeViewProtocol
|
||||
//--------------------------------------------------
|
||||
public func updateView(_ size: CGFloat) {}
|
||||
|
||||
}
|
||||
76
MVMCoreUI/Atomic/Atoms/Views/PaginationModel.swift
Normal file
76
MVMCoreUI/Atomic/Atoms/Views/PaginationModel.swift
Normal file
@ -0,0 +1,76 @@
|
||||
//
|
||||
// PaginationModel.swift
|
||||
// MVMCoreUI
|
||||
//
|
||||
// Created by Matt Bruce on 8/27/24.
|
||||
// Copyright © 2024 Verizon Wireless. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import VDS
|
||||
import MVMCore
|
||||
|
||||
open class PaginationModel: MoleculeModelProtocol {
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Properties
|
||||
//--------------------------------------------------
|
||||
open class var identifier: String { "pagination" }
|
||||
open var moleculeName: String { Self.identifier }
|
||||
open var backgroundColor: Color?
|
||||
open var id: String = UUID().uuidString
|
||||
open var accessibilityIdentifier: String?
|
||||
|
||||
open var totalPages: Int = 0
|
||||
open var selectedPage: Int = 0
|
||||
open var enabled: Bool = true
|
||||
open var inverted: Bool = false
|
||||
open var surface: Surface { inverted ? .dark : .light }
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Keys
|
||||
//--------------------------------------------------
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case moleculeName
|
||||
case accessibilityIdentifier
|
||||
case totalPages
|
||||
case selectedPage
|
||||
case enabled
|
||||
case inverted
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Initializers
|
||||
//--------------------------------------------------
|
||||
|
||||
required public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
id = try container.decode(String.self, forKey: .id)
|
||||
accessibilityIdentifier = try container.decodeIfPresent(String.self, forKey: .accessibilityIdentifier)
|
||||
totalPages = try container.decode(Int.self, forKey: .totalPages)
|
||||
selectedPage = try container.decodeIfPresent(Int.self, forKey: .selectedPage) ?? 0
|
||||
enabled = try container.decodeIfPresent(Bool.self, forKey: .enabled) ?? false
|
||||
inverted = try container.decodeIfPresent(Bool.self, forKey: .inverted) ?? false
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(id, forKey: .id)
|
||||
try container.encode(moleculeName, forKey: .moleculeName)
|
||||
try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier)
|
||||
try container.encode(totalPages, forKey: .totalPages)
|
||||
try container.encode(selectedPage, forKey: .selectedPage)
|
||||
try container.encode(enabled, forKey: .enabled)
|
||||
try container.encode(inverted, forKey: .inverted)
|
||||
}
|
||||
|
||||
public func isEqual(to model: any ModelComparisonProtocol) -> Bool {
|
||||
guard let model = model as? Self else { return false }
|
||||
return inverted == model.inverted
|
||||
&& enabled == model.enabled
|
||||
&& totalPages == model.totalPages
|
||||
&& selectedPage == model.selectedPage
|
||||
}
|
||||
}
|
||||
@ -75,6 +75,9 @@ import Foundation
|
||||
guard let progressBarModel = model as? ProgressBarModel else { return }
|
||||
|
||||
self.progressBarModel = progressBarModel
|
||||
if let accessibilityIdentifier = model.accessibilityIdentifier {
|
||||
self.accessibilityIdentifier = accessibilityIdentifier
|
||||
}
|
||||
thickness = progressBarModel.thickness ?? 8
|
||||
progress = Float((progressBarModel.percent) / 100.0)
|
||||
progressTintColor = progressBarModel.color.uiColor
|
||||
|
||||
@ -11,7 +11,8 @@ import Foundation
|
||||
@objcMembers open class ProgressBarModel: MoleculeModelProtocol {
|
||||
open class var identifier: String { "progressBar" }
|
||||
public var id: String = UUID().uuidString
|
||||
|
||||
public var accessibilityIdentifier: String?
|
||||
|
||||
@Percent public var percent: CGFloat
|
||||
public var color: Color = Color(uiColor: .mfCerulean())
|
||||
public var backgroundColor: Color? = Color(uiColor: .mfLightSilver())
|
||||
@ -20,6 +21,7 @@ import Foundation
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case accessibilityIdentifier
|
||||
case moleculeName
|
||||
case roundedCorners
|
||||
case thickness
|
||||
@ -35,6 +37,7 @@ import Foundation
|
||||
required public init(from decoder: Decoder) throws {
|
||||
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
|
||||
id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString
|
||||
accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier)
|
||||
percent = try typeContainer.decode(CGFloat.self, forKey: .percent)
|
||||
if let color = try typeContainer.decodeIfPresent(Color.self, forKey: .color) {
|
||||
self.color = color
|
||||
@ -50,6 +53,7 @@ import Foundation
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(id, forKey: .id)
|
||||
try container.encode(moleculeName, forKey: .moleculeName)
|
||||
try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier)
|
||||
try container.encode(percent, forKey: .percent)
|
||||
try container.encode(color, forKey: .color)
|
||||
try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor)
|
||||
|
||||
@ -14,7 +14,8 @@ open class StarModel: MoleculeModelProtocol {
|
||||
//--------------------------------------------------
|
||||
public static var identifier: String = "star"
|
||||
public var id: String = UUID().uuidString
|
||||
|
||||
public var accessibilityIdentifier: String?
|
||||
|
||||
public var backgroundColor: Color?
|
||||
@Percent public var percent: CGFloat = 0
|
||||
public var borderColor: Color?
|
||||
@ -26,6 +27,7 @@ open class StarModel: MoleculeModelProtocol {
|
||||
//--------------------------------------------------
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case accessibilityIdentifier
|
||||
case moleculeName
|
||||
case backgroundColor
|
||||
case percent
|
||||
@ -47,6 +49,7 @@ open class StarModel: MoleculeModelProtocol {
|
||||
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString
|
||||
accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier)
|
||||
if let percent = try typeContainer.decodeIfPresent(CGFloat.self, forKey: .percent) {
|
||||
self.percent = percent
|
||||
}
|
||||
@ -63,6 +66,7 @@ open class StarModel: MoleculeModelProtocol {
|
||||
try container.encode(id, forKey: .id)
|
||||
try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor)
|
||||
try container.encode(moleculeName, forKey: .moleculeName)
|
||||
try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier)
|
||||
try container.encode(percent, forKey: .percent)
|
||||
try container.encodeIfPresent(borderColor, forKey: .borderColor)
|
||||
try container.encodeIfPresent(fillColor, forKey: .fillColor)
|
||||
|
||||
@ -14,7 +14,8 @@ import MVMCore
|
||||
//--------------------------------------------------
|
||||
public static var identifier: String = "stars"
|
||||
public var id: String = UUID().uuidString
|
||||
|
||||
public var accessibilityIdentifier: String?
|
||||
|
||||
public var backgroundColor: Color?
|
||||
public var starBackgroundColor: Color?
|
||||
public var stars: [StarModel]
|
||||
@ -28,6 +29,7 @@ import MVMCore
|
||||
//--------------------------------------------------
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case accessibilityIdentifier
|
||||
case moleculeName
|
||||
case backgroundColor
|
||||
case starBackgroundColor
|
||||
@ -53,6 +55,7 @@ import MVMCore
|
||||
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString
|
||||
accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier)
|
||||
stars = try typeContainer.decode([StarModel].self, forKey: .stars)
|
||||
backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor)
|
||||
starBackgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .starBackgroundColor)
|
||||
@ -70,6 +73,7 @@ import MVMCore
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(id, forKey: .id)
|
||||
try container.encode(moleculeName, forKey: .moleculeName)
|
||||
try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier)
|
||||
try container.encode(stars, forKey: .stars)
|
||||
try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor)
|
||||
try container.encodeIfPresent(starBackgroundColor, forKey: .starBackgroundColor)
|
||||
|
||||
@ -40,7 +40,9 @@ open class TileContainer: VDS.TileContainer, VDSMoleculeViewProtocol{
|
||||
// MARK: - Public
|
||||
//--------------------------------------------------
|
||||
public func viewModelDidUpdate() {
|
||||
|
||||
if let accessibilityIdentifier = viewModel.accessibilityIdentifier {
|
||||
self.accessibilityIdentifier = accessibilityIdentifier
|
||||
}
|
||||
if let moleculeModel = viewModel.molecule {
|
||||
if let molecule,
|
||||
moleculeModel.moleculeName == molecule.model?.moleculeName {
|
||||
@ -99,7 +101,9 @@ open class TileContainer: VDS.TileContainer, VDSMoleculeViewProtocol{
|
||||
//--------------------------------------------------
|
||||
// MARK: - MVMCoreViewProtocol
|
||||
//--------------------------------------------------
|
||||
open func updateView(_ size: CGFloat) {}
|
||||
open func updateView(_ size: CGFloat) {
|
||||
(molecule as? MVMCoreViewProtocol)?.updateView(size)
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - MoleculeViewProtocol
|
||||
@ -135,8 +139,6 @@ open class TileContainer: VDS.TileContainer, VDSMoleculeViewProtocol{
|
||||
}
|
||||
|
||||
extension TileContainer: MVMCoreUIViewConstrainingProtocol {
|
||||
public func horizontalAlignment() -> UIStackView.Alignment { .leading }
|
||||
|
||||
public func isClippable() -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
@ -67,11 +67,13 @@ open class TileContainerBaseModel<PaddingType: DefaultValuing & Codable, TileCon
|
||||
public var showDropShadow: Bool = false
|
||||
public var padding = PaddingType.defaultValue
|
||||
public var color: TileContainerType.BackgroundColor = .black
|
||||
public var aspectRatio: TileContainerType.AspectRatio = .ratio1x1
|
||||
public var aspectRatio: TileContainerType.AspectRatio = .none
|
||||
public var backgroundEffect: TileContainerType.BackgroundEffect = .none
|
||||
public var surface: Surface { inverted ? .dark : .light }
|
||||
|
||||
public var accessibilityIdentifier: String?
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case accessibilityIdentifier
|
||||
case inverted
|
||||
case backgroundImage
|
||||
case action
|
||||
@ -88,6 +90,7 @@ open class TileContainerBaseModel<PaddingType: DefaultValuing & Codable, TileCon
|
||||
|
||||
required public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
accessibilityIdentifier = try container.decodeIfPresent(String.self, forKey: .accessibilityIdentifier)
|
||||
inverted = try container.decodeIfPresent(Bool.self, forKey: .inverted) ?? false
|
||||
backgroundImage = try container.decodeIfPresent(String.self, forKey: .backgroundImage)
|
||||
action = try container.decodeModelIfPresent(codingKey: .action)
|
||||
@ -98,12 +101,13 @@ open class TileContainerBaseModel<PaddingType: DefaultValuing & Codable, TileCon
|
||||
showDropShadow = try container.decodeIfPresent(Bool.self, forKey: .showDropShadow) ?? false
|
||||
padding = try container.decodeIfPresent(PaddingType.self, forKey: .padding) ?? PaddingType.defaultValue
|
||||
color = try container.decodeIfPresent(TileContainerType.BackgroundColor.self, forKey: .color) ?? .black
|
||||
aspectRatio = try container.decodeIfPresent(TileContainerType.AspectRatio.self, forKey: .aspectRatio) ?? .ratio1x1
|
||||
aspectRatio = try container.decodeIfPresent(TileContainerType.AspectRatio.self, forKey: .aspectRatio) ?? .none
|
||||
backgroundEffect = try container.decodeIfPresent(TileContainerType.BackgroundEffect.self, forKey: .backgroundEffect) ?? .none
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier)
|
||||
try container.encodeIfPresent(backgroundImage, forKey: .backgroundImage)
|
||||
try container.encodeModelIfPresent(action, forKey: .action)
|
||||
try container.encodeIfPresent(imageFallbackColor, forKey: .imageFallbackColor)
|
||||
|
||||
@ -39,6 +39,9 @@ open class Tilelet: VDS.Tilelet, VDSMoleculeViewProtocol{
|
||||
// MARK: - Public
|
||||
//--------------------------------------------------
|
||||
public func viewModelDidUpdate() {
|
||||
if let accessibilityIdentifier = viewModel.accessibilityIdentifier {
|
||||
self.accessibilityIdentifier = accessibilityIdentifier
|
||||
}
|
||||
//tilelet specific properties
|
||||
if let value = viewModel.textWidth {
|
||||
textWidth = .value(value)
|
||||
@ -48,7 +51,7 @@ open class Tilelet: VDS.Tilelet, VDSMoleculeViewProtocol{
|
||||
eyebrowModel = viewModel.eyebrowModel(delegateObject: delegateObject, additionalData: additionalData)
|
||||
titleModel = viewModel.titleModel(delegateObject: delegateObject, additionalData: additionalData)
|
||||
subTitleModel = viewModel.subTitleModel(delegateObject: delegateObject, additionalData: additionalData)
|
||||
badgeModel = viewModel.badge
|
||||
badgeModel = viewModel.badgeModel()
|
||||
descriptiveIconModel = viewModel.descriptiveIcon
|
||||
directionalIconModel = viewModel.directionalIcon
|
||||
//setup action
|
||||
@ -136,10 +139,6 @@ open class Tilelet: VDS.Tilelet, VDSMoleculeViewProtocol{
|
||||
}
|
||||
|
||||
extension Tilelet: MVMCoreUIViewConstrainingProtocol {
|
||||
|
||||
// Investigate later.
|
||||
//public func horizontalAlignment() -> UIStackView.Alignment { .leading }
|
||||
|
||||
public func isClippable() -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
@ -18,7 +18,7 @@ open class TileletModel: TileContainerBaseModel<Tilelet.Padding, Tilelet>, Molec
|
||||
public var id: String = UUID().uuidString
|
||||
public var backgroundColor: Color?
|
||||
|
||||
public var badge: Tilelet.BadgeModel?
|
||||
public var badge: BadgeModel?
|
||||
public var eyebrow: LabelModel?
|
||||
public var eyebrowColor: TitleLockup.TextColor = .primary
|
||||
public var title: LabelModel?
|
||||
@ -45,10 +45,11 @@ open class TileletModel: TileContainerBaseModel<Tilelet.Padding, Tilelet>, Molec
|
||||
case textWidth
|
||||
case textPercentage
|
||||
}
|
||||
|
||||
required public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
id = try container.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString
|
||||
badge = try container.decodeIfPresent(Tilelet.BadgeModel.self, forKey: .badge)
|
||||
badge = try container.decodeIfPresent(BadgeModel.self, forKey: .badge)
|
||||
eyebrow = try container.decodeIfPresent(LabelModel.self, forKey: .eyebrow)
|
||||
title = try container.decodeIfPresent(LabelModel.self, forKey: .title)
|
||||
subTitle = try container.decodeIfPresent(LabelModel.self, forKey: .subTitle)
|
||||
@ -86,36 +87,52 @@ open class TileletModel: TileContainerBaseModel<Tilelet.Padding, Tilelet>, Molec
|
||||
} else {
|
||||
subTitleColor = .primary
|
||||
}
|
||||
|
||||
|
||||
try super.init(from: decoder)
|
||||
}
|
||||
|
||||
public func badgeModel() -> Tilelet.BadgeModel? {
|
||||
guard let badge else { return nil }
|
||||
return .init(text: badge.text,
|
||||
textColor: badge.textColorStyle,
|
||||
fillColor: badge.fillColorStyle,
|
||||
surface: badge.surface,
|
||||
numberOfLines: badge.numberOfLines,
|
||||
maxWidth: badge.maxWidth
|
||||
)
|
||||
}
|
||||
|
||||
public func eyebrowModel(delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) -> Tilelet.EyebrowModel? {
|
||||
guard let eyebrow else { return nil }
|
||||
let attrs = eyebrow.attributes?.toVDSLabelAttributeModel(delegateObject: delegateObject, additionalData: additionalData)
|
||||
var isBold: Bool = true
|
||||
do {
|
||||
if let style = eyebrow.fontStyle {
|
||||
isBold = style.isBold()
|
||||
return .init(text: eyebrow.text,
|
||||
textColor: eyebrowColor,
|
||||
textAttributes: attrs, isBold: style.isBold(),
|
||||
textAttributes: attrs,
|
||||
isBold: isBold,
|
||||
standardStyle: try style.vdsSubsetStyle())
|
||||
}
|
||||
} catch MVMCoreError.errorObject(let object) {
|
||||
MVMCoreLoggingHandler.shared()?.addError(toLog: object)
|
||||
} catch { }
|
||||
|
||||
return .init(text: eyebrow.text, textColor: eyebrowColor, textAttributes: attrs)
|
||||
return .init(text: eyebrow.text, textColor: eyebrowColor, textAttributes: attrs, isBold: isBold)
|
||||
}
|
||||
|
||||
public func titleModel(delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) -> Tilelet.TitleModel? {
|
||||
guard let title else { return nil }
|
||||
let attrs = title.attributes?.toVDSLabelAttributeModel(delegateObject: delegateObject, additionalData: additionalData)
|
||||
|
||||
var isBold: Bool = true
|
||||
do {
|
||||
if let style = title.fontStyle {
|
||||
isBold = style.isBold()
|
||||
return .init(text: title.text,
|
||||
textColor: titleColor,
|
||||
textAttributes: attrs,
|
||||
isBold: isBold,
|
||||
standardStyle: try style.vdsSubsetStyle())
|
||||
}
|
||||
|
||||
@ -123,7 +140,7 @@ open class TileletModel: TileContainerBaseModel<Tilelet.Padding, Tilelet>, Molec
|
||||
MVMCoreLoggingHandler.shared()?.addError(toLog: object)
|
||||
} catch { }
|
||||
|
||||
return .init(text: title.text, textColor: titleColor, textAttributes: attrs)
|
||||
return .init(text: title.text, textColor: titleColor, textAttributes: attrs, isBold: isBold)
|
||||
}
|
||||
|
||||
public func subTitleModel(delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) -> Tilelet.SubTitleModel? {
|
||||
|
||||
@ -28,6 +28,9 @@ open class Tooltip: VDS.Tooltip, VDSMoleculeViewProtocol{
|
||||
// MARK: - Public
|
||||
//--------------------------------------------------
|
||||
public func viewModelDidUpdate() {
|
||||
if let accessibilityIdentifier = viewModel.accessibilityIdentifier {
|
||||
self.accessibilityIdentifier = accessibilityIdentifier
|
||||
}
|
||||
surface = viewModel.surface
|
||||
fillColor = viewModel.fillColor
|
||||
size = viewModel.size
|
||||
|
||||
@ -22,6 +22,8 @@ open class TooltipModel: MoleculeModelProtocol {
|
||||
|
||||
public var backgroundColor: Color?
|
||||
|
||||
public var accessibilityIdentifier: String?
|
||||
|
||||
public var size: VDS.Tooltip.Size = .medium
|
||||
|
||||
public var fillColor: VDS.Tooltip.FillColor = .primary
|
||||
@ -39,6 +41,7 @@ open class TooltipModel: MoleculeModelProtocol {
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case accessibilityIdentifier
|
||||
case moleculeName
|
||||
case backgroundColor
|
||||
case closeButtonText
|
||||
@ -52,13 +55,14 @@ open class TooltipModel: MoleculeModelProtocol {
|
||||
|
||||
required public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
self.id = try container.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString
|
||||
self.backgroundColor = try container.decodeIfPresent(Color.self, forKey: .backgroundColor)
|
||||
id = try container.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString
|
||||
backgroundColor = try container.decodeIfPresent(Color.self, forKey: .backgroundColor)
|
||||
accessibilityIdentifier = try container.decodeIfPresent(String.self, forKey: .accessibilityIdentifier)
|
||||
|
||||
self.title = try container.decodeIfPresent(String.self, forKey: .title)
|
||||
self.content = try container.decodeIfPresent(String.self, forKey: .content)
|
||||
title = try container.decodeIfPresent(String.self, forKey: .title)
|
||||
content = try container.decodeIfPresent(String.self, forKey: .content)
|
||||
|
||||
self.molecule = try container.decodeModelIfPresent(codingKey: .contentView)
|
||||
molecule = try container.decodeModelIfPresent(codingKey: .contentView)
|
||||
|
||||
if let closeButtonText = try container.decodeIfPresent(String.self, forKey: .closeButtonText) {
|
||||
self.closeButtonText = closeButtonText
|
||||
@ -81,6 +85,7 @@ open class TooltipModel: MoleculeModelProtocol {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
try container.encode(id, forKey: .id)
|
||||
try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier)
|
||||
try container.encode(moleculeName, forKey: .moleculeName)
|
||||
try container.encode(surface, forKey: .surface)
|
||||
try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor)
|
||||
@ -96,3 +101,17 @@ open class TooltipModel: MoleculeModelProtocol {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension TooltipModel {
|
||||
public func convertToVDSTooltipModel() -> Tooltip.TooltipModel {
|
||||
var moleculeView: MoleculeViewProtocol?
|
||||
if let molecule, let view = ModelRegistry.createMolecule(molecule) {
|
||||
moleculeView = view
|
||||
}
|
||||
return .init(closeButtonText: closeButtonText,
|
||||
title: title,
|
||||
content: content,
|
||||
contentView: moleculeView
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,6 +11,7 @@ import Foundation
|
||||
open class VideoModel: MoleculeModelProtocol, PageBehaviorProtocolRequirer {
|
||||
public static var identifier = "video"
|
||||
public var id: String = UUID().uuidString
|
||||
public var accessibilityIdentifier: String?
|
||||
public var backgroundColor: Color?
|
||||
public var video: String
|
||||
public var showControls = false
|
||||
@ -45,6 +46,7 @@ open class VideoModel: MoleculeModelProtocol, PageBehaviorProtocolRequirer {
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case accessibilityIdentifier
|
||||
case moleculeName
|
||||
case video
|
||||
case showControls
|
||||
@ -60,6 +62,7 @@ open class VideoModel: MoleculeModelProtocol, PageBehaviorProtocolRequirer {
|
||||
required public init(from decoder: Decoder) throws {
|
||||
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
|
||||
id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString
|
||||
accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier)
|
||||
video = try typeContainer.decode(String.self, forKey:.video)
|
||||
if let showControls = try typeContainer.decodeIfPresent(Bool.self, forKey: .showControls) {
|
||||
self.showControls = showControls
|
||||
@ -76,6 +79,7 @@ open class VideoModel: MoleculeModelProtocol, PageBehaviorProtocolRequirer {
|
||||
open func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(id, forKey: .id)
|
||||
try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier)
|
||||
try container.encode(moleculeName, forKey: .moleculeName)
|
||||
try container.encode(video, forKey: .video)
|
||||
try container.encode(showControls, forKey: .showControls)
|
||||
|
||||
@ -13,7 +13,8 @@ import MVMCore
|
||||
public static var identifier: String = "webview"
|
||||
public var moleculeName: String = WebViewModel.identifier
|
||||
public var id: String = UUID().uuidString
|
||||
|
||||
public var accessibilityIdentifier: String?
|
||||
|
||||
public var backgroundColor: Color?
|
||||
public var url: URL?
|
||||
public var htmlString: String?
|
||||
@ -26,6 +27,7 @@ import MVMCore
|
||||
|
||||
private enum CodingKeys: String, CodingKey{
|
||||
case id
|
||||
case accessibilityIdentifier
|
||||
case moleculeName
|
||||
case backgroundColor
|
||||
case url
|
||||
@ -43,6 +45,7 @@ import MVMCore
|
||||
required public init(from decoder: Decoder) throws {
|
||||
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
|
||||
id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString
|
||||
accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier)
|
||||
backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor)
|
||||
url = try typeContainer.decodeIfPresent(URL.self, forKey: .url)
|
||||
htmlString = try typeContainer.decodeIfPresent(String.self, forKey: .htmlString)
|
||||
@ -57,6 +60,7 @@ import MVMCore
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(id, forKey: .id)
|
||||
try container.encode(moleculeName, forKey: .moleculeName)
|
||||
try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier)
|
||||
try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor)
|
||||
try container.encodeIfPresent(url, forKey: .url)
|
||||
try container.encodeIfPresent(htmlString, forKey: .htmlString)
|
||||
|
||||
@ -8,30 +8,22 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
public enum GraphSize: String, Codable {
|
||||
case small, medium, large
|
||||
}
|
||||
|
||||
public enum GraphStyle: String, Codable {
|
||||
case unlimited, safetyMode
|
||||
}
|
||||
|
||||
public class WheelModel: MoleculeModelProtocol {
|
||||
public class WheelModel: GraphSizeBase, MoleculeModelProtocol {
|
||||
|
||||
public static var identifier: String = "wheel"
|
||||
public var id: String = UUID().uuidString
|
||||
|
||||
public var accessibilityIdentifier: String?
|
||||
|
||||
public var style: GraphStyle = .unlimited {
|
||||
didSet {
|
||||
updateStyle()
|
||||
}
|
||||
}
|
||||
|
||||
public var size: GraphSize = .small {
|
||||
didSet {
|
||||
updateSize()
|
||||
}
|
||||
}
|
||||
public var diameter: CGFloat = 24
|
||||
public var lineWidth: CGFloat = 5
|
||||
public var clockwise: Bool = true
|
||||
@ -39,13 +31,15 @@ public class WheelModel: MoleculeModelProtocol {
|
||||
public var colors = [Color]()
|
||||
public var backgroundColor: Color?
|
||||
|
||||
public init() {
|
||||
public override init() {
|
||||
super.init()
|
||||
updateStyle()
|
||||
updateSize()
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case accessibilityIdentifier
|
||||
case style
|
||||
case size
|
||||
case diameter
|
||||
@ -58,9 +52,11 @@ public class WheelModel: MoleculeModelProtocol {
|
||||
}
|
||||
|
||||
required public init(from decoder: Decoder) throws {
|
||||
super.init()
|
||||
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
|
||||
id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString
|
||||
|
||||
accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier)
|
||||
|
||||
if let style = try typeContainer.decodeIfPresent(GraphStyle.self, forKey: .style) {
|
||||
self.style = style
|
||||
}
|
||||
@ -91,6 +87,7 @@ public class WheelModel: MoleculeModelProtocol {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(id, forKey: .id)
|
||||
try container.encode(moleculeName, forKey: .moleculeName)
|
||||
try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier)
|
||||
try container.encode(style, forKey: .style)
|
||||
try container.encode(size, forKey: .size)
|
||||
try container.encode(diameter, forKey: .diameter)
|
||||
@ -123,7 +120,7 @@ public class WheelModel: MoleculeModelProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
func updateSize() {
|
||||
public override func updateSize() {
|
||||
switch size {
|
||||
case .small:
|
||||
diameter = MFSizeObject(standardSize: 20)?.getValueBasedOnApplicationWidth() ?? 20
|
||||
|
||||
@ -9,13 +9,13 @@
|
||||
import Foundation
|
||||
import VDS
|
||||
import VDSCoreTokens
|
||||
import MVMCore
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Codable Extensions
|
||||
//--------------------------------------------------
|
||||
|
||||
extension VDS.Surface: Codable {}
|
||||
extension VDS.Badge.FillColor: Codable {}
|
||||
extension VDS.BadgeIndicator.FillColor: Codable {}
|
||||
extension VDS.BadgeIndicator.Kind: Codable {}
|
||||
extension VDS.BadgeIndicator.MaximumDigits: Codable {}
|
||||
@ -25,21 +25,31 @@ extension VDS.ButtonIcon.Size: Codable {}
|
||||
extension VDS.ButtonIcon.BadgeIndicatorModel.ExpandDirection: Codable {}
|
||||
extension VDS.ButtonIcon.SurfaceType: Codable {}
|
||||
extension VDS.ButtonGroup.Alignment: Codable {}
|
||||
extension VDS.CarouselScrollbar.Layout: Codable {}
|
||||
extension VDS.DatePicker.DateFormat: Codable {}
|
||||
extension VDS.EntryFieldBase.HelperTextPlacement: Codable {}
|
||||
extension VDS.Icon.Name: Codable {}
|
||||
extension VDS.Icon.Size: Codable {}
|
||||
extension VDS.InputField.CreditCardType: Codable {}
|
||||
extension VDS.InputField.DateFormat: Codable {}
|
||||
extension VDS.InputField.FieldType: Codable {}
|
||||
extension VDS.Line.Style: Codable {}
|
||||
extension VDS.Line.Orientation: Codable {}
|
||||
extension VDS.Tabs.Orientation: Codable {}
|
||||
extension VDS.Tabs.IndicatorPosition: Codable {}
|
||||
extension VDS.Tabs.Overflow: Codable {}
|
||||
extension VDS.Tabs.Size: Codable {}
|
||||
extension VDS.TextArea.Height: Codable {}
|
||||
extension VDS.TextLink.Size: Codable {}
|
||||
extension VDS.TextLinkCaret.IconPosition: Codable {}
|
||||
extension VDS.TileContainerBase.AspectRatio: Codable {}
|
||||
extension VDS.Tilelet.Padding: Codable {}
|
||||
extension VDS.TitleLockup.TextAlignment: Codable {}
|
||||
extension VDS.Toggle.TextSize: Codable {}
|
||||
extension VDS.Toggle.TextPosition: Codable {}
|
||||
extension VDS.Toggle.TextWeight: Codable {}
|
||||
extension VDS.Tooltip.FillColor: Codable {}
|
||||
extension VDS.Tooltip.Size: Codable {}
|
||||
extension VDS.Line.Style: Codable {}
|
||||
extension VDS.Line.Orientation: Codable {}
|
||||
extension VDS.Use: Codable {}
|
||||
|
||||
extension VDS.Button.Size: RawRepresentableCodable {
|
||||
@ -47,6 +57,52 @@ extension VDS.Button.Size: RawRepresentableCodable {
|
||||
public static var defaultValue: VDS.Button.Size? { nil }
|
||||
}
|
||||
|
||||
extension VDS.CalendarBase.CalendarIndicatorModel: Codable, ModelComparisonProtocol, MoleculeModelComparisonProtocol {
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case label
|
||||
case date
|
||||
case dateFormat
|
||||
}
|
||||
|
||||
public init(from decoder: any Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
let label = try container.decode(String.self, forKey: .label)
|
||||
let formatter = Self.formatter()
|
||||
formatter.dateFormat = try container.decodeIfPresent(String.self, forKey: .dateFormat) ?? "MMM d, y"
|
||||
let foundDate = try container.decode(String.self, forKey: .date)
|
||||
let date = formatter.date(from: foundDate)!
|
||||
self = .init(label: label, date: date)
|
||||
}
|
||||
|
||||
public func encode(to encoder: any Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(label, forKey: .label)
|
||||
try container.encode(Self.formatter().string(from: date), forKey: .date)
|
||||
}
|
||||
|
||||
static func formatter() -> DateFormatter {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateStyle = .medium
|
||||
formatter.timeZone = NSTimeZone.system
|
||||
formatter.locale = .current
|
||||
formatter.formatterBehavior = .default
|
||||
return formatter
|
||||
}
|
||||
|
||||
public func isEqual(to model: any ModelComparisonProtocol) -> Bool {
|
||||
guard let model = model as? Self else { return false }
|
||||
return label == model.label
|
||||
&& date == model.date
|
||||
}
|
||||
|
||||
public func isVisuallyEquivalent(to model: any MoleculeModelComparisonProtocol) -> Bool {
|
||||
guard let model = model as? Self else { return false }
|
||||
return label == model.label
|
||||
&& date == model.date
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Decodable Defaults
|
||||
//--------------------------------------------------
|
||||
@ -237,6 +293,45 @@ extension VDS.TitleLockup.TextColor: Codable {
|
||||
}
|
||||
}
|
||||
|
||||
extension VDS.Badge.FillColor: Codable {
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
let type = try container.decode(String.self)
|
||||
switch type {
|
||||
case "red":
|
||||
self = .red
|
||||
case "yellow":
|
||||
self = .yellow
|
||||
case "green":
|
||||
self = .green
|
||||
case "orange":
|
||||
self = .orange
|
||||
case "blue":
|
||||
self = .blue
|
||||
case "black":
|
||||
self = .black
|
||||
case "white":
|
||||
self = .white
|
||||
default:
|
||||
if let color = try? Color(from: decoder) {
|
||||
self = .custom(color.uiColor)
|
||||
} else {
|
||||
self = .custom(UIColor(hexString: type))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
switch self {
|
||||
case .custom(let value):
|
||||
try container.encode(Color(uiColor: value))
|
||||
default:
|
||||
try container.encode(String(reflecting: self))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension VDS.TitleLockup.TitleTextColor: Codable {
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
|
||||
@ -16,7 +16,7 @@ extension Array where Element: MVMCoreUI.LabelAttributeModel {
|
||||
var attributes: [any VDS.LabelAttributeModel] = []
|
||||
forEach { atomicLabelAttribute in
|
||||
if let attr = atomicLabelAttribute as? (any VDSLabelAttributeConvertable),
|
||||
let vds = attr.convertToVDSLabelAttirbute(delegateObject: delegateObject,
|
||||
let vds = attr.convertToVDSLabelAttribute(delegateObject: delegateObject,
|
||||
additionalData: additionalData){
|
||||
attributes.append(vds)
|
||||
}
|
||||
@ -28,12 +28,12 @@ extension Array where Element: MVMCoreUI.LabelAttributeModel {
|
||||
//VDS Convertable Protocol and Extensions
|
||||
public protocol VDSLabelAttributeConvertable<LabelAttributeType> {
|
||||
associatedtype LabelAttributeType: VDS.LabelAttributeModel
|
||||
func convertToVDSLabelAttirbute(delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) -> LabelAttributeType?
|
||||
func convertToVDSLabelAttribute(delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) -> LabelAttributeType?
|
||||
}
|
||||
|
||||
extension LabelAttributeUnderlineModel: VDSLabelAttributeConvertable {
|
||||
|
||||
public func convertToVDSLabelAttirbute(delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) -> UnderlineLabelAttribute? {
|
||||
public func convertToVDSLabelAttribute(delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) -> UnderlineLabelAttribute? {
|
||||
guard let style = UnderlineLabelAttribute.Style(rawValue: style.rawValue) else { return nil }
|
||||
|
||||
var pattern: UnderlineLabelAttribute.Pattern?
|
||||
@ -50,7 +50,7 @@ extension LabelAttributeUnderlineModel: VDSLabelAttributeConvertable {
|
||||
}
|
||||
|
||||
extension LabelAttributeActionModel: VDSLabelAttributeConvertable {
|
||||
public func convertToVDSLabelAttirbute(delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) -> ActionLabelAttribute? {
|
||||
public func convertToVDSLabelAttribute(delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) -> ActionLabelAttribute? {
|
||||
var vdsAttribute = VDS.ActionLabelAttribute(location: location, length: length)
|
||||
vdsAttribute.subscriber = vdsAttribute.action.sink { [weak self] in
|
||||
guard let self else { return }
|
||||
@ -64,11 +64,13 @@ extension LabelAttributeActionModel: VDSLabelAttributeConvertable {
|
||||
}
|
||||
|
||||
extension LabelAttributeFontModel: VDSLabelAttributeConvertable {
|
||||
public func convertToVDSLabelAttirbute(delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) -> TextStyleLabelAttribute? {
|
||||
public func convertToVDSLabelAttribute(delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) -> TextStyleLabelAttribute? {
|
||||
|
||||
var textStyle: TextStyle?
|
||||
if let found = style?.vdsTextStyle() {
|
||||
textStyle = found
|
||||
} else if let name, let size, let font = UIFont(name: name, size: size) {
|
||||
textStyle = TextStyle.convert(font: font)
|
||||
} else if let name, let found = TextStyle(rawValue: name) {
|
||||
textStyle = found
|
||||
}
|
||||
@ -82,7 +84,7 @@ extension LabelAttributeFontModel: VDSLabelAttributeConvertable {
|
||||
}
|
||||
|
||||
extension LabelAttributeColorModel: VDSLabelAttributeConvertable {
|
||||
public func convertToVDSLabelAttirbute(delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) -> ColorLabelAttribute? {
|
||||
public func convertToVDSLabelAttribute(delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) -> ColorLabelAttribute? {
|
||||
guard let textColor else { return nil }
|
||||
return ColorLabelAttribute(location: location,
|
||||
length: length,
|
||||
@ -91,14 +93,14 @@ extension LabelAttributeColorModel: VDSLabelAttributeConvertable {
|
||||
}
|
||||
|
||||
extension LabelAttributeStrikeThroughModel: VDSLabelAttributeConvertable {
|
||||
public func convertToVDSLabelAttirbute(delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) -> StrikeThroughLabelAttribute? {
|
||||
public func convertToVDSLabelAttribute(delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) -> StrikeThroughLabelAttribute? {
|
||||
return StrikeThroughLabelAttribute(location: location,
|
||||
length: length)
|
||||
}
|
||||
}
|
||||
|
||||
extension LabelAttributeImageModel: VDSLabelAttributeConvertable {
|
||||
public func convertToVDSLabelAttirbute(delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) -> AtomicImageLabelAttribute? {
|
||||
public func convertToVDSLabelAttribute(delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) -> AtomicImageLabelAttribute? {
|
||||
var frame: CGRect?
|
||||
if let size {
|
||||
frame = CGRect(x: 0, y: 0, width: size, height: size)
|
||||
|
||||
59
MVMCoreUI/Atomic/Molecules/Breadcrumbs/Breadcrumbs.swift
Normal file
59
MVMCoreUI/Atomic/Molecules/Breadcrumbs/Breadcrumbs.swift
Normal file
@ -0,0 +1,59 @@
|
||||
//
|
||||
// Breadcrumbs.swift
|
||||
// MVMCoreUI
|
||||
//
|
||||
// Created by Matt Bruce on 8/26/24.
|
||||
// Copyright © 2024 Verizon Wireless. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import VDS
|
||||
|
||||
@objcMembers open class Breadcrumbs: VDS.Breadcrumbs, VDSMoleculeViewProtocol {
|
||||
//------------------------------------------------------
|
||||
// MARK: - Properties
|
||||
//------------------------------------------------------
|
||||
open var viewModel: BreadcrumbsModel!
|
||||
open var delegateObject: MVMCoreUIDelegateObject?
|
||||
open var additionalData: [AnyHashable : Any]?
|
||||
|
||||
// Form Validation
|
||||
open var fieldKey: String?
|
||||
open var fieldValue: JSONValue?
|
||||
open var groupName: String?
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Initializers
|
||||
//--------------------------------------------------
|
||||
public convenience required init() {
|
||||
self.init(frame:.zero)
|
||||
}
|
||||
|
||||
open func viewModelDidUpdate() {
|
||||
if let accessibilityIdentifier = viewModel.accessibilityIdentifier {
|
||||
self.accessibilityIdentifier = accessibilityIdentifier
|
||||
}
|
||||
isEnabled = viewModel.enabled
|
||||
surface = viewModel.surface
|
||||
breadcrumbModels = viewModel.breadcrumbs.compactMap { [unowned self] breadcrumb in
|
||||
return .init(text: breadcrumb.text,
|
||||
selected: breadcrumb.selected,
|
||||
onClick: { _ in
|
||||
MVMCoreUIActionHandler.performActionUnstructured(with: breadcrumb.action,
|
||||
sourceModel: breadcrumb,
|
||||
additionalData: self.additionalData,
|
||||
delegateObject: self.delegateObject)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Actions
|
||||
//--------------------------------------------------
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - MoleculeViewProtocol
|
||||
//--------------------------------------------------
|
||||
public func updateView(_ size: CGFloat) {}
|
||||
|
||||
}
|
||||
124
MVMCoreUI/Atomic/Molecules/Breadcrumbs/BreadcrumbsModel.swift
Normal file
124
MVMCoreUI/Atomic/Molecules/Breadcrumbs/BreadcrumbsModel.swift
Normal file
@ -0,0 +1,124 @@
|
||||
//
|
||||
// BreadCrumbs.swift
|
||||
// MVMCoreUI
|
||||
//
|
||||
// Created by Matt Bruce on 8/26/24.
|
||||
// Copyright © 2024 Verizon Wireless. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import VDS
|
||||
import MVMCore
|
||||
|
||||
open class BreadcrumbsModel: MoleculeModelProtocol, ParentMoleculeModelProtocol {
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Properties
|
||||
//--------------------------------------------------
|
||||
open class var identifier: String { "breadcrumbs" }
|
||||
open var moleculeName: String { Self.identifier }
|
||||
open var backgroundColor: Color?
|
||||
open var id: String = UUID().uuidString
|
||||
open var accessibilityIdentifier: String?
|
||||
open var children: [any MoleculeModelProtocol] { breadcrumbs }
|
||||
|
||||
open var breadcrumbs: [BreadcrumbModel] = []
|
||||
open var enabled: Bool = true
|
||||
open var inverted: Bool = false
|
||||
open var surface: Surface { inverted ? .dark : .light }
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Keys
|
||||
//--------------------------------------------------
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case accessibilityIdentifier
|
||||
case moleculeName
|
||||
case breadcrumbs
|
||||
case enabled
|
||||
case inverted
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Initializers
|
||||
//--------------------------------------------------
|
||||
|
||||
required public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
accessibilityIdentifier = try container.decodeIfPresent(String.self, forKey: .accessibilityIdentifier)
|
||||
breadcrumbs = try container.decode([BreadcrumbModel].self, forKey: .breadcrumbs)
|
||||
enabled = try container.decodeIfPresent(Bool.self, forKey: .enabled) ?? false
|
||||
inverted = try container.decodeIfPresent(Bool.self, forKey: .inverted) ?? false
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier)
|
||||
try container.encode(moleculeName, forKey: .moleculeName)
|
||||
try container.encode(breadcrumbs, forKey: .breadcrumbs)
|
||||
try container.encode(enabled, forKey: .enabled)
|
||||
try container.encode(inverted, forKey: .inverted)
|
||||
}
|
||||
|
||||
public func isEqual(to model: any ModelComparisonProtocol) -> Bool {
|
||||
guard let model = model as? Self else { return false }
|
||||
return inverted == model.inverted
|
||||
&& enabled == model.enabled
|
||||
}
|
||||
}
|
||||
|
||||
open class BreadcrumbModel: MoleculeModelProtocol {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Properties
|
||||
//--------------------------------------------------
|
||||
open class var identifier: String { "breadcrumb" }
|
||||
open var moleculeName: String { Self.identifier }
|
||||
open var backgroundColor: Color?
|
||||
open var id: String = UUID().uuidString
|
||||
open var accessibilityIdentifier: String?
|
||||
|
||||
open var text: String = ""
|
||||
open var selected: Bool = false
|
||||
open var action: ActionModelProtocol
|
||||
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Keys
|
||||
//--------------------------------------------------
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case moleculeName
|
||||
case accessibilityIdentifier
|
||||
case text
|
||||
case selected
|
||||
case action
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Initializers
|
||||
//--------------------------------------------------
|
||||
|
||||
required public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
accessibilityIdentifier = try container.decodeIfPresent(String.self, forKey: .accessibilityIdentifier)
|
||||
text = try container.decode(String.self, forKey: .text)
|
||||
selected = try container.decodeIfPresent(Bool.self, forKey: .selected) ?? false
|
||||
action = try container.decodeModel(codingKey: .action)
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier)
|
||||
try container.encode(moleculeName, forKey: .moleculeName)
|
||||
try container.encode(text, forKey: .text)
|
||||
try container.encode(selected, forKey: .selected)
|
||||
try container.encodeModelIfPresent(action, forKey: .action)
|
||||
}
|
||||
|
||||
public func isEqual(to model: any ModelComparisonProtocol) -> Bool {
|
||||
guard let model = model as? Self else { return false }
|
||||
return text == model.text
|
||||
&& selected == model.selected
|
||||
&& action.isEqual(to: model.action)
|
||||
}
|
||||
}
|
||||
@ -52,6 +52,9 @@ public class HeadersH1ButtonModel: HeaderModel, MoleculeModelProtocol, ParentMol
|
||||
if titleLockup.subTitle?.fontStyle == nil {
|
||||
titleLockup.subTitle?.fontStyle = Styler.Font.RegularTitleMedium
|
||||
}
|
||||
if titleLockup.title.accessibilityTraits == nil {
|
||||
titleLockup.title.accessibilityTraits = [.header]
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
|
||||
@ -31,6 +31,9 @@ public class HeadersH1NoButtonsBodyTextModel: HeaderModel, MoleculeModelProtocol
|
||||
if titleLockup.subTitle?.fontStyle == nil {
|
||||
titleLockup.subTitle?.fontStyle = Styler.Font.RegularTitleMedium
|
||||
}
|
||||
if titleLockup.title.accessibilityTraits == nil {
|
||||
titleLockup.title.accessibilityTraits = [.header]
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
|
||||
@ -47,18 +47,15 @@ public class HeadersH2ButtonsModel: HeaderModel, MoleculeModelProtocol, ParentMo
|
||||
//--------------------------------------------------
|
||||
|
||||
public override func setDefaults() {
|
||||
if topPadding == nil {
|
||||
topPadding = Padding.Component.VerticalMarginSpacing
|
||||
}
|
||||
if bottomPadding == nil {
|
||||
bottomPadding = Padding.Component.VerticalMarginSpacing
|
||||
}
|
||||
if titleLockup.title.fontStyle == nil {
|
||||
titleLockup.title.fontStyle = Styler.Font.RegularTitleXLarge
|
||||
}
|
||||
if titleLockup.subTitle?.fontStyle == nil {
|
||||
titleLockup.subTitle?.fontStyle = Styler.Font.RegularTitleMedium
|
||||
}
|
||||
if titleLockup.title.accessibilityTraits == nil {
|
||||
titleLockup.title.accessibilityTraits = [.header]
|
||||
}
|
||||
super.setDefaults()
|
||||
}
|
||||
|
||||
|
||||
@ -42,18 +42,15 @@ public class HeadersH2CaretLinkModel: HeaderModel, MoleculeModelProtocol, Parent
|
||||
// MARK: - Methods
|
||||
//--------------------------------------------------
|
||||
public override func setDefaults() {
|
||||
if topPadding == nil {
|
||||
topPadding = Padding.Component.VerticalMarginSpacing
|
||||
}
|
||||
if bottomPadding == nil {
|
||||
bottomPadding = Padding.Component.VerticalMarginSpacing
|
||||
}
|
||||
if titleLockup.title.fontStyle == nil {
|
||||
titleLockup.title.fontStyle = Styler.Font.RegularTitleXLarge
|
||||
}
|
||||
if titleLockup.subTitle?.fontStyle == nil {
|
||||
titleLockup.subTitle?.fontStyle = Styler.Font.RegularTitleMedium
|
||||
}
|
||||
if titleLockup.title.accessibilityTraits == nil {
|
||||
titleLockup.title.accessibilityTraits = [.header]
|
||||
}
|
||||
super.setDefaults()
|
||||
}
|
||||
|
||||
|
||||
@ -46,18 +46,15 @@ public class HeadersH2LinkModel: HeaderModel, ParentMoleculeModelProtocol {
|
||||
//--------------------------------------------------
|
||||
|
||||
public override func setDefaults() {
|
||||
if topPadding == nil {
|
||||
topPadding = Padding.Component.VerticalMarginSpacing
|
||||
}
|
||||
if bottomPadding == nil {
|
||||
bottomPadding = Padding.Component.VerticalMarginSpacing
|
||||
}
|
||||
if titleLockup.title.fontStyle == nil {
|
||||
if titleLockup.title.fontStyle == nil {
|
||||
titleLockup.title.fontStyle = Styler.Font.RegularTitleXLarge
|
||||
}
|
||||
if titleLockup.subTitle?.fontStyle == nil {
|
||||
titleLockup.subTitle?.fontStyle = Styler.Font.RegularTitleMedium
|
||||
}
|
||||
if titleLockup.title.accessibilityTraits == nil {
|
||||
titleLockup.title.accessibilityTraits = [.header]
|
||||
}
|
||||
super.setDefaults()
|
||||
}
|
||||
|
||||
|
||||
@ -36,18 +36,15 @@ public class HeadersH2NoButtonsBodyTextModel: HeaderModel, MoleculeModelProtocol
|
||||
}
|
||||
|
||||
public override func setDefaults() {
|
||||
if topPadding == nil {
|
||||
topPadding = Padding.Component.VerticalMarginSpacing
|
||||
}
|
||||
if bottomPadding == nil {
|
||||
bottomPadding = Padding.Component.VerticalMarginSpacing
|
||||
}
|
||||
if titleLockup.title.fontStyle == nil {
|
||||
titleLockup.title.fontStyle = Styler.Font.RegularTitleXLarge
|
||||
}
|
||||
if titleLockup.subTitle?.fontStyle == nil {
|
||||
titleLockup.subTitle?.fontStyle = Styler.Font.RegularTitleMedium
|
||||
}
|
||||
if titleLockup.title.accessibilityTraits == nil {
|
||||
titleLockup.title.accessibilityTraits = [.header]
|
||||
}
|
||||
super.setDefaults()
|
||||
}
|
||||
|
||||
|
||||
@ -57,12 +57,6 @@ public class HeadersH2PricingTwoRowsModel: HeaderModel, MoleculeModelProtocol, P
|
||||
// MARK: - Methods
|
||||
//--------------------------------------------------
|
||||
public override func setDefaults() {
|
||||
if topPadding == nil {
|
||||
topPadding = Padding.Component.VerticalMarginSpacing
|
||||
}
|
||||
if bottomPadding == nil {
|
||||
bottomPadding = Padding.Component.VerticalMarginSpacing
|
||||
}
|
||||
if headline.accessibilityTraits == nil {
|
||||
headline.accessibilityTraits = .header
|
||||
}
|
||||
|
||||
@ -47,18 +47,15 @@ public class HeadersH2TinyButtonModel: HeaderModel, MoleculeModelProtocol, Paren
|
||||
//--------------------------------------------------
|
||||
|
||||
public override func setDefaults() {
|
||||
if topPadding == nil {
|
||||
topPadding = Padding.Component.VerticalMarginSpacing
|
||||
}
|
||||
if bottomPadding == nil {
|
||||
bottomPadding = Padding.Component.VerticalMarginSpacing
|
||||
}
|
||||
if titleLockup.title.fontStyle == nil {
|
||||
titleLockup.title.fontStyle = Styler.Font.RegularTitleXLarge
|
||||
}
|
||||
if titleLockup.subTitle?.fontStyle == nil {
|
||||
titleLockup.subTitle?.fontStyle = Styler.Font.RegularTitleMedium
|
||||
}
|
||||
if titleLockup.title.accessibilityTraits == nil {
|
||||
titleLockup.title.accessibilityTraits = [.header]
|
||||
}
|
||||
super.setDefaults()
|
||||
button.style = .secondary
|
||||
button.size = .small
|
||||
|
||||
@ -83,9 +83,7 @@
|
||||
func updateAccessibilityLabel() {
|
||||
|
||||
var message = ""
|
||||
|
||||
checkbox.updateAccessibilityLabel()
|
||||
|
||||
|
||||
if let checkboxLabel = checkbox.accessibilityLabel, !checkboxLabel.isEmpty {
|
||||
message += checkboxLabel + ", "
|
||||
}
|
||||
|
||||
@ -88,9 +88,7 @@
|
||||
func updateAccessibilityLabel() {
|
||||
|
||||
var message = ""
|
||||
|
||||
checkbox.updateAccessibilityLabel()
|
||||
|
||||
|
||||
if let checkboxLabel = checkbox.accessibilityLabel {
|
||||
message += checkboxLabel + ", "
|
||||
}
|
||||
|
||||
@ -81,9 +81,7 @@
|
||||
func updateAccessibilityLabel() {
|
||||
|
||||
var message = ""
|
||||
|
||||
radioButton.updateAccessibilityLabel()
|
||||
|
||||
|
||||
if let radioButtonLabel = radioButton.accessibilityLabel {
|
||||
message += radioButtonLabel + ", "
|
||||
}
|
||||
|
||||
@ -98,9 +98,7 @@ import UIKit
|
||||
func updateAccessibilityLabel() {
|
||||
|
||||
var message = ""
|
||||
|
||||
radioButton.updateAccessibilityLabel()
|
||||
|
||||
|
||||
if let radioButtonLabel = radioButton.accessibilityLabel {
|
||||
message += radioButtonLabel + ", "
|
||||
}
|
||||
|
||||
@ -85,7 +85,6 @@ open class ListLeftVariableRadioButtonBodyText: TableViewCell {
|
||||
|
||||
var message = ""
|
||||
|
||||
radioButton.updateAccessibilityLabel()
|
||||
if let radioButtonLabel = radioButton.accessibilityLabel {
|
||||
message += radioButtonLabel + ", "
|
||||
}
|
||||
|
||||
@ -14,7 +14,8 @@ public class LockUpsPlanNamesModel: MoleculeModelProtocol {
|
||||
//--------------------------------------------------
|
||||
public static var identifier: String = "planNamesLockup"
|
||||
public var id: String = UUID().uuidString
|
||||
|
||||
public var accessibilityIdentifier: String?
|
||||
|
||||
public var backgroundColor: Color?
|
||||
public var headline: LabelModel
|
||||
public var subHeadline: LabelModel
|
||||
@ -35,6 +36,7 @@ public class LockUpsPlanNamesModel: MoleculeModelProtocol {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case moleculeName
|
||||
case accessibilityIdentifier
|
||||
case backgroundColor
|
||||
case headline
|
||||
case subHeadline
|
||||
@ -47,6 +49,7 @@ public class LockUpsPlanNamesModel: MoleculeModelProtocol {
|
||||
required public init(from decoder: Decoder) throws {
|
||||
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
|
||||
id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString
|
||||
accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier)
|
||||
backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor)
|
||||
headline = try typeContainer.decode(LabelModel.self, forKey: .headline)
|
||||
subHeadline = try typeContainer.decode(LabelModel.self, forKey: .subHeadline)
|
||||
@ -57,6 +60,7 @@ public class LockUpsPlanNamesModel: MoleculeModelProtocol {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(id, forKey: .id)
|
||||
try container.encode(moleculeName, forKey: .moleculeName)
|
||||
try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier)
|
||||
try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor)
|
||||
try container.encode(headline, forKey: .headline)
|
||||
try container.encode(subHeadline, forKey: .subHeadline)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user