From 2316ffee542774f8837a0ca474c55e88389e876e Mon Sep 17 00:00:00 2001 From: Ivan Magda Date: Fri, 26 Jul 2024 08:55:58 +0700 Subject: [PATCH 1/2] iOS: Fix failed to show full content length in table problem bottom sheet (#1121) ^ALTAPPS-1294 --- iosHyperskillApp/Podfile | 1 + iosHyperskillApp/Podfile.lock | 6 +- .../project.pbxproj | 52 +-- ...IStackView+RemoveAllArrangedSubviews.swift | 10 + .../StepQuizTable/StepQuizTableViewData.swift | 2 +- ...StepQuizTableSelectColumnsColumnView.swift | 207 ++++++++++++ ...StepQuizTableSelectColumnsHeaderView.swift | 164 ++++++++++ .../StepQuizTableSelectColumnsView.swift | 185 +++++++++++ ...QuizTableSelectColumnsViewController.swift | 77 +++-- ...StepQuizTableSelectColumnsColumnView.swift | 74 ----- .../StepQuizTableSelectColumnsView.swift | 105 ------ .../UIKit/UIKitScrollableStackView.swift | 303 ++++++++++++++++++ .../Views/UIKit/UIKitTapProxyView.swift | 14 + .../UIStackViewExtensionTests.swift | 17 + 14 files changed, 997 insertions(+), 220 deletions(-) create mode 100644 iosHyperskillApp/iosHyperskillApp/Sources/Extensions/UIKit/UIStackView+RemoveAllArrangedSubviews.swift create mode 100644 iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizTableSelectColumns/StepQuizTableSelectColumnsColumnView.swift create mode 100644 iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizTableSelectColumns/StepQuizTableSelectColumnsHeaderView.swift create mode 100644 iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizTableSelectColumns/StepQuizTableSelectColumnsView.swift delete mode 100644 iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizTableSelectColumns/Views/StepQuizTableSelectColumnsColumnView.swift delete mode 100644 iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizTableSelectColumns/Views/StepQuizTableSelectColumnsView.swift create mode 100644 iosHyperskillApp/iosHyperskillApp/Sources/Views/UIKit/UIKitScrollableStackView.swift create mode 100644 iosHyperskillApp/iosHyperskillApp/Sources/Views/UIKit/UIKitTapProxyView.swift create mode 100644 iosHyperskillApp/iosHyperskillAppTests/ExtensionsTests/UIStackViewExtensionTests.swift diff --git a/iosHyperskillApp/Podfile b/iosHyperskillApp/Podfile index 6cdd987662..045cd22969 100644 --- a/iosHyperskillApp/Podfile +++ b/iosHyperskillApp/Podfile @@ -31,6 +31,7 @@ target "iosHyperskillApp" do pod "SVProgressHUD", "2.3.1" pod "SkeletonUI", "1.0.11" pod "lottie-ios", "4.4.3" + pod "BEMCheckBox", "1.4.1" pod "PanModal", :git => "https://github.com/ivan-magda/PanModal.git", :branch => "remove-presenting-appearance-transitions" pod "CombineSchedulers", :git => "https://github.com/ivan-magda/combine-schedulers.git", :branch => "main" diff --git a/iosHyperskillApp/Podfile.lock b/iosHyperskillApp/Podfile.lock index 7b7d71a7e5..9df6b57437 100644 --- a/iosHyperskillApp/Podfile.lock +++ b/iosHyperskillApp/Podfile.lock @@ -12,6 +12,7 @@ PODS: - AppsFlyerFramework/Main (= 6.14.2) - AppsFlyerFramework/Main (6.14.2) - Atributika (4.10.1) + - BEMCheckBox (1.4.1) - CocoaLumberjack (3.8.2): - CocoaLumberjack/Core (= 3.8.2) - CocoaLumberjack/Core (3.8.2) @@ -116,6 +117,7 @@ DEPENDENCIES: - AmplitudeSwift (= 1.4.5) - AppsFlyerFramework (= 6.14.2) - Atributika (= 4.10.1) + - BEMCheckBox (= 1.4.1) - CombineSchedulers (from `https://github.com/ivan-magda/combine-schedulers.git`, branch `main`) - Firebase/CoreOnly (= 10.24.0) - Firebase/Messaging (= 10.24.0) @@ -143,6 +145,7 @@ SPEC REPOS: - AppAuth - AppsFlyerFramework - Atributika + - BEMCheckBox - CocoaLumberjack - Firebase - FirebaseCore @@ -212,6 +215,7 @@ SPEC CHECKSUMS: AppAuth: 501c04eda8a8d11f179dbe8637b7a91bb7e5d2fa AppsFlyerFramework: b3de9a49c6af8a8e38c44603e468b5e207f22466 Atributika: 47e778507cfb3cd2c996278b0285221a62e97d71 + BEMCheckBox: 5ba6e37ade3d3657b36caecc35c8b75c6c2b1a4e CocoaLumberjack: f8d89a516e7710fdb2e9b8f1560b16ec6040eef0 CombineSchedulers: 80f670c732b4754eb011cd1147d9a08654b1c463 Firebase: 91fefd38712feb9186ea8996af6cbdef41473442 @@ -245,6 +249,6 @@ SPEC CHECKSUMS: SVProgressHUD: 4837c74bdfe2e51e8821c397825996a8d7de6e22 SwiftLint: c1de071d9d08c8aba837545f6254315bc900e211 -PODFILE CHECKSUM: 9392d71b37a5fe79e30faedcb4e232aa00f3bc67 +PODFILE CHECKSUM: 1177593fe82412f60dfb7e58578b56066fe42f4f COCOAPODS: 1.15.2 diff --git a/iosHyperskillApp/iosHyperskillApp.xcodeproj/project.pbxproj b/iosHyperskillApp/iosHyperskillApp.xcodeproj/project.pbxproj index 87cfd53c8d..008e05b4fb 100644 --- a/iosHyperskillApp/iosHyperskillApp.xcodeproj/project.pbxproj +++ b/iosHyperskillApp/iosHyperskillApp.xcodeproj/project.pbxproj @@ -33,6 +33,14 @@ 2C05AC5F2A0ED9710039C7EF /* BadgeView+ConcreateTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C05AC5E2A0ED9710039C7EF /* BadgeView+ConcreateTypes.swift */; }; 2C05AC622A0EDFC20039C7EF /* ProjectSelectionListGridCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C05AC612A0EDFC20039C7EF /* ProjectSelectionListGridCellView.swift */; }; 2C05AC642A0EDFD80039C7EF /* ProjectSelectionListGridCellBadgesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C05AC632A0EDFD80039C7EF /* ProjectSelectionListGridCellBadgesView.swift */; }; + 2C065AA22C52347E00B820F5 /* StepQuizTableSelectColumnsColumnView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C065AA12C52347E00B820F5 /* StepQuizTableSelectColumnsColumnView.swift */; }; + 2C065AA42C523EE900B820F5 /* StepQuizTableSelectColumnsHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C065AA32C523EE900B820F5 /* StepQuizTableSelectColumnsHeaderView.swift */; }; + 2C065AA62C5240E900B820F5 /* StepQuizTableSelectColumnsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C065AA52C5240E900B820F5 /* StepQuizTableSelectColumnsView.swift */; }; + 2C065AAA2C5242AE00B820F5 /* StepQuizTableSelectColumnsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C065AA92C5242AE00B820F5 /* StepQuizTableSelectColumnsViewController.swift */; }; + 2C065AAC2C524BB800B820F5 /* UIKitScrollableStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C065AAB2C524BB800B820F5 /* UIKitScrollableStackView.swift */; }; + 2C065AAE2C524C7600B820F5 /* UIStackView+RemoveAllArrangedSubviews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C065AAD2C524C7600B820F5 /* UIStackView+RemoveAllArrangedSubviews.swift */; }; + 2C065AB02C524CDA00B820F5 /* UIStackViewExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C065AAF2C524CDA00B820F5 /* UIStackViewExtensionTests.swift */; }; + 2C065AB22C524E4100B820F5 /* UIKitTapProxyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C065AB12C524E4100B820F5 /* UIKitTapProxyView.swift */; }; 2C069EB128F03782009A3DA1 /* AnalyticExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C069EB028F03782009A3DA1 /* AnalyticExtensions.swift */; }; 2C078CE52AE26CB400D97E24 /* FillBlanksQuizTitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C078CE42AE26CB400D97E24 /* FillBlanksQuizTitleView.swift */; }; 2C078CE72AE26E2000D97E24 /* UIKitSeparatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C078CE62AE26E2000D97E24 /* UIKitSeparatorView.swift */; }; @@ -315,9 +323,6 @@ 2C8E4F982848961C0011ADFA /* UIViewController+PresentPanModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C8E4F972848961C0011ADFA /* UIViewController+PresentPanModal.swift */; }; 2C8E4F9A284897360011ADFA /* PanModalSwiftUIViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C8E4F99284897360011ADFA /* PanModalSwiftUIViewController.swift */; }; 2C8E4F9C2848A1550011ADFA /* PanModalViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C8E4F9B2848A1550011ADFA /* PanModalViewModifier.swift */; }; - 2C8E4FA12848BF1F0011ADFA /* StepQuizTableSelectColumnsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C8E4FA02848BF1F0011ADFA /* StepQuizTableSelectColumnsViewController.swift */; }; - 2C8E4FB12848C9050011ADFA /* StepQuizTableSelectColumnsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C8E4FB02848C9050011ADFA /* StepQuizTableSelectColumnsView.swift */; }; - 2C8E4FB42848CB980011ADFA /* StepQuizTableSelectColumnsColumnView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C8E4FB32848CB980011ADFA /* StepQuizTableSelectColumnsColumnView.swift */; }; 2C8E4FB628490C020011ADFA /* PanModalPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C8E4FB528490C020011ADFA /* PanModalPresenter.swift */; }; 2C8E66D52878771B00D3928D /* ProfilePresentationDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C8E66D42878771B00D3928D /* ProfilePresentationDescription.swift */; }; 2C8E66D7287877F500D3928D /* ProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C8E66D6287877F500D3928D /* ProfileViewModel.swift */; }; @@ -815,6 +820,14 @@ 2C05AC5E2A0ED9710039C7EF /* BadgeView+ConcreateTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BadgeView+ConcreateTypes.swift"; sourceTree = ""; }; 2C05AC612A0EDFC20039C7EF /* ProjectSelectionListGridCellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProjectSelectionListGridCellView.swift; sourceTree = ""; }; 2C05AC632A0EDFD80039C7EF /* ProjectSelectionListGridCellBadgesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProjectSelectionListGridCellBadgesView.swift; sourceTree = ""; }; + 2C065AA12C52347E00B820F5 /* StepQuizTableSelectColumnsColumnView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepQuizTableSelectColumnsColumnView.swift; sourceTree = ""; }; + 2C065AA32C523EE900B820F5 /* StepQuizTableSelectColumnsHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepQuizTableSelectColumnsHeaderView.swift; sourceTree = ""; }; + 2C065AA52C5240E900B820F5 /* StepQuizTableSelectColumnsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepQuizTableSelectColumnsView.swift; sourceTree = ""; }; + 2C065AA92C5242AE00B820F5 /* StepQuizTableSelectColumnsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepQuizTableSelectColumnsViewController.swift; sourceTree = ""; }; + 2C065AAB2C524BB800B820F5 /* UIKitScrollableStackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIKitScrollableStackView.swift; sourceTree = ""; }; + 2C065AAD2C524C7600B820F5 /* UIStackView+RemoveAllArrangedSubviews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIStackView+RemoveAllArrangedSubviews.swift"; sourceTree = ""; }; + 2C065AAF2C524CDA00B820F5 /* UIStackViewExtensionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIStackViewExtensionTests.swift; sourceTree = ""; }; + 2C065AB12C524E4100B820F5 /* UIKitTapProxyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIKitTapProxyView.swift; sourceTree = ""; }; 2C069EB028F03782009A3DA1 /* AnalyticExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticExtensions.swift; sourceTree = ""; }; 2C078CE42AE26CB400D97E24 /* FillBlanksQuizTitleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FillBlanksQuizTitleView.swift; sourceTree = ""; }; 2C078CE62AE26E2000D97E24 /* UIKitSeparatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIKitSeparatorView.swift; sourceTree = ""; }; @@ -1105,9 +1118,6 @@ 2C8E4F972848961C0011ADFA /* UIViewController+PresentPanModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+PresentPanModal.swift"; sourceTree = ""; }; 2C8E4F99284897360011ADFA /* PanModalSwiftUIViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PanModalSwiftUIViewController.swift; sourceTree = ""; }; 2C8E4F9B2848A1550011ADFA /* PanModalViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PanModalViewModifier.swift; sourceTree = ""; }; - 2C8E4FA02848BF1F0011ADFA /* StepQuizTableSelectColumnsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepQuizTableSelectColumnsViewController.swift; sourceTree = ""; }; - 2C8E4FB02848C9050011ADFA /* StepQuizTableSelectColumnsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepQuizTableSelectColumnsView.swift; sourceTree = ""; }; - 2C8E4FB32848CB980011ADFA /* StepQuizTableSelectColumnsColumnView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepQuizTableSelectColumnsColumnView.swift; sourceTree = ""; }; 2C8E4FB528490C020011ADFA /* PanModalPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PanModalPresenter.swift; sourceTree = ""; }; 2C8E66D42878771B00D3928D /* ProfilePresentationDescription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePresentationDescription.swift; sourceTree = ""; }; 2C8E66D6287877F500D3928D /* ProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewModel.swift; sourceTree = ""; }; @@ -1956,6 +1966,7 @@ 2C1F587C280D2F6600372A37 /* ExtensionsTests */ = { isa = PBXGroup; children = ( + 2C065AAF2C524CDA00B820F5 /* UIStackViewExtensionTests.swift */, 2C1F587D280D2F9200372A37 /* URLExtensionsTests.swift */, ); path = ExtensionsTests; @@ -2913,21 +2924,14 @@ 2C8E4F9D2848BE420011ADFA /* StepQuizTableSelectColumns */ = { isa = PBXGroup; children = ( - 2C8E4FA02848BF1F0011ADFA /* StepQuizTableSelectColumnsViewController.swift */, - 2C8E4FB22848CB6A0011ADFA /* Views */, + 2C065AA12C52347E00B820F5 /* StepQuizTableSelectColumnsColumnView.swift */, + 2C065AA32C523EE900B820F5 /* StepQuizTableSelectColumnsHeaderView.swift */, + 2C065AA52C5240E900B820F5 /* StepQuizTableSelectColumnsView.swift */, + 2C065AA92C5242AE00B820F5 /* StepQuizTableSelectColumnsViewController.swift */, ); path = StepQuizTableSelectColumns; sourceTree = ""; }; - 2C8E4FB22848CB6A0011ADFA /* Views */ = { - isa = PBXGroup; - children = ( - 2C8E4FB32848CB980011ADFA /* StepQuizTableSelectColumnsColumnView.swift */, - 2C8E4FB02848C9050011ADFA /* StepQuizTableSelectColumnsView.swift */, - ); - path = Views; - sourceTree = ""; - }; 2C8EBE522A4D475200A77205 /* Project */ = { isa = PBXGroup; children = ( @@ -3195,6 +3199,7 @@ 2C2CCB482B74FA6600D1E596 /* UIFont+PreferredFont.swift */, 2C7CB6812ADFDB45006F78DA /* UIFont+SizeOfString.swift */, 2CDF14D728EF1E080060D972 /* UINavigationControllerExtensions.swift */, + 2C065AAD2C524C7600B820F5 /* UIStackView+RemoveAllArrangedSubviews.swift */, 2C5B2A22286596400097B270 /* UITableView+RegisterReusable.swift */, 2CC78D0D28C75A3D0006EF91 /* UIViewControllerExtensions.swift */, 2CA7B89328932EB100A789EF /* UIWindowExtensions.swift */, @@ -3516,7 +3521,9 @@ 2CBD1918291D399500F5FB0B /* UIKitBounceButton.swift */, 2CE1E188292CCB450041FE14 /* UIKitIntrospectionView.swift */, 2CBD191C291D3BF400F5FB0B /* UIKitRoundedRectangleButton.swift */, + 2C065AAB2C524BB800B820F5 /* UIKitScrollableStackView.swift */, 2C078CE62AE26E2000D97E24 /* UIKitSeparatorView.swift */, + 2C065AB12C524E4100B820F5 /* UIKitTapProxyView.swift */, 2C5F19162AE6857F0039414D /* CollectionViewLayouts */, ); path = UIKit; @@ -4908,6 +4915,7 @@ 2CB2BDB12BEB7F65009E2D83 /* CodePlaygroundGetChangesSubstringTests.swift in Sources */, 2CB2BDB52BEB81A1009E2D83 /* CodePlaygroundShouldMakeTabLineAfterTests.swift in Sources */, 2C919E3727EF00950022A2F2 /* QueueTests.swift in Sources */, + 2C065AB02C524CDA00B820F5 /* UIStackViewExtensionTests.swift in Sources */, 2CB2BDB92BEB934D009E2D83 /* CodePlaygroundShouldMakeTabLineAfterPerformanceTests.swift in Sources */, 2CB2BDB72BEB8488009E2D83 /* CodePlaygroundGetChangesSubstringPerformanceTests.swift in Sources */, 2CF87DA929B71D500092FF83 /* IntrospectViewControllerTests.swift in Sources */, @@ -4936,6 +4944,7 @@ E9F27D782906447A007F16D7 /* StepQuizHintCardView.swift in Sources */, E9F0A2AA29D417C800C4A61E /* StudyPlanSectionHeaderView.swift in Sources */, 2C8EBE542A4D476700A77205 /* ProgressScreenProjectProgressContentView.swift in Sources */, + 2C065AAA2C5242AE00B820F5 /* StepQuizTableSelectColumnsViewController.swift in Sources */, E9FB89A72893CD2C0011EFFB /* LocalNotificationsService.swift in Sources */, E9CF48A92A38499F00938CCE /* StreakRecoveryModalViewController.swift in Sources */, 2CC78D0928C74E7D0006EF91 /* UIViewControllerEventsWrapper.swift in Sources */, @@ -5009,6 +5018,7 @@ 2C725B612809125700A49043 /* LayoutInsets.swift in Sources */, 2CEB50CE288AACEA0044F9AB /* StepQuizCodeFullScreenTab.swift in Sources */, 2CEB50C8288A94050044F9AB /* BlockExtensions.swift in Sources */, + 2C065AAE2C524C7600B820F5 /* UIStackView+RemoveAllArrangedSubviews.swift in Sources */, 2C54E4282A1F717F003406B9 /* CardView.swift in Sources */, 2C5837A62B284E570096B89B /* NavigationLink+Empty.swift in Sources */, E9D2D675284E0B30000757AC /* StepQuizMatchingView.swift in Sources */, @@ -5087,7 +5097,6 @@ 2CD20ED12B73475400FB5269 /* ApplicationShortcutsService.swift in Sources */, 2C5F4A5A2971C71200677530 /* GamificationToolbarContent.swift in Sources */, 2C5B2A1F286595AF0097B270 /* CodeCompletionTableViewController.swift in Sources */, - 2C8E4FB12848C9050011ADFA /* StepQuizTableSelectColumnsView.swift in Sources */, 2CE0F6EE2BB40B760032C439 /* StepFeedbackViewModel.swift in Sources */, 2CCCA3A12862E62F00D98089 /* StepQuizStringViewData.swift in Sources */, 2C1061A2285C349400EBD614 /* StepQuizChildQuizAssembly.swift in Sources */, @@ -5134,7 +5143,6 @@ 2C8247AF2BB6BAAF00CDD668 /* StepQuizCodeFixCodeMistakesBadge.swift in Sources */, 2CEB50D0288AADA40044F9AB /* StepQuizCodeFullScreenDetailsView.swift in Sources */, E9A1DA702ACFF86B006A9D4B /* FirstProblemOnboardingFeatureViewStateKsExtensions.swift in Sources */, - 2C8E4FB42848CB980011ADFA /* StepQuizTableSelectColumnsColumnView.swift in Sources */, 2C83FBC22B1781FA007AD7E2 /* LeaderboardListRowView.swift in Sources */, 2CEB33772949930B00B9E437 /* StepQuizSQLSkeletonView.swift in Sources */, 2CB45762288EC29D007C2D77 /* StepQuizActionButtons.swift in Sources */, @@ -5168,6 +5176,7 @@ 2C5B2A1D286595960097B270 /* CodeCompletionTableViewCell.swift in Sources */, 2C27C77E28773042006A641A /* NukeManager.swift in Sources */, 2C0DB91028645332001EA35E /* CodeEditorTheme.swift in Sources */, + 2C065AA62C5240E900B820F5 /* StepQuizTableSelectColumnsView.swift in Sources */, 2CBD1919291D399500F5FB0B /* UIKitBounceButton.swift in Sources */, 2CAA3C6D2AA9CA9D004F6CE6 /* StepQuizProblemOnboardingModalViewController.swift in Sources */, E9523BF029DA933C0013A661 /* StudyPlanViewModel.swift in Sources */, @@ -5227,6 +5236,7 @@ E996D414292228A700A47498 /* TopicsRepetitionsView.swift in Sources */, E94BB0462A9DEF7C00736B7C /* StepQuizParsonsControlsView.swift in Sources */, 2C5CBBE72948FC7A00113007 /* StepQuizSQLView.swift in Sources */, + 2C065AAC2C524BB800B820F5 /* UIKitScrollableStackView.swift in Sources */, 2C54E4262A1F7086003406B9 /* TrackSelectionDetailsDescriptionView.swift in Sources */, 2CEA454F2BD6B75C0011838D /* StepQuizToolbarContent.swift in Sources */, 2C9AA4052C2556F400F5170E /* WelcomeOnboardingOutputProtocol.swift in Sources */, @@ -5238,6 +5248,7 @@ 2C7CB66B2ADFB947006F78DA /* StepQuizFillBlanksAssembly.swift in Sources */, 2CBC97CA2A5553330078E445 /* StageImplementStageCompletedModalViewController.swift in Sources */, 2C20FBC9284F6F97006D879E /* UnitConverters.swift in Sources */, + 2C065AB22C524E4100B820F5 /* UIKitTapProxyView.swift in Sources */, 2C336D152865C49D00C91342 /* ApplicationThemeService.swift in Sources */, 2C3796122877001700C197E2 /* ProfileHeaderView.swift in Sources */, 2C725B5E28090D1F00A49043 /* View+Border.swift in Sources */, @@ -5264,7 +5275,6 @@ 2C58DE2B2803DEE2002A2774 /* VerticalCenteredScrollView.swift in Sources */, 2CD48D892858657100CFCC4A /* StepQuizView.swift in Sources */, 2CD4148729A8D92000ACA855 /* CodeInputPasteControl.swift in Sources */, - 2C8E4FA12848BF1F0011ADFA /* StepQuizTableSelectColumnsViewController.swift in Sources */, 2C078CE72AE26E2000D97E24 /* UIKitSeparatorView.swift in Sources */, 2C677D022C4A3F860019AF03 /* StepQuizCodeBlanksSuggestionsView.swift in Sources */, 2C1F5877280D2B4800372A37 /* ApplicationInfo.swift in Sources */, @@ -5290,6 +5300,7 @@ 2CAE8D0528055D8300E6C83D /* StepTheoryActionButton.swift in Sources */, 2C45E7BD2A0FD9D600DFF32D /* ProjectSelectionListGridCellProjectLevelView.swift in Sources */, 2C37960F2876F36F00C197E2 /* ProfileViewData.swift in Sources */, + 2C065AA22C52347E00B820F5 /* StepQuizTableSelectColumnsColumnView.swift in Sources */, 2C2600852A1FF8B000BD3D39 /* AppPowerModeObserver.swift in Sources */, 2C2D4930281151EB00753F16 /* AuthCredentialsViewModel.swift in Sources */, E94BC944291E8BF5000B18D3 /* TopicsRepetitionsInfoBlock.swift in Sources */, @@ -5441,6 +5452,7 @@ 2C80D503288C5EBB00B2CD1E /* StepQuizCodeNavigationState.swift in Sources */, 2C32374D2837F7190062CAF6 /* Images.swift in Sources */, 2C23C00A2879EB1E0083709F /* StreakViewBuilder.swift in Sources */, + 2C065AA42C523EE900B820F5 /* StepQuizTableSelectColumnsHeaderView.swift in Sources */, 2C1F588B280D8C8600372A37 /* GoogleSocialAuthSDKProvider.swift in Sources */, 2C84E7082C47BA11002EE787 /* StepQuizCodeBlanksViewModel.swift in Sources */, E99B21832887E996006A6154 /* StepQuizChoiceSkeletonView.swift in Sources */, diff --git a/iosHyperskillApp/iosHyperskillApp/Sources/Extensions/UIKit/UIStackView+RemoveAllArrangedSubviews.swift b/iosHyperskillApp/iosHyperskillApp/Sources/Extensions/UIKit/UIStackView+RemoveAllArrangedSubviews.swift new file mode 100644 index 0000000000..20e87a385e --- /dev/null +++ b/iosHyperskillApp/iosHyperskillApp/Sources/Extensions/UIKit/UIStackView+RemoveAllArrangedSubviews.swift @@ -0,0 +1,10 @@ +import UIKit + +extension UIStackView { + func removeAllArrangedSubviews() { + for subview in arrangedSubviews { + removeArrangedSubview(subview) + subview.removeFromSuperview() + } + } +} diff --git a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizTable/StepQuizTableViewData.swift b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizTable/StepQuizTableViewData.swift index f15c75914f..09ec83becf 100644 --- a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizTable/StepQuizTableViewData.swift +++ b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizTable/StepQuizTableViewData.swift @@ -16,7 +16,7 @@ struct StepQuizTableViewData { } } - struct Column: Identifiable { + struct Column: Identifiable, Equatable { let id: Int let text: String diff --git a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizTableSelectColumns/StepQuizTableSelectColumnsColumnView.swift b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizTableSelectColumns/StepQuizTableSelectColumnsColumnView.swift new file mode 100644 index 0000000000..a3e048a46c --- /dev/null +++ b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizTableSelectColumns/StepQuizTableSelectColumnsColumnView.swift @@ -0,0 +1,207 @@ +import BEMCheckBox +import SnapKit +import UIKit + +extension StepQuizTableSelectColumnsColumnView { + struct Appearance { + var checkBoxBoxType: BEMBoxType = .circle + let checkBoxLineWidth: CGFloat = 2 + let checkBoxAnimationDuration: CGFloat = 0.5 + let checkBoxTintColor = ColorPalette.primary + let checkBoxOnCheckColor = UIColor.white + let checkBoxOnFillColor = ColorPalette.primary + let checkBoxOnTintColor = ColorPalette.primary + let checkBoxWidthHeight: CGFloat = 20 + let checkBoxInsets = LayoutInsets(leading: 16) + + let titleFont = UIFont.preferredFont(forTextStyle: .body) + let titleTextColor = UIColor.primaryText + let titleInsets = LayoutInsets.default + + let contentViewMinHeight: CGFloat = 44 + + let backgroundColor = UIColor.clear + } +} + +final class StepQuizTableSelectColumnsColumnView: UIControl { + let appearance: Appearance + + private lazy var checkBox: BEMCheckBox = { + let checkBox = BEMCheckBox() + checkBox.lineWidth = appearance.checkBoxLineWidth + checkBox.hideBox = false + checkBox.boxType = appearance.checkBoxBoxType + checkBox.tintColor = appearance.checkBoxTintColor + checkBox.onCheckColor = appearance.checkBoxOnCheckColor + checkBox.onFillColor = appearance.checkBoxOnFillColor + checkBox.onTintColor = appearance.checkBoxOnTintColor + checkBox.animationDuration = appearance.checkBoxAnimationDuration + checkBox.onAnimationType = .fill + checkBox.offAnimationType = .fill + return checkBox + }() + + private lazy var titleProcessedContentView: ProcessedContentView = { + let processedContentViewAppearance = ProcessedContentView.Appearance( + labelFont: appearance.titleFont, + labelTextColor: appearance.titleTextColor, + backgroundColor: .clear + ) + + let contentProcessor = ContentProcessor( + injections: ContentProcessor.defaultInjections + [ + FontInjection(font: appearance.titleFont), + TextColorInjection(dynamicColor: appearance.titleTextColor) + ] + ) + + let processedContentView = ProcessedContentView( + frame: .zero, + appearance: processedContentViewAppearance, + contentProcessor: contentProcessor, + htmlToAttributedStringConverter: HTMLToAttributedStringConverter(font: appearance.titleFont) + ) + processedContentView.delegate = self + + return processedContentView + }() + + private lazy var contentView = UIView() + + private lazy var tapProxyView = UIKitTapProxyView(targetView: self) + + var isOn: Bool { checkBox.on } + + var onValueChanged: ((Bool) -> Void)? + + var onContentLoad: (() -> Void)? + + override var isHighlighted: Bool { + didSet { + titleProcessedContentView.alpha = isHighlighted ? 0.5 : 1.0 + } + } + + override var intrinsicContentSize: CGSize { + let titleProcessedContentViewIntrinsicContentSize = titleProcessedContentView.intrinsicContentSize + let titleProcessedContentViewHeightWithInsets = titleProcessedContentViewIntrinsicContentSize.height + + appearance.titleInsets.top + + appearance.titleInsets.bottom + + let height = max(appearance.contentViewMinHeight, titleProcessedContentViewHeightWithInsets) + + return CGSize(width: UIView.noIntrinsicMetric, height: height) + } + + init( + frame: CGRect = .zero, + appearance: Appearance = Appearance() + ) { + self.appearance = appearance + super.init(frame: frame) + + self.setupView() + self.addSubviews() + self.makeConstraints() + } + + @available(*, unavailable) + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + invalidateIntrinsicContentSize() + } + + func setOn(_ isOn: Bool, animated: Bool) { + checkBox.setOn(isOn, animated: animated) + } + + func setTitle(_ title: String) { + titleProcessedContentView.setText(title) + } + + @objc + private func clicked() { + let newValue = !checkBox.on + onValueChanged?(newValue) + } +} + +extension StepQuizTableSelectColumnsColumnView: ProgrammaticallyInitializableViewProtocol { + func setupView() { + backgroundColor = appearance.backgroundColor + contentView.backgroundColor = appearance.backgroundColor + + addTarget(self, action: #selector(clicked), for: .touchUpInside) + } + + func addSubviews() { + addSubview(contentView) + contentView.addSubview(checkBox) + contentView.addSubview(titleProcessedContentView) + + addSubview(tapProxyView) + } + + func makeConstraints() { + contentView.translatesAutoresizingMaskIntoConstraints = false + contentView.snp.makeConstraints { make in + make.edges.equalToSuperview() + make.height.greaterThanOrEqualTo(appearance.contentViewMinHeight) + } + + checkBox.translatesAutoresizingMaskIntoConstraints = false + checkBox.snp.makeConstraints { make in + make.leading.equalToSuperview().offset(appearance.checkBoxInsets.leading) + make.centerY.equalToSuperview() + make.width.height.equalTo(appearance.checkBoxWidthHeight) + } + + titleProcessedContentView.translatesAutoresizingMaskIntoConstraints = false + titleProcessedContentView.snp.makeConstraints { make in + make.top.greaterThanOrEqualToSuperview().offset(appearance.titleInsets.top) + make.leading.equalTo(checkBox.snp.trailing).offset(appearance.titleInsets.leading) + make.bottom.lessThanOrEqualToSuperview().offset(-appearance.titleInsets.bottom) + make.trailing.equalToSuperview().offset(-appearance.titleInsets.trailing) + make.centerY.equalToSuperview() + } + + tapProxyView.translatesAutoresizingMaskIntoConstraints = false + tapProxyView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + } +} + +extension StepQuizTableSelectColumnsColumnView: ProcessedContentViewDelegate { + func processedContentViewDidLoadContent(_ view: ProcessedContentView) { + invalidateIntrinsicContentSize() + onContentLoad?() + } + + func processedContentView(_ view: ProcessedContentView, didReportNewHeight height: Int) { + invalidateIntrinsicContentSize() + } + + func processedContentView(_ view: ProcessedContentView, didOpenImageURL url: URL) {} + + func processedContentView(_ view: ProcessedContentView, didOpenLink url: URL) {} +} + +#if DEBUG +@available(iOS 17.0, *) +#Preview { + let view = StepQuizTableSelectColumnsColumnView() + view.setTitle("test") + + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + view.setOn(true, animated: true) + } + + return view +} +#endif diff --git a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizTableSelectColumns/StepQuizTableSelectColumnsHeaderView.swift b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizTableSelectColumns/StepQuizTableSelectColumnsHeaderView.swift new file mode 100644 index 0000000000..52abef5703 --- /dev/null +++ b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizTableSelectColumns/StepQuizTableSelectColumnsHeaderView.swift @@ -0,0 +1,164 @@ +import SnapKit +import UIKit + +extension StepQuizTableSelectColumnsHeaderView { + struct Appearance { + let promptFont = UIFont.preferredFont(forTextStyle: .caption1) + let promptTextColor = UIColor.secondaryText + + let titleFont = UIFont.preferredFont(forTextStyle: .body) + let titleTextColor = UIColor.primaryText + + let contentStackViewSpacing: CGFloat = LayoutInsets.defaultInset + let contentStackViewInsets = LayoutInsets.default.uiEdgeInsets + + let backgroundColor = UIColor.clear + } +} + +final class StepQuizTableSelectColumnsHeaderView: UIView { + let appearance: Appearance + + private lazy var promptLabel: UILabel = { + let label = UILabel() + label.font = appearance.promptFont + label.textColor = appearance.promptTextColor + label.numberOfLines = 1 + label.textAlignment = .center + return label + }() + + private lazy var titleProcessedContentView: ProcessedContentView = { + let processedContentViewAppearance = ProcessedContentView.Appearance( + labelFont: appearance.titleFont, + labelTextColor: appearance.titleTextColor, + backgroundColor: .clear + ) + + let contentProcessor = ContentProcessor( + injections: ContentProcessor.defaultInjections + [ + FontInjection(font: appearance.titleFont), + TextColorInjection(dynamicColor: appearance.titleTextColor) + ] + ) + + let processedContentView = ProcessedContentView( + frame: .zero, + appearance: processedContentViewAppearance, + contentProcessor: contentProcessor, + htmlToAttributedStringConverter: HTMLToAttributedStringConverter(font: appearance.titleFont) + ) + processedContentView.delegate = self + + return processedContentView + }() + + private lazy var contentStackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .vertical + stackView.spacing = appearance.contentStackViewSpacing + return stackView + }() + + private lazy var separatorView = UIKitSeparatorView() + + var prompt: String? { + didSet { + promptLabel.text = prompt + promptLabel.isHidden = prompt?.isEmpty ?? true + } + } + + var title: String? { + didSet { + titleProcessedContentView.setText(title) + } + } + + override var intrinsicContentSize: CGSize { + let contentStackViewIntrinsicContentSize = contentStackView + .systemLayoutSizeFitting(UIView.layoutFittingCompressedSize) + let contentStackViewHeightWithInsets = contentStackViewIntrinsicContentSize.height + + appearance.contentStackViewInsets.top + + appearance.contentStackViewInsets.bottom + + let height = contentStackViewHeightWithInsets.rounded(.up) + + return CGSize(width: UIView.noIntrinsicMetric, height: height) + } + + var onContentLoad: (() -> Void)? + + init( + frame: CGRect = .zero, + appearance: Appearance = Appearance() + ) { + self.appearance = appearance + super.init(frame: frame) + + self.setupView() + self.addSubviews() + self.makeConstraints() + } + + @available(*, unavailable) + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + invalidateIntrinsicContentSize() + } +} + +extension StepQuizTableSelectColumnsHeaderView: ProgrammaticallyInitializableViewProtocol { + func setupView() { + backgroundColor = appearance.backgroundColor + } + + func addSubviews() { + addSubview(contentStackView) + contentStackView.addArrangedSubview(promptLabel) + contentStackView.addArrangedSubview(titleProcessedContentView) + + addSubview(separatorView) + } + + func makeConstraints() { + contentStackView.translatesAutoresizingMaskIntoConstraints = false + contentStackView.snp.makeConstraints { make in + make.edges.equalToSuperview().inset(appearance.contentStackViewInsets) + } + + separatorView.translatesAutoresizingMaskIntoConstraints = false + separatorView.snp.makeConstraints { make in + make.leading.bottom.trailing.equalToSuperview() + } + } +} + +extension StepQuizTableSelectColumnsHeaderView: ProcessedContentViewDelegate { + func processedContentViewDidLoadContent(_ view: ProcessedContentView) { + invalidateIntrinsicContentSize() + onContentLoad?() + } + + func processedContentView(_ view: ProcessedContentView, didReportNewHeight height: Int) { + invalidateIntrinsicContentSize() + } + + func processedContentView(_ view: ProcessedContentView, didOpenImageURL url: URL) {} + + func processedContentView(_ view: ProcessedContentView, didOpenLink url: URL) {} +} + +#if DEBUG +@available(iOS 17.0, *) +#Preview { + let view = StepQuizTableSelectColumnsHeaderView() + view.prompt = Strings.StepQuizTable.multipleChoicePrompt + view.title = "Title goes here" + return view +} +#endif diff --git a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizTableSelectColumns/StepQuizTableSelectColumnsView.swift b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizTableSelectColumns/StepQuizTableSelectColumnsView.swift new file mode 100644 index 0000000000..b58c8ac8ce --- /dev/null +++ b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizTableSelectColumns/StepQuizTableSelectColumnsView.swift @@ -0,0 +1,185 @@ +import SnapKit +import UIKit + +protocol StepQuizTableSelectColumnsViewDelegate: AnyObject { + func tableQuizSelectColumnsView( + _ view: StepQuizTableSelectColumnsView, + didSelectColumn column: StepQuizTableViewData.Column, + isOn: Bool + ) + func tableQuizSelectColumnsViewDidTapConfirm(_ view: StepQuizTableSelectColumnsView) + func tableQuizSelectColumnsViewDidLoadContent(_ view: StepQuizTableSelectColumnsView) +} + +extension StepQuizTableSelectColumnsView { + struct Appearance { + let backgroundColor = UIColor.systemBackground + + let confirmButtonInsets = LayoutInsets.default.uiEdgeInsets + } +} + +final class StepQuizTableSelectColumnsView: UIView { + let appearance: Appearance + + weak var delegate: StepQuizTableSelectColumnsViewDelegate? + + private lazy var headerView = StepQuizTableSelectColumnsHeaderView() + + private lazy var columnsStackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .vertical + return stackView + }() + + private lazy var confirmButton: UIButton = { + let button = UIKitRoundedRectangleButton(style: .violet) + button.addTarget(self, action: #selector(confirmButtonTapped), for: .touchUpInside) + button.setTitle(Strings.StepQuizTable.confirmButton, for: .normal) + return button + }() + private lazy var confirmButtonContainerView = UIView() + + private lazy var scrollableContentStackView = UIKitScrollableStackView(orientation: .vertical) + + private var loadGroup: DispatchGroup? + + private var columns = [StepQuizTableViewData.Column]() + private var selectedColumnsIDs = Set() + + var prompt: String? { + didSet { + headerView.prompt = prompt + } + } + + var isMultipleChoice = false + + init( + frame: CGRect = .zero, + appearance: Appearance = Appearance() + ) { + self.appearance = appearance + super.init(frame: frame) + + self.setupView() + self.addSubviews() + self.makeConstraints() + } + + @available(*, unavailable) + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func set( + title: String, + columns: [StepQuizTableViewData.Column], + selectedColumnsIDs: Set + ) { + loadGroup = DispatchGroup() + let enterCount = columns.count + 1 // title + columns + for _ in 0..) { + self.selectedColumnsIDs = selectedColumnsIDs + + for arrangedSubview in columnsStackView.arrangedSubviews { + guard let columnView = arrangedSubview as? StepQuizTableSelectColumnsColumnView else { + continue + } + + let id = columnView.tag + let isOn = selectedColumnsIDs.contains(id) + + let animated = columnView.isOn != isOn + + columnView.setOn(isOn, animated: animated) + } + } + + @objc + private func confirmButtonTapped() { + delegate?.tableQuizSelectColumnsViewDidTapConfirm(self) + } +} + +extension StepQuizTableSelectColumnsView: ProgrammaticallyInitializableViewProtocol { + func setupView() { + backgroundColor = appearance.backgroundColor + } + + func addSubviews() { + addSubview(scrollableContentStackView) + + scrollableContentStackView.addArrangedView(headerView) + scrollableContentStackView.addArrangedView(columnsStackView) + scrollableContentStackView.addArrangedView(confirmButtonContainerView) + + confirmButtonContainerView.addSubview(confirmButton) + } + + func makeConstraints() { + scrollableContentStackView.translatesAutoresizingMaskIntoConstraints = false + scrollableContentStackView.snp.makeConstraints { make in + make.top.bottom.equalToSuperview() + make.leading.trailing.equalTo(safeAreaLayoutGuide) + } + + confirmButton.translatesAutoresizingMaskIntoConstraints = false + confirmButton.snp.makeConstraints { make in + make.edges.equalToSuperview().inset(appearance.confirmButtonInsets) + } + } +} + +extension StepQuizTableSelectColumnsView: PanModalScrollable { + var panScrollable: UIScrollView? { scrollableContentStackView.panScrollable } +} diff --git a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizTableSelectColumns/StepQuizTableSelectColumnsViewController.swift b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizTableSelectColumns/StepQuizTableSelectColumnsViewController.swift index 44ca4ba8cc..9cc9e3389e 100644 --- a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizTableSelectColumns/StepQuizTableSelectColumnsViewController.swift +++ b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizTableSelectColumns/StepQuizTableSelectColumnsViewController.swift @@ -1,5 +1,4 @@ import PanModal -import SwiftUI import UIKit extension StepQuizTableSelectColumnsViewController { @@ -8,12 +7,19 @@ extension StepQuizTableSelectColumnsViewController { } } -final class StepQuizTableSelectColumnsViewController: PanModalSwiftUIViewController { +final class StepQuizTableSelectColumnsViewController: PanModalPresentableViewController { + private let rowTitle: String private let columns: [StepQuizTableViewData.Column] private var selectedColumnsIDs: Set private let isMultipleChoice: Bool private let onColumnsSelected: (Set) -> Void + var tableQuizSelectColumnsView: StepQuizTableSelectColumnsView? { self.view as? StepQuizTableSelectColumnsView } + + override var panScrollable: UIScrollView? { tableQuizSelectColumnsView?.panScrollable } + + override var shortFormHeight: PanModalHeight { longFormHeight } + init( title: String, columns: [StepQuizTableViewData.Column], @@ -21,43 +27,76 @@ final class StepQuizTableSelectColumnsViewController: PanModalSwiftUIViewControl isMultipleChoice: Bool, onColumnsSelected: @escaping (Set) -> Void ) { + self.rowTitle = title self.columns = columns self.selectedColumnsIDs = selectedColumnsIDs self.isMultipleChoice = isMultipleChoice self.onColumnsSelected = onColumnsSelected + super.init() + } + + override func loadView() { + let view = StepQuizTableSelectColumnsView(frame: UIScreen.main.bounds) + self.view = view + view.delegate = self + } + + override func viewDidLoad() { + super.viewDidLoad() + let prompt = isMultipleChoice ? Strings.StepQuizTable.multipleChoicePrompt : Strings.StepQuizTable.singleChoicePrompt + tableQuizSelectColumnsView?.prompt = prompt + tableQuizSelectColumnsView?.isMultipleChoice = isMultipleChoice - var view = StepQuizTableSelectColumnsView( - prompt: prompt, - title: title, + tableQuizSelectColumnsView?.set( + title: rowTitle, columns: columns, - selectedColumnsIDs: selectedColumnsIDs, - isMultipleChoice: isMultipleChoice + selectedColumnsIDs: selectedColumnsIDs ) - super.init( - isPresented: .constant(false), - content: { view } - ) - - view.onColumnsChanged = self.handleColumnsChanged(_:) - view.onConfirmTapped = self.finishColumnsSelection + panModalSetNeedsLayoutUpdate() } +} + +extension StepQuizTableSelectColumnsViewController: StepQuizTableSelectColumnsViewDelegate { + func tableQuizSelectColumnsView( + _ view: StepQuizTableSelectColumnsView, + didSelectColumn column: StepQuizTableViewData.Column, + isOn: Bool + ) { + if isMultipleChoice { + if isOn { + selectedColumnsIDs.insert(column.id) + } else { + selectedColumnsIDs.remove(column.id) + } + } else { + assert(selectedColumnsIDs.count <= 1, "Sigle choice") + selectedColumnsIDs.removeAll() - // MARK: Private API + if isOn { + selectedColumnsIDs.insert(column.id) + } + } - private func handleColumnsChanged(_ newColumns: Set) { - self.selectedColumnsIDs = newColumns + tableQuizSelectColumnsView?.update(selectedColumnsIDs: selectedColumnsIDs) } - private func finishColumnsSelection() { - self.onColumnsSelected(self.selectedColumnsIDs) + func tableQuizSelectColumnsViewDidTapConfirm(_ view: StepQuizTableSelectColumnsView) { + onColumnsSelected(selectedColumnsIDs) DispatchQueue.main.asyncAfter(deadline: .now() + Animation.dismissAnimationDelay) { self.dismiss(animated: true) } } + + func tableQuizSelectColumnsViewDidLoadContent(_ view: StepQuizTableSelectColumnsView) { + DispatchQueue.main.async { + self.panModalSetNeedsLayoutUpdate() + self.panModalTransition(to: .longForm) + } + } } diff --git a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizTableSelectColumns/Views/StepQuizTableSelectColumnsColumnView.swift b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizTableSelectColumns/Views/StepQuizTableSelectColumnsColumnView.swift deleted file mode 100644 index ce296b8953..0000000000 --- a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizTableSelectColumns/Views/StepQuizTableSelectColumnsColumnView.swift +++ /dev/null @@ -1,74 +0,0 @@ -import SwiftUI - -extension StepQuizTableSelectColumnsColumnView { - struct Appearance { - let interItemSpacing = LayoutInsets.smallInset - - let checkboxIndicatorWidthHeight: CGFloat = 18 - let radioIndicatorWidthHeight: CGFloat = 20 - } -} - -struct StepQuizTableSelectColumnsColumnView: View { - private(set) var appearance = Appearance() - - let isSelected: Bool - - let text: String - - var isMultipleChoice: Bool - - var onTap: () -> Void - - var body: some View { - Button(action: onTap) { - HStack(spacing: appearance.interItemSpacing) { - buildIndicator(isSelected: isSelected, onTap: onTap) - - LatexView( - text: text, - configuration: .quizContent() - ) - } - } - } - - @ViewBuilder - private func buildIndicator(isSelected: Bool, onTap: @escaping () -> Void) -> some View { - if isMultipleChoice { - CheckboxButton( - appearance: .init(backgroundUnselectedColor: .clear), - isSelected: isSelected, - onClick: onTap - ) - .frame(widthHeight: appearance.checkboxIndicatorWidthHeight) - } else { - RadioButton( - appearance: .init(indicatorUnselectedColor: .clear, backgroundColor: .clear), - isSelected: isSelected, - onClick: onTap - ) - .frame(widthHeight: appearance.radioIndicatorWidthHeight) - } - } -} - -#if DEBUG -#Preview { - VStack { - StepQuizTableSelectColumnsColumnView( - isSelected: true, - text: "Some option", - isMultipleChoice: false, - onTap: {} - ) - StepQuizTableSelectColumnsColumnView( - isSelected: true, - text: "Some option", - isMultipleChoice: true, - onTap: {} - ) - } - .padding() -} -#endif diff --git a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizTableSelectColumns/Views/StepQuizTableSelectColumnsView.swift b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizTableSelectColumns/Views/StepQuizTableSelectColumnsView.swift deleted file mode 100644 index daac6c1973..0000000000 --- a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizTableSelectColumns/Views/StepQuizTableSelectColumnsView.swift +++ /dev/null @@ -1,105 +0,0 @@ -import SwiftUI - -struct StepQuizTableSelectColumnsView: View { - let prompt: String - let title: String - - let columns: [StepQuizTableViewData.Column] - @State private(set) var selectedColumnsIDs: Set - - let isMultipleChoice: Bool - - var onColumnsChanged: ((Set) -> Void)? - var onConfirmTapped: (() -> Void)? - - var body: some View { - VStack(spacing: LayoutInsets.defaultInset) { - Text(prompt) - .font(.caption) - .foregroundColor(.primaryText) - - VStack(alignment: .leading, spacing: 0) { - LatexView( - text: title, - configuration: .quizContent() - ) - .padding(.bottom, LayoutInsets.smallInset) - - VStack(alignment: .leading, spacing: 0) { - ForEach(columns) { column in - StepQuizTableSelectColumnsColumnView( - isSelected: selectedColumnsIDs.contains(column.id), - text: column.text, - isMultipleChoice: isMultipleChoice, - onTap: { - handleColumnTapped(column: column) - } - ) - .padding(.vertical, LayoutInsets.smallInset) - } - } - .padding(.bottom, LayoutInsets.smallInset) - - Button(Strings.StepQuizTable.confirmButton, action: { onConfirmTapped?() }) - .buttonStyle(RoundedRectangleButtonStyle(style: .violet)) - .padding(.vertical) - - Spacer() - } - } - .padding() - .ignoresSafeArea() - } - - private func handleColumnTapped(column: StepQuizTableViewData.Column) { - let isContains = selectedColumnsIDs.contains(column.id) - - if isMultipleChoice { - if isContains { - selectedColumnsIDs.remove(column.id) - } else { - selectedColumnsIDs.insert(column.id) - } - } else { - assert(selectedColumnsIDs.count <= 1, "Sigle choice") - selectedColumnsIDs.removeAll() - - if !isContains { - selectedColumnsIDs.insert(column.id) - } - } - - onColumnsChanged?(selectedColumnsIDs) - } -} - -struct StepQuizTableSelectColumnsView_Previews: PreviewProvider { - static var previews: some View { - Group { - StepQuizTableSelectColumnsView( - prompt: "Choose from the table", - title: "Variant A", - columns: [ - .init(text: "1"), - .init(text: "2"), - .init(text: "3") - ], - selectedColumnsIDs: ["2".hashValue], - isMultipleChoice: false, - onColumnsChanged: { _ in }, - onConfirmTapped: {} - ) - - StepQuizTableSelectColumnsView( - prompt: "Choose one or multiple options", - title: "Variant A", - columns: [.init(text: "1"), .init(text: "2"), .init(text: "3")], - selectedColumnsIDs: ["1".hashValue, "2".hashValue], - isMultipleChoice: true, - onColumnsChanged: { _ in }, - onConfirmTapped: {} - ) - } - .previewLayout(.sizeThatFits) - } -} diff --git a/iosHyperskillApp/iosHyperskillApp/Sources/Views/UIKit/UIKitScrollableStackView.swift b/iosHyperskillApp/iosHyperskillApp/Sources/Views/UIKit/UIKitScrollableStackView.swift new file mode 100644 index 0000000000..59572c18a5 --- /dev/null +++ b/iosHyperskillApp/iosHyperskillApp/Sources/Views/UIKit/UIKitScrollableStackView.swift @@ -0,0 +1,303 @@ +import SnapKit +import UIKit + +protocol UIKitScrollableStackViewDelegate: AnyObject { + func scrollableStackViewRefreshControlDidRefresh(_ scrollableStackView: UIKitScrollableStackView) +} + +final class UIKitScrollableStackView: UIView { + private let orientation: Orientation + + weak var delegate: UIKitScrollableStackViewDelegate? + + private lazy var stackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = orientation.stackViewOrientation + return stackView + }() + + private lazy var scrollView = UIScrollView() + + // MARK: - Refresh control + + var isRefreshControlEnabled = false { + didSet { + guard oldValue != isRefreshControlEnabled else { + return + } + + let refreshControl = isRefreshControlEnabled ? UIRefreshControl() : nil + if let refreshControl { + refreshControl.addTarget( + self, + action: #selector(onRefreshControlValueChanged), + for: .valueChanged + ) + } + + scrollView.refreshControl = refreshControl + } + } + + private var refreshControl: UIRefreshControl? { + scrollView.subviews.first(where: { $0 is UIRefreshControl }) as? UIRefreshControl + } + + // MARK: - Blocks + + var arrangedSubviews: [UIView] { + stackView.arrangedSubviews + } + + // MARK: - Proxy properties + + var showsHorizontalScrollIndicator: Bool { + get { + scrollView.showsHorizontalScrollIndicator + } + set { + scrollView.showsHorizontalScrollIndicator = newValue + } + } + + var showsVerticalScrollIndicator: Bool { + get { + scrollView.showsVerticalScrollIndicator + } + set { + scrollView.showsVerticalScrollIndicator = newValue + } + } + + var spacing: CGFloat { + get { + stackView.spacing + } + set { + stackView.spacing = newValue + } + } + + var contentInsetAdjustmentBehavior: UIScrollView.ContentInsetAdjustmentBehavior { + get { + scrollView.contentInsetAdjustmentBehavior + } + set { + scrollView.contentInsetAdjustmentBehavior = newValue + } + } + + var scrollDelegate: UIScrollViewDelegate? { + get { + scrollView.delegate + } + set { + scrollView.delegate = newValue + } + } + + var contentInsets: UIEdgeInsets { + get { + scrollView.contentInset + } + set { + scrollView.contentInset = newValue + } + } + + var contentOffset: CGPoint { + get { + scrollView.contentOffset + } + set { + scrollView.contentOffset = newValue + } + } + + var verticalScrollIndicatorInsets: UIEdgeInsets { + get { + scrollView.verticalScrollIndicatorInsets + } + set { + scrollView.verticalScrollIndicatorInsets = newValue + } + } + + var horizontalScrollIndicatorInsets: UIEdgeInsets { + get { + scrollView.horizontalScrollIndicatorInsets + } + set { + scrollView.horizontalScrollIndicatorInsets = newValue + } + } + + var automaticallyAdjustsScrollIndicatorInsets: Bool { + get { + scrollView.automaticallyAdjustsScrollIndicatorInsets + } + set { + scrollView.automaticallyAdjustsScrollIndicatorInsets = newValue + } + } + + var shouldBounce: Bool { + get { + scrollView.bounces + } + set { + scrollView.bounces = newValue + } + } + + var isPagingEnabled: Bool { + get { + scrollView.isPagingEnabled + } + set { + scrollView.isPagingEnabled = newValue + } + } + + var isScrollEnabled: Bool { + get { + scrollView.isScrollEnabled + } + set { + scrollView.isScrollEnabled = newValue + } + } + + var contentSize: CGSize { + get { + scrollView.contentSize + } + set { + scrollView.contentSize = newValue + } + } + + // MARK: - Inits + + init(frame: CGRect = .zero, orientation: Orientation) { + self.orientation = orientation + super.init(frame: frame) + + self.setupView() + self.addSubviews() + self.makeConstraints() + } + + @available(*, unavailable) + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: Public interface + + func addArrangedView(_ view: UIView) { + stackView.addArrangedSubview(view) + } + + func removeArrangedView(_ view: UIView) { + for subview in stackView.subviews where subview == view { + stackView.removeArrangedSubview(subview) + subview.removeFromSuperview() + } + } + + func insertArrangedView(_ view: UIView, at index: Int) { + stackView.insertArrangedSubview(view, at: index) + } + + func removeAllArrangedViews() { + for subview in stackView.subviews { + removeArrangedView(subview) + } + } + + func startRefreshing() { + refreshControl?.beginRefreshing() + } + + func endRefreshing() { + refreshControl?.endRefreshing() + } + + func scrollTo(arrangedViewIndex: Int) { + guard let targetFrame = arrangedSubviews[safe: arrangedViewIndex]?.frame else { + return + } + + scrollView.scrollRectToVisible(targetFrame, animated: true) + } + + // MARK: - Private methods + + @objc + private func onRefreshControlValueChanged() { + delegate?.scrollableStackViewRefreshControlDidRefresh(self) + } + + enum Orientation { + case vertical + case horizontal + + var stackViewOrientation: NSLayoutConstraint.Axis { + switch self { + case .vertical: + NSLayoutConstraint.Axis.vertical + case .horizontal: + NSLayoutConstraint.Axis.horizontal + } + } + } +} + +extension UIKitScrollableStackView: ProgrammaticallyInitializableViewProtocol { + func setupView() { + stackView.clipsToBounds = false + scrollView.clipsToBounds = false + + // For pull-to-refresh when contentSize is too small for scrolling + if orientation == .horizontal { + scrollView.alwaysBounceHorizontal = true + } else { + scrollView.alwaysBounceVertical = true + } + scrollView.bounces = true + } + + func addSubviews() { + addSubview(scrollView) + scrollView.addSubview(stackView) + } + + func makeConstraints() { + scrollView.translatesAutoresizingMaskIntoConstraints = false + scrollView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + + stackView.translatesAutoresizingMaskIntoConstraints = false + stackView.snp.makeConstraints { make in + make.edges.equalToSuperview() + + if case .vertical = orientation { + make.width.equalTo(scrollView.snp.width) + } else { + make.height.equalTo(scrollView.snp.height) + } + } + } +} + +// MARK: - PanModalScrollable - + +protocol PanModalScrollable: AnyObject { + var panScrollable: UIScrollView? { get } +} + +extension UIKitScrollableStackView: PanModalScrollable { + var panScrollable: UIScrollView? { scrollView } +} diff --git a/iosHyperskillApp/iosHyperskillApp/Sources/Views/UIKit/UIKitTapProxyView.swift b/iosHyperskillApp/iosHyperskillApp/Sources/Views/UIKit/UIKitTapProxyView.swift new file mode 100644 index 0000000000..d4e865cfab --- /dev/null +++ b/iosHyperskillApp/iosHyperskillApp/Sources/Views/UIKit/UIKitTapProxyView.swift @@ -0,0 +1,14 @@ +import UIKit + +class UIKitTapProxyView: UIView { + var targetView: UIView? + + convenience init(targetView: UIView) { + self.init() + self.targetView = targetView + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + bounds.contains(point) ? targetView : nil + } +} diff --git a/iosHyperskillApp/iosHyperskillAppTests/ExtensionsTests/UIStackViewExtensionTests.swift b/iosHyperskillApp/iosHyperskillAppTests/ExtensionsTests/UIStackViewExtensionTests.swift new file mode 100644 index 0000000000..dd0f1d5589 --- /dev/null +++ b/iosHyperskillApp/iosHyperskillAppTests/ExtensionsTests/UIStackViewExtensionTests.swift @@ -0,0 +1,17 @@ +import XCTest + +@testable import iosHyperskillApp + +final class UIStackViewExtensionTests: XCTestCase { + func testRemoveAllArrangedSubviews() { + let stackView = UIStackView() + let view1 = UIView() + stackView.addArrangedSubview(view1) + let view2 = UIView() + stackView.addArrangedSubview(view2) + + stackView.removeAllArrangedSubviews() + + XCTAssertTrue(stackView.arrangedSubviews.isEmpty) + } +} From 1b9e0c497e33cc06fff6ec3d77e8c9389ea9d098 Mon Sep 17 00:00:00 2001 From: Ivan Magda Date: Fri, 26 Jul 2024 09:21:09 +0700 Subject: [PATCH 2/2] iOS: Improve table problem with one option flow (#1124) ^ALTAPPS-1288 --- .../StepQuizTableSelectColumnsView.swift | 6 +++++- ...QuizTableSelectColumnsViewController.swift | 21 +++++++++++++------ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizTableSelectColumns/StepQuizTableSelectColumnsView.swift b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizTableSelectColumns/StepQuizTableSelectColumnsView.swift index b58c8ac8ce..fc7fdb0533 100644 --- a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizTableSelectColumns/StepQuizTableSelectColumnsView.swift +++ b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizTableSelectColumns/StepQuizTableSelectColumnsView.swift @@ -53,7 +53,11 @@ final class StepQuizTableSelectColumnsView: UIView { } } - var isMultipleChoice = false + var isMultipleChoice = false { + didSet { + confirmButtonContainerView.isHidden = !isMultipleChoice + } + } init( frame: CGRect = .zero, diff --git a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizTableSelectColumns/StepQuizTableSelectColumnsViewController.swift b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizTableSelectColumns/StepQuizTableSelectColumnsViewController.swift index 9cc9e3389e..c7c6e31949 100644 --- a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizTableSelectColumns/StepQuizTableSelectColumnsViewController.swift +++ b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizTableSelectColumns/StepQuizTableSelectColumnsViewController.swift @@ -3,7 +3,8 @@ import UIKit extension StepQuizTableSelectColumnsViewController { enum Animation { - static let dismissAnimationDelay: TimeInterval = 0.33 + static let dismissAnimationDelayAfterDidTapConfirm: TimeInterval = 0.33 + static let dismissAnimationDelayAfterChoiceSelected: TimeInterval = 0.5 } } @@ -83,14 +84,14 @@ extension StepQuizTableSelectColumnsViewController: StepQuizTableSelectColumnsVi } tableQuizSelectColumnsView?.update(selectedColumnsIDs: selectedColumnsIDs) + + if !isMultipleChoice { + confirmSelection(delay: Animation.dismissAnimationDelayAfterChoiceSelected) + } } func tableQuizSelectColumnsViewDidTapConfirm(_ view: StepQuizTableSelectColumnsView) { - onColumnsSelected(selectedColumnsIDs) - - DispatchQueue.main.asyncAfter(deadline: .now() + Animation.dismissAnimationDelay) { - self.dismiss(animated: true) - } + confirmSelection(delay: Animation.dismissAnimationDelayAfterDidTapConfirm) } func tableQuizSelectColumnsViewDidLoadContent(_ view: StepQuizTableSelectColumnsView) { @@ -99,4 +100,12 @@ extension StepQuizTableSelectColumnsViewController: StepQuizTableSelectColumnsVi self.panModalTransition(to: .longForm) } } + + private func confirmSelection(delay: TimeInterval) { + onColumnsSelected(selectedColumnsIDs) + + DispatchQueue.main.asyncAfter(deadline: .now() + delay) { + self.dismiss(animated: true) + } + } }