diff --git a/Stepic.xcodeproj/project.pbxproj b/Stepic.xcodeproj/project.pbxproj index b864e3a717..b1bb4709bc 100644 --- a/Stepic.xcodeproj/project.pbxproj +++ b/Stepic.xcodeproj/project.pbxproj @@ -231,7 +231,6 @@ 089877AD214047EE0065DFA2 /* Numbers+Random.swift in Sources */ = {isa = PBXBuildFile; fileRef = 089877A8214047EC0065DFA2 /* Numbers+Random.swift */; }; 089877B0214047EE0065DFA2 /* UserDefaults+StorageServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 089877AB214047ED0065DFA2 /* UserDefaults+StorageServiceProtocol.swift */; }; 089877B221404CF10065DFA2 /* StringStorageServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 089877B121404CF00065DFA2 /* StringStorageServiceProtocol.swift */; }; - 089A0DA71BE9FFCE004AF4EB /* UIViewExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 089A0DA61BE9FFCE004AF4EB /* UIViewExtensions.swift */; }; 08A1256F1BDE8E460066B2B2 /* Sorter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08A1256E1BDE8E460066B2B2 /* Sorter.swift */; }; 08A125791BDEBCC90066B2B2 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 08A1257B1BDEBCC90066B2B2 /* Localizable.strings */; }; 08A3A9CE1BD5A14D0032C36E /* NSDateExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08A3A9CD1BD5A14D0032C36E /* NSDateExtensions.swift */; }; @@ -594,7 +593,10 @@ 2C4436B321356D960084489C /* CourseSubscriber.swift in Sources */ = {isa = PBXBuildFile; fileRef = 088FD8161FB242B3008A2953 /* CourseSubscriber.swift */; }; 2C453398204D46E90061342A /* PinsMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C453397204D46E90061342A /* PinsMap.swift */; }; 2C45339A204D5DEE0061342A /* PinsMapSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C453399204D5DEE0061342A /* PinsMapSpec.swift */; }; + 2C456C3425D2C47100435A86 /* ContinueCourseEmptyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C456C3325D2C47100435A86 /* ContinueCourseEmptyView.swift */; }; + 2C456C3E25D2C52F00435A86 /* ContinueCourseBackgroundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C456C3D25D2C52F00435A86 /* ContinueCourseBackgroundView.swift */; }; 2C45C6EF254160A100AF24BF /* HTMLExtractorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C45C6EE254160A100AF24BF /* HTMLExtractorTests.swift */; }; + 2C45E4FD2774808B001AAF09 /* UIViewExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CFA489F25D1828C00BECD32 /* UIViewExtensions.swift */; }; 2C461FED276A488E00AAAE20 /* IAPSettingsStorageManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C461FEC276A488E00AAAE20 /* IAPSettingsStorageManager.swift */; }; 2C47914626BBD17400920ED2 /* InstructionType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C47914526BBD17400920ED2 /* InstructionType.swift */; }; 2C47A164206284B1003E87EC /* NotificationRequestAlertContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C47A163206284B1003E87EC /* NotificationRequestAlertContext.swift */; }; @@ -704,6 +706,9 @@ 2C63C777253EFE2300B20195 /* TableDataset.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C63C776253EFE2300B20195 /* TableDataset.swift */; }; 2C67E7E326C180D700F4450B /* StepQuizReviewExpandQuizView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C67E7E226C180D700F4450B /* StepQuizReviewExpandQuizView.swift */; }; 2C68B3C823F5292100171F89 /* ScrollViewKeyboardAdjuster.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C68B3C723F5292100171F89 /* ScrollViewKeyboardAdjuster.swift */; }; + 2C6917C625EE47D700BAE0F5 /* NewExploreBlockPlaceholderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6917C525EE47D700BAE0F5 /* NewExploreBlockPlaceholderView.swift */; }; + 2C6917D525EE489900BAE0F5 /* ExplorePlaceholderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6917D425EE489900BAE0F5 /* ExplorePlaceholderView.swift */; }; + 2C6917E325EE650200BAE0F5 /* ExplorePlaceholderActionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6917E225EE650200BAE0F5 /* ExplorePlaceholderActionButton.swift */; }; 2C698C9524CEA90100979661 /* NewProfileCoverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C698C9424CEA90100979661 /* NewProfileCoverView.swift */; }; 2C69BDCE26C147A3007C51FB /* StepQuizReviewTeacherView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C69BDCD26C147A3007C51FB /* StepQuizReviewTeacherView.swift */; }; 2C69BDD126C1482B007C51FB /* StepQuizReviewViewProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C69BDD026C1482B007C51FB /* StepQuizReviewViewProtocols.swift */; }; @@ -2205,7 +2210,6 @@ 089877AB214047ED0065DFA2 /* UserDefaults+StorageServiceProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UserDefaults+StorageServiceProtocol.swift"; sourceTree = ""; }; 089877B121404CF00065DFA2 /* StringStorageServiceProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StringStorageServiceProtocol.swift; sourceTree = ""; }; 089877B521407AB50065DFA2 /* Model_profile_staff_v24.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model_profile_staff_v24.xcdatamodel; sourceTree = ""; }; - 089A0DA61BE9FFCE004AF4EB /* UIViewExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIViewExtensions.swift; sourceTree = ""; }; 08A1256E1BDE8E460066B2B2 /* Sorter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Sorter.swift; sourceTree = ""; }; 08A1257A1BDEBCC90066B2B2 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = ""; }; 08A1257D1BDEBCD60066B2B2 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; @@ -2603,6 +2607,8 @@ 2C4391A9271994CB000770AE /* CourseInfoPurchaseModalHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseInfoPurchaseModalHeaderView.swift; sourceTree = ""; }; 2C453397204D46E90061342A /* PinsMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinsMap.swift; sourceTree = ""; }; 2C453399204D5DEE0061342A /* PinsMapSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinsMapSpec.swift; sourceTree = ""; }; + 2C456C3325D2C47100435A86 /* ContinueCourseEmptyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContinueCourseEmptyView.swift; sourceTree = ""; }; + 2C456C3D25D2C52F00435A86 /* ContinueCourseBackgroundView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContinueCourseBackgroundView.swift; sourceTree = ""; }; 2C45C6EE254160A100AF24BF /* HTMLExtractorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTMLExtractorTests.swift; sourceTree = ""; }; 2C461FEC276A488E00AAAE20 /* IAPSettingsStorageManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IAPSettingsStorageManager.swift; sourceTree = ""; }; 2C46417A24CFD23400FB6696 /* Model_lessons_demo_access_v58.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model_lessons_demo_access_v58.xcdatamodel; sourceTree = ""; }; @@ -2726,6 +2732,9 @@ 2C67CA5124E3F9D0002634EB /* Model_profile_personal_preferences_v62.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model_profile_personal_preferences_v62.xcdatamodel; sourceTree = ""; }; 2C67E7E226C180D700F4450B /* StepQuizReviewExpandQuizView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepQuizReviewExpandQuizView.swift; sourceTree = ""; }; 2C68B3C723F5292100171F89 /* ScrollViewKeyboardAdjuster.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollViewKeyboardAdjuster.swift; sourceTree = ""; }; + 2C6917C525EE47D700BAE0F5 /* NewExploreBlockPlaceholderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewExploreBlockPlaceholderView.swift; sourceTree = ""; }; + 2C6917D425EE489900BAE0F5 /* ExplorePlaceholderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExplorePlaceholderView.swift; sourceTree = ""; }; + 2C6917E225EE650200BAE0F5 /* ExplorePlaceholderActionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExplorePlaceholderActionButton.swift; sourceTree = ""; }; 2C698C9424CEA90100979661 /* NewProfileCoverView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewProfileCoverView.swift; sourceTree = ""; }; 2C69BDCD26C147A3007C51FB /* StepQuizReviewTeacherView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepQuizReviewTeacherView.swift; sourceTree = ""; }; 2C69BDD026C1482B007C51FB /* StepQuizReviewViewProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepQuizReviewViewProtocols.swift; sourceTree = ""; }; @@ -3276,6 +3285,7 @@ 2CF9BD3B2538508800C2AFD2 /* PromiseKit+Retry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PromiseKit+Retry.swift"; sourceTree = ""; }; 2CFA04DA2685DEBD0023FCE7 /* CourseBenefitDetailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseBenefitDetailViewModel.swift; sourceTree = ""; }; 2CFA04DD2685F2FB0023FCE7 /* CourseBenefitDetailHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseBenefitDetailHeaderView.swift; sourceTree = ""; }; + 2CFA489F25D1828C00BECD32 /* UIViewExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewExtensions.swift; sourceTree = ""; }; 2CFC5ABB228ADFC400B5248A /* StepsPersistenceService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepsPersistenceService.swift; sourceTree = ""; }; 2CFCBEC2B5195CB046BCB207 /* StepikAcademyCourseListView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StepikAcademyCourseListView.swift; sourceTree = ""; }; 2CFDA8181FBB3CFD0098A441 /* Model_sections_with_course_id_v21.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model_sections_with_course_id_v21.xcdatamodel; sourceTree = ""; }; @@ -4389,6 +4399,7 @@ 62E9845643D7541307856023 /* UITableViewExtensions.swift */, 2CE3318D266E0D090092EF08 /* UIView+Animations.swift */, 2C9776E924222AED0097AEFC /* UIView+TraitCollection.swift */, + 2CFA489F25D1828C00BECD32 /* UIViewExtensions.swift */, ); path = UIKit; sourceTree = ""; @@ -4948,6 +4959,17 @@ path = ApplicationShortcuts; sourceTree = ""; }; + 2C64388D25D17F480045D139 /* Views */ = { + isa = PBXGroup; + children = ( + 2C456C3D25D2C52F00435A86 /* ContinueCourseBackgroundView.swift */, + 2C456C3325D2C47100435A86 /* ContinueCourseEmptyView.swift */, + 62E98021576A447517397409 /* ContinueCourseView.swift */, + 62E98FC21463AD1C43A467BB /* ContinueLastStepView.swift */, + ); + path = Views; + sourceTree = ""; + }; 2C67075125FF844400B18163 /* Resources */ = { isa = PBXGroup; children = ( @@ -4976,6 +4998,17 @@ path = EditSplitTests; sourceTree = ""; }; + 2C6917CF25EE484300BAE0F5 /* ExploreBlockPlaceholderView */ = { + isa = PBXGroup; + children = ( + 62E9892AAAD4FAA927DCFC57 /* ExploreBlockPlaceholderView.swift */, + 2C6917E225EE650200BAE0F5 /* ExplorePlaceholderActionButton.swift */, + 2C6917D425EE489900BAE0F5 /* ExplorePlaceholderView.swift */, + 2C6917C525EE47D700BAE0F5 /* NewExploreBlockPlaceholderView.swift */, + ); + path = ExploreBlockPlaceholderView; + sourceTree = ""; + }; 2C69BDCF26C147B0007C51FB /* StepQuizReviewView */ = { isa = PBXGroup; children = ( @@ -5000,7 +5033,6 @@ 08CA59E91BBD3D55008DC44D /* UILabelExtensions.swift */, 62E98D9D45E6DA6099AF1C0B /* UITableView+TableHeaderLayout.swift */, 62E982BB5246C2BBC5E88D06 /* UIView+FromNib.swift */, - 089A0DA61BE9FFCE004AF4EB /* UIViewExtensions.swift */, 089877AB214047ED0065DFA2 /* UserDefaults+StorageServiceProtocol.swift */, ); path = Extensions; @@ -7356,15 +7388,6 @@ path = AchievementBadgeView; sourceTree = ""; }; - 2CFF8FFE242A22DF00FD7311 /* ContinueLastStepView */ = { - isa = PBXGroup; - children = ( - 62E9807E3506DAF40179593F /* ContinueActionButton.swift */, - 62E98FC21463AD1C43A467BB /* ContinueLastStepView.swift */, - ); - path = ContinueLastStepView; - sourceTree = ""; - }; 2CFF8FFF242A22EB00FD7311 /* LoadingPaginationView */ = { isa = PBXGroup; children = ( @@ -8965,6 +8988,7 @@ isa = PBXGroup; children = ( 62E98EDC02AF16EA5BAA748A /* BounceButton.swift */, + 62E9807E3506DAF40179593F /* ContinueActionButton.swift */, 62E9888DC7DFE8CFE5C220B6 /* CourseCoverImageView.swift */, 2C381BEE25505EC90084AD90 /* CourseListFilterBarButtonItem.swift */, 62E9856B0B8FB44A5492EF67 /* CourseRatingView.swift */, @@ -8986,7 +9010,6 @@ 62E986AC65867C28C93D475E /* TableInputTextField.swift */, 2C06E09A2241045800AF4DA2 /* TableInputTextView.swift */, 62E988C4DC97F14EAC09BC41 /* TabSegmentedControlView.swift */, - 2CFF8FFE242A22DF00FD7311 /* ContinueLastStepView */, 2C11C9F024EFC80400A4647B /* Layouts */, 62E989892C62C2BC2B1A3741 /* TabBar */, ); @@ -9619,9 +9642,9 @@ 62E98B7A07FDA68E6CA47ECB /* ContinueCourseOutputProtocol.swift */, 62E98A6EFA74ACC7FF243E12 /* ContinueCoursePresenter.swift */, 62E9818650597467F6A1D814 /* ContinueCourseProvider.swift */, - 62E98021576A447517397409 /* ContinueCourseView.swift */, 62E98EC2E16550A310CE6CEF /* ContinueCourseViewController.swift */, 62E984C48E60011A6C45B243 /* ContinueCourseViewModel.swift */, + 2C64388D25D17F480045D139 /* Views */, ); path = ContinueCourse; sourceTree = ""; @@ -9733,11 +9756,11 @@ isa = PBXGroup; children = ( 62E984A1DC4A18EBC5E68559 /* CourseListContainerViewFactory.swift */, - 62E9892AAAD4FAA927DCFC57 /* ExploreBlockPlaceholderView.swift */, 62E98101487588BB70A5C1A7 /* ExploreSearchBar.swift */, 62E98664A22248D073D6BAE2 /* ExploreStoriesContainerView.swift */, 2C73B0D725628ECD00EA217D /* ExploreBlockContainerView */, 2C73B0E625628FD300EA217D /* ExploreBlockHeaderView */, + 2C6917CF25EE484300BAE0F5 /* ExploreBlockPlaceholderView */, ); path = View; sourceTree = ""; @@ -11223,6 +11246,7 @@ 2CF9BD3C2538508800C2AFD2 /* PromiseKit+Retry.swift in Sources */, 0813EEA71BFE5A5400DB4B83 /* Assignment.swift in Sources */, 2C06E0B02243CD2D00AF4DA2 /* CourseInfoTabReviewsCellSkeletonView.swift in Sources */, + 2C6917E325EE650200BAE0F5 /* ExplorePlaceholderActionButton.swift in Sources */, 089877A2214047650065DFA2 /* SplitTestingService.swift in Sources */, 2C5D341625C997DF00372C61 /* CurrencySymbolMap.swift in Sources */, 2CF1B3402163BE820008DA0C /* StoriesViewController.swift in Sources */, @@ -11280,7 +11304,6 @@ 2C98423626DE2AD00098E36B /* SearchResultsNetworkService.swift in Sources */, 2C85C6A522D38A3800FDBAFE /* VotesNetworkService.swift in Sources */, 2CDC9EFA24E4FD0D00916BAE /* CourseInfoTryForFreeButton.swift in Sources */, - 089A0DA71BE9FFCE004AF4EB /* UIViewExtensions.swift in Sources */, 2C4CB60C26810CD900DA6C52 /* CourseRevenueHeaderViewModel.swift in Sources */, 2C21959626E57040008D9439 /* UserInfo.swift in Sources */, 2CD04084250F67D0004D284F /* ProcessedContent.swift in Sources */, @@ -11333,8 +11356,9 @@ 2CD9B9681F87A58B00D446C2 /* NotificationDataExtractor.swift in Sources */, 2C762B402653C93A00E0AB59 /* SwiftyJSON+DecimalNumber.swift in Sources */, 2C6FA0B02588B0EC00D50DAA /* DefaultSimpleCourseListView.swift in Sources */, - 2C98423C26DE4CA80098E36B /* SearchResult+CoreDataProperties.swift in Sources */, 2C12E4722565668000DC52CB /* UIViewController+PresentPanModal.swift in Sources */, + 2C6917C625EE47D700BAE0F5 /* NewExploreBlockPlaceholderView.swift in Sources */, + 2C98423C26DE4CA80098E36B /* SearchResult+CoreDataProperties.swift in Sources */, 2CBEAACE271EFA9A00B52D2C /* CourseInfoPurchaseModalDisclaimerView.swift in Sources */, 2CA867C12588FFF40006576E /* GridSimpleCourseListCollectionHeaderView.swift in Sources */, 2C0FE8C425F81A4900626289 /* InstructionPlainObject.swift in Sources */, @@ -11357,6 +11381,7 @@ 2CCB4B1A26E77CED0056C44E /* AnnouncementsAPI.swift in Sources */, 08C1FC331F41E74500E14B46 /* QuizPresenter.swift in Sources */, 2C604E21207E4609001588FB /* CodeEditorPreviewView.swift in Sources */, + 2C6917D525EE489900BAE0F5 /* ExplorePlaceholderView.swift in Sources */, 2CFED65F252C5AC900FCAD41 /* Result+Stepik.swift in Sources */, 2CE42D4423CCC4530073F774 /* StreamVideoQualityStorageManager.swift in Sources */, 083F2B1D1E9D9ABB00714173 /* CertificateViewData.swift in Sources */, @@ -11493,6 +11518,7 @@ 0885F84E1BA837E200F2A188 /* ApiDataDownloader.swift in Sources */, 2C5E90842333DF2100288BE3 /* CodeLanguageSuggestionsService.swift in Sources */, 2CB9E8C41F7AA5CD0004E17F /* NotificationsPresenter.swift in Sources */, + 2C456C3425D2C47100435A86 /* ContinueCourseEmptyView.swift in Sources */, 2C8BCC222486540F00DFB009 /* ShowAllButton.swift in Sources */, 08F485A21C57987C000165AA /* NumberQuizViewController.swift in Sources */, 2C5DF1431FED2758003B1177 /* CardStepDelegate.swift in Sources */, @@ -11990,6 +12016,7 @@ 62E980B234CAE5A61B9E546C /* CourseInfoTabSyllabusView.swift in Sources */, 2C698C9524CEA90100979661 /* NewProfileCoverView.swift in Sources */, 62E9870EF451D397B27AB075 /* CourseInfoTabSyllabusCellStatsView.swift in Sources */, + 2C456C3E25D2C52F00435A86 /* ContinueCourseBackgroundView.swift in Sources */, 62E9893B98E774DA8141AD0F /* CourseInfoTabSyllabusCellView.swift in Sources */, 2C2F6E6024DD3E69000E8844 /* StepikAnalyticsEvents.swift in Sources */, 62E98C92B0827BEB4E28A8D9 /* CourseInfoTabSyllabusTableViewCell.swift in Sources */, @@ -12665,6 +12692,7 @@ 2C6277BA270C5CB300FDAFD9 /* SiriShortcutsContinueUserActivityService.swift in Sources */, 0901F2E70A0CA50D44C96729 /* CourseRevenueTabPurchasesViewController.swift in Sources */, C6BC3AA52225E047D18CBB8C /* CourseRevenueTabPurchasesInputProtocol.swift in Sources */, + 2C45E4FD2774808B001AAF09 /* UIViewExtensions.swift in Sources */, 2C1966AD26E7C553000D5B06 /* AnnouncementsNetworkService.swift in Sources */, C6CF7E954A942E8ADC3FE8A2 /* CourseBenefitDetailAssembly.swift in Sources */, 7E361F13052A77F4C0D25940 /* CourseBenefitDetailDataFlow.swift in Sources */, @@ -13146,7 +13174,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 398; + CURRENT_PROJECT_VERSION = 399; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_BITCODE = YES; INFOPLIST_FILE = "Stepic/Info-Production.plist"; @@ -13176,7 +13204,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 398; + CURRENT_PROJECT_VERSION = 399; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_BITCODE = YES; "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64; @@ -13267,7 +13295,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 398; + CURRENT_PROJECT_VERSION = 399; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_BITCODE = YES; INFOPLIST_FILE = "Stepic/Info-Develop.plist"; @@ -13400,7 +13428,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 398; + CURRENT_PROJECT_VERSION = 399; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_BITCODE = YES; INFOPLIST_FILE = "Stepic/Info-Develop.plist"; @@ -13968,7 +13996,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 398; + CURRENT_PROJECT_VERSION = 399; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_BITCODE = YES; INFOPLIST_FILE = "Stepic/Info-Release.plist"; @@ -14104,7 +14132,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 398; + CURRENT_PROJECT_VERSION = 399; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_BITCODE = YES; INFOPLIST_FILE = "Stepic/Info-Release.plist"; diff --git a/Stepic/Images.xcassets/Continue Learning/Contents.json b/Stepic/Images.xcassets/Continue Learning/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Stepic/Images.xcassets/Continue Learning/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Stepic/Images.xcassets/Continue Learning/continue_learning_arrow_right.imageset/Contents.json b/Stepic/Images.xcassets/Continue Learning/continue_learning_arrow_right.imageset/Contents.json new file mode 100644 index 0000000000..b81355b517 --- /dev/null +++ b/Stepic/Images.xcassets/Continue Learning/continue_learning_arrow_right.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "continue_learning_arrow_right.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Stepic/Images.xcassets/Continue Learning/continue_learning_arrow_right.imageset/continue_learning_arrow_right.pdf b/Stepic/Images.xcassets/Continue Learning/continue_learning_arrow_right.imageset/continue_learning_arrow_right.pdf new file mode 100644 index 0000000000..884ba2aba6 Binary files /dev/null and b/Stepic/Images.xcassets/Continue Learning/continue_learning_arrow_right.imageset/continue_learning_arrow_right.pdf differ diff --git a/Stepic/Images.xcassets/Continue Learning/continue_learning_gradient.imageset/Contents.json b/Stepic/Images.xcassets/Continue Learning/continue_learning_gradient.imageset/Contents.json new file mode 100644 index 0000000000..c23d1304c6 --- /dev/null +++ b/Stepic/Images.xcassets/Continue Learning/continue_learning_gradient.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "continue_learning_gradient.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Stepic/Images.xcassets/Continue Learning/continue_learning_gradient.imageset/continue_learning_gradient.pdf b/Stepic/Images.xcassets/Continue Learning/continue_learning_gradient.imageset/continue_learning_gradient.pdf new file mode 100644 index 0000000000..dea47dda74 Binary files /dev/null and b/Stepic/Images.xcassets/Continue Learning/continue_learning_gradient.imageset/continue_learning_gradient.pdf differ diff --git a/Stepic/Images.xcassets/Course_list_placeholder/Contents.json b/Stepic/Images.xcassets/Course_list_placeholder/Contents.json index da4a164c91..73c00596a7 100644 --- a/Stepic/Images.xcassets/Course_list_placeholder/Contents.json +++ b/Stepic/Images.xcassets/Course_list_placeholder/Contents.json @@ -1,6 +1,6 @@ { "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git a/Stepic/Images.xcassets/Course_list_placeholder/course_list_purple_placeholder.imageset/Contents.json b/Stepic/Images.xcassets/Course_list_placeholder/course_list_purple_placeholder.imageset/Contents.json index f58b7a5587..63d8a15bd0 100644 --- a/Stepic/Images.xcassets/Course_list_placeholder/course_list_purple_placeholder.imageset/Contents.json +++ b/Stepic/Images.xcassets/Course_list_placeholder/course_list_purple_placeholder.imageset/Contents.json @@ -1,21 +1,15 @@ { "images" : [ { - "idiom" : "universal", "filename" : "course_list_purple_placeholder.png", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" + "idiom" : "universal" } ], "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true } -} \ No newline at end of file +} diff --git a/Stepic/Images.xcassets/Course_list_placeholder/courses_gradient_blue.imageset/Contents.json b/Stepic/Images.xcassets/Course_list_placeholder/courses_gradient_blue.imageset/Contents.json index 27c906d4f7..70bf494f51 100644 --- a/Stepic/Images.xcassets/Course_list_placeholder/courses_gradient_blue.imageset/Contents.json +++ b/Stepic/Images.xcassets/Course_list_placeholder/courses_gradient_blue.imageset/Contents.json @@ -1,21 +1,15 @@ { "images" : [ { - "idiom" : "universal", "filename" : "courses_gradient_blue.png", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" + "idiom" : "universal" } ], "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true } -} \ No newline at end of file +} diff --git a/Stepic/Images.xcassets/Course_list_placeholder/courses_gradient_pink.imageset/Contents.json b/Stepic/Images.xcassets/Course_list_placeholder/courses_gradient_pink.imageset/Contents.json index 5bd7aeeea6..d0dd800d62 100644 --- a/Stepic/Images.xcassets/Course_list_placeholder/courses_gradient_pink.imageset/Contents.json +++ b/Stepic/Images.xcassets/Course_list_placeholder/courses_gradient_pink.imageset/Contents.json @@ -1,21 +1,15 @@ { "images" : [ { - "idiom" : "universal", "filename" : "courses_gradient_pink.png", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" + "idiom" : "universal" } ], "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true } -} \ No newline at end of file +} diff --git a/Stepic/Images.xcassets/Course_list_placeholder/new_courses_gradient.imageset/Contents.json b/Stepic/Images.xcassets/Course_list_placeholder/new_courses_gradient.imageset/Contents.json new file mode 100644 index 0000000000..36eb76b2c7 --- /dev/null +++ b/Stepic/Images.xcassets/Course_list_placeholder/new_courses_gradient.imageset/Contents.json @@ -0,0 +1,25 @@ +{ + "images" : [ + { + "filename" : "new_courses_gradient.pdf", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "new_courses_gradient_dark.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Stepic/Images.xcassets/Course_list_placeholder/new_courses_gradient.imageset/new_courses_gradient.pdf b/Stepic/Images.xcassets/Course_list_placeholder/new_courses_gradient.imageset/new_courses_gradient.pdf new file mode 100644 index 0000000000..896ec0ae7a Binary files /dev/null and b/Stepic/Images.xcassets/Course_list_placeholder/new_courses_gradient.imageset/new_courses_gradient.pdf differ diff --git a/Stepic/Images.xcassets/Course_list_placeholder/new_courses_gradient.imageset/new_courses_gradient_dark.pdf b/Stepic/Images.xcassets/Course_list_placeholder/new_courses_gradient.imageset/new_courses_gradient_dark.pdf new file mode 100644 index 0000000000..4d9ee3797a Binary files /dev/null and b/Stepic/Images.xcassets/Course_list_placeholder/new_courses_gradient.imageset/new_courses_gradient_dark.pdf differ diff --git a/Stepic/Images.xcassets/Course_list_placeholder/new_courses_placeholder_gradient_large.imageset/Contents.json b/Stepic/Images.xcassets/Course_list_placeholder/new_courses_placeholder_gradient_large.imageset/Contents.json new file mode 100644 index 0000000000..4588bcbe34 --- /dev/null +++ b/Stepic/Images.xcassets/Course_list_placeholder/new_courses_placeholder_gradient_large.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "new_courses_placeholder_gradient_large.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Stepic/Images.xcassets/Course_list_placeholder/new_courses_placeholder_gradient_large.imageset/new_courses_placeholder_gradient_large.pdf b/Stepic/Images.xcassets/Course_list_placeholder/new_courses_placeholder_gradient_large.imageset/new_courses_placeholder_gradient_large.pdf new file mode 100644 index 0000000000..9fb81b865c Binary files /dev/null and b/Stepic/Images.xcassets/Course_list_placeholder/new_courses_placeholder_gradient_large.imageset/new_courses_placeholder_gradient_large.pdf differ diff --git a/Stepic/Images.xcassets/Course_list_placeholder/new_courses_placeholder_gradient_small.imageset/Contents.json b/Stepic/Images.xcassets/Course_list_placeholder/new_courses_placeholder_gradient_small.imageset/Contents.json new file mode 100644 index 0000000000..967dc6aa26 --- /dev/null +++ b/Stepic/Images.xcassets/Course_list_placeholder/new_courses_placeholder_gradient_small.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "new_courses_placeholder_gradient_small.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Stepic/Images.xcassets/Course_list_placeholder/new_courses_placeholder_gradient_small.imageset/new_courses_placeholder_gradient_small.pdf b/Stepic/Images.xcassets/Course_list_placeholder/new_courses_placeholder_gradient_small.imageset/new_courses_placeholder_gradient_small.pdf new file mode 100644 index 0000000000..ea90b05a3e Binary files /dev/null and b/Stepic/Images.xcassets/Course_list_placeholder/new_courses_placeholder_gradient_small.imageset/new_courses_placeholder_gradient_small.pdf differ diff --git a/Stepic/Images.xcassets/Course_list_placeholder/placeholder_gradient_blue.imageset/Contents.json b/Stepic/Images.xcassets/Course_list_placeholder/placeholder_gradient_blue.imageset/Contents.json index 88f20cbed8..bcd6924e32 100644 --- a/Stepic/Images.xcassets/Course_list_placeholder/placeholder_gradient_blue.imageset/Contents.json +++ b/Stepic/Images.xcassets/Course_list_placeholder/placeholder_gradient_blue.imageset/Contents.json @@ -1,15 +1,15 @@ { "images" : [ { - "idiom" : "universal", - "filename" : "placeholder_gradient_blue.pdf" + "filename" : "placeholder_gradient_blue.pdf", + "idiom" : "universal" } ], "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 }, "properties" : { "preserves-vector-representation" : true } -} \ No newline at end of file +} diff --git a/Stepic/Images.xcassets/Course_list_placeholder/placeholder_gradient_pink.imageset/Contents.json b/Stepic/Images.xcassets/Course_list_placeholder/placeholder_gradient_pink.imageset/Contents.json index 8801a76166..78d2bbb8da 100644 --- a/Stepic/Images.xcassets/Course_list_placeholder/placeholder_gradient_pink.imageset/Contents.json +++ b/Stepic/Images.xcassets/Course_list_placeholder/placeholder_gradient_pink.imageset/Contents.json @@ -1,15 +1,15 @@ { "images" : [ { - "idiom" : "universal", - "filename" : "placeholder_gradient_pink.pdf" + "filename" : "placeholder_gradient_pink.pdf", + "idiom" : "universal" } ], "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 }, "properties" : { "preserves-vector-representation" : true } -} \ No newline at end of file +} diff --git a/Stepic/Images.xcassets/Course_list_placeholder/placeholder_gradient_purple.imageset/Contents.json b/Stepic/Images.xcassets/Course_list_placeholder/placeholder_gradient_purple.imageset/Contents.json index 75e060d221..1c1230c482 100644 --- a/Stepic/Images.xcassets/Course_list_placeholder/placeholder_gradient_purple.imageset/Contents.json +++ b/Stepic/Images.xcassets/Course_list_placeholder/placeholder_gradient_purple.imageset/Contents.json @@ -1,15 +1,15 @@ { "images" : [ { - "idiom" : "universal", - "filename" : "placeholder_gradient_purple.pdf" + "filename" : "placeholder_gradient_purple.pdf", + "idiom" : "universal" } ], "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 }, "properties" : { "preserves-vector-representation" : true } -} \ No newline at end of file +} diff --git a/Stepic/Images.xcassets/plus.imageset/Contents.json b/Stepic/Images.xcassets/plus.imageset/Contents.json new file mode 100644 index 0000000000..0babeff575 --- /dev/null +++ b/Stepic/Images.xcassets/plus.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "plus.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Stepic/Images.xcassets/plus.imageset/plus.pdf b/Stepic/Images.xcassets/plus.imageset/plus.pdf new file mode 100644 index 0000000000..28c8c8d8db Binary files /dev/null and b/Stepic/Images.xcassets/plus.imageset/plus.pdf differ diff --git a/Stepic/Info-Production.plist b/Stepic/Info-Production.plist index 0f7d8ff08e..92175e85e2 100644 --- a/Stepic/Info-Production.plist +++ b/Stepic/Info-Production.plist @@ -62,7 +62,7 @@ CFBundleVersion - 398 + $(CURRENT_PROJECT_VERSION) FacebookAppID 171127739724012 FacebookDisplayName @@ -85,52 +85,19 @@ NSAllowsArbitraryLoads - UIBackgroundModes - - audio - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UIRequiredDeviceCapabilities - - armv7 - - UIStatusBarStyle - UIStatusBarStyleLightContent - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - UIInterfaceOrientationPortraitUpsideDown - - UISupportedInterfaceOrientations~ipad + NSUserActivityTypes - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight + $(PRODUCT_BUNDLE_IDENTIFIER).ContinueLearningUserActivity - UIViewControllerBasedStatusBarAppearance - - branch_key - - live - key_live_ekt7qHLldFSKQyO2DT3NYellwFfko55Q - test - key_test_gjtWCOSjnAMKMtV9qJ4YyeelFzced092 - UIApplicationShortcutItems - UIApplicationShortcutItemType - $(PRODUCT_BUNDLE_IDENTIFIER).ContinueLearning UIApplicationShortcutItemIconType UIApplicationShortcutIconTypePlay UIApplicationShortcutItemTitle shortcutTitleContinueLearning + UIApplicationShortcutItemType + $(PRODUCT_BUNDLE_IDENTIFIER).ContinueLearning UIApplicationShortcutItemUserInfo version @@ -138,12 +105,12 @@ - UIApplicationShortcutItemType - $(PRODUCT_BUNDLE_IDENTIFIER).SearchCourses UIApplicationShortcutItemIconType UIApplicationShortcutIconTypeSearch UIApplicationShortcutItemTitle shortcutTitleSearchCourses + UIApplicationShortcutItemType + $(PRODUCT_BUNDLE_IDENTIFIER).SearchCourses UIApplicationShortcutItemUserInfo version @@ -151,12 +118,12 @@ - UIApplicationShortcutItemType - $(PRODUCT_BUNDLE_IDENTIFIER).Profile UIApplicationShortcutItemIconType UIApplicationShortcutIconTypeContact UIApplicationShortcutItemTitle shortcutTitleProfile + UIApplicationShortcutItemType + $(PRODUCT_BUNDLE_IDENTIFIER).Profile UIApplicationShortcutItemUserInfo version @@ -164,12 +131,12 @@ - UIApplicationShortcutItemType - $(PRODUCT_BUNDLE_IDENTIFIER).Notifications UIApplicationShortcutItemIconFile shortcut-item-notifications UIApplicationShortcutItemTitle shortcutTitleNotifications + UIApplicationShortcutItemType + $(PRODUCT_BUNDLE_IDENTIFIER).Notifications UIApplicationShortcutItemUserInfo version @@ -177,9 +144,42 @@ - NSUserActivityTypes + UIBackgroundModes - $(PRODUCT_BUNDLE_IDENTIFIER).ContinueLearningUserActivity + audio + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UIStatusBarStyle + UIStatusBarStyleLightContent + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + UIInterfaceOrientationPortraitUpsideDown + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + branch_key + + live + key_live_ekt7qHLldFSKQyO2DT3NYellwFfko55Q + test + key_test_gjtWCOSjnAMKMtV9qJ4YyeelFzced092 + diff --git a/Stepic/Legacy/Controllers/CodeEditorSettings/CodeEditorPreview/CodeEditorPreviewView.swift b/Stepic/Legacy/Controllers/CodeEditorSettings/CodeEditorPreview/CodeEditorPreviewView.swift index 3d52a0f1ad..010e199f80 100644 --- a/Stepic/Legacy/Controllers/CodeEditorSettings/CodeEditorPreview/CodeEditorPreviewView.swift +++ b/Stepic/Legacy/Controllers/CodeEditorSettings/CodeEditorPreview/CodeEditorPreviewView.swift @@ -61,7 +61,7 @@ final class CodeEditorPreviewView: NibInitializableView { self.previewTextView = UITextView(frame: previewContainer.frame, textContainer: textContainer) self.previewTextView.translatesAutoresizingMaskIntoConstraints = false - self.previewTextView.setRoundedCorners(cornerRadius: 5, borderWidth: 1.0, borderColor: .lightGray) + self.previewTextView.roundAllCorners(radius: 5, borderWidth: 1.0, borderColor: .lightGray) self.previewTextView.isEditable = false self.previewTextView.isSelectable = false self.previewContainer.addSubview(previewTextView) diff --git a/Stepic/Legacy/Controllers/PersonalDeadlines/PersonalDeadlineModeCollectionViewCell/PersonalDeadlineModeCollectionViewCell.swift b/Stepic/Legacy/Controllers/PersonalDeadlines/PersonalDeadlineModeCollectionViewCell/PersonalDeadlineModeCollectionViewCell.swift index fd2c341d34..9fa23c3525 100644 --- a/Stepic/Legacy/Controllers/PersonalDeadlines/PersonalDeadlineModeCollectionViewCell/PersonalDeadlineModeCollectionViewCell.swift +++ b/Stepic/Legacy/Controllers/PersonalDeadlines/PersonalDeadlineModeCollectionViewCell/PersonalDeadlineModeCollectionViewCell.swift @@ -34,7 +34,7 @@ final class PersonalDeadlineModeCollectionViewCell: UICollectionViewCell { override func awakeFromNib() { super.awakeFromNib() - self.contentView.setRoundedCorners(cornerRadius: 8, borderWidth: 1, borderColor: .stepikSeparator) + self.contentView.roundAllCorners(radius: 8, borderWidth: 1, borderColor: .stepikSeparator) self.colorize() } diff --git a/Stepic/Legacy/Controllers/Quizzes/BaseQuiz/QuizViewController/QuizViewController.swift b/Stepic/Legacy/Controllers/Quizzes/BaseQuiz/QuizViewController/QuizViewController.swift index 6bb1dc2eb8..c2fb82e6a9 100644 --- a/Stepic/Legacy/Controllers/Quizzes/BaseQuiz/QuizViewController/QuizViewController.swift +++ b/Stepic/Legacy/Controllers/Quizzes/BaseQuiz/QuizViewController/QuizViewController.swift @@ -125,7 +125,7 @@ class QuizViewController: UIViewController, QuizView, QuizControllerDataSource, self?.presenter?.refreshAttempt() }), for: .connectionError) - self.hintView.setRoundedCorners(cornerRadius: 8, borderWidth: 1, borderColor: UIColor.black) + self.hintView.roundAllCorners(radius: 8, borderWidth: 1, borderColor: UIColor.black) self.hintHeightWebViewHelper = CellWebViewHelper( webView: hintWebView, fontSize: StepFontSizeStorageManager().globalStepFontSize diff --git a/Stepic/Legacy/Controllers/Quizzes/StringQuiz/StringQuizViewController.swift b/Stepic/Legacy/Controllers/Quizzes/StringQuiz/StringQuizViewController.swift index abd7810593..0c3cec9c13 100644 --- a/Stepic/Legacy/Controllers/Quizzes/StringQuiz/StringQuizViewController.swift +++ b/Stepic/Legacy/Controllers/Quizzes/StringQuiz/StringQuizViewController.swift @@ -36,7 +36,7 @@ final class StringQuizViewController: QuizViewController { make.trailing.equalTo(containerView.safeAreaLayoutGuide.snp.trailing).offset(useSmallPadding ? -8 : -16) } - textView.setRoundedCorners(cornerRadius: 8.0, borderWidth: 0.5, borderColor: UIColor.lightGray) + textView.roundAllCorners(radius: 8.0, borderWidth: 0.5, borderColor: UIColor.lightGray) textView.font = UIFont.systemFont(ofSize: 16) textView.snp.makeConstraints { $0.height.equalTo(textViewHeight) } diff --git a/Stepic/Legacy/Controllers/Stories/Stories/StoriesAssembly.swift b/Stepic/Legacy/Controllers/Stories/Stories/StoriesAssembly.swift index fb21390a9d..e857362d34 100644 --- a/Stepic/Legacy/Controllers/Stories/Stories/StoriesAssembly.swift +++ b/Stepic/Legacy/Controllers/Stories/Stories/StoriesAssembly.swift @@ -11,7 +11,10 @@ import UIKit final class StoriesAssembly: Assembly { weak var moduleOutput: StoriesOutputProtocol? - init(output: StoriesOutputProtocol?) { + private let storyOpenSource: StoryOpenSource + + init(storyOpenSource: StoryOpenSource, output: StoriesOutputProtocol? = nil) { + self.storyOpenSource = storyOpenSource self.moduleOutput = output } @@ -28,6 +31,7 @@ final class StoriesAssembly: Assembly { ) presenter.moduleOutput = self.moduleOutput viewController.presenter = presenter + viewController.storyOpenSource = self.storyOpenSource return viewController } diff --git a/Stepic/Legacy/Controllers/Stories/Stories/StoriesViewController/StoriesViewController.swift b/Stepic/Legacy/Controllers/Stories/Stories/StoriesViewController/StoriesViewController.swift index 62417523ad..46017fef81 100644 --- a/Stepic/Legacy/Controllers/Stories/Stories/StoriesViewController/StoriesViewController.swift +++ b/Stepic/Legacy/Controllers/Stories/Stories/StoriesViewController/StoriesViewController.swift @@ -13,6 +13,7 @@ final class StoriesViewController: UIViewController, ControllerWithStepikPlaceho var placeholderContainer = StepikPlaceholderControllerContainer() var presenter: StoriesPresenterProtocol? + var storyOpenSource = StoryOpenSource.catalog private var stories: [Story] = [] private var currentItemFrame: CGRect? @@ -97,23 +98,25 @@ final class StoriesViewController: UIViewController, ControllerWithStepikPlaceho }() func showStory(at index: Int) { - let moduleToPresent = OpenedStoriesAssembly( + let assembly = OpenedStoriesAssembly( stories: self.stories, startPosition: index, - storyOpenSource: .catalog, + storyOpenSource: self.storyOpenSource, moduleOutput: self.presenter as? OpenedStoriesOutputProtocol - ).makeModule() + ) + let viewController = assembly.makeModule() + if DeviceInfo.current.isPad { self.customPresentViewController( self.storyPresentr, - viewController: moduleToPresent, + viewController: viewController, animated: true, completion: nil ) } else { - moduleToPresent.modalPresentationStyle = .custom - moduleToPresent.transitioningDelegate = self - self.present(moduleToPresent, animated: true, completion: nil) + viewController.modalPresentationStyle = .custom + viewController.transitioningDelegate = self + self.present(viewController, animated: true, completion: nil) } } diff --git a/Stepic/Legacy/Controllers/Stories/StoryPartViews/FeedbackStoryView/FeedbackStoryFormView.swift b/Stepic/Legacy/Controllers/Stories/StoryPartViews/FeedbackStoryView/FeedbackStoryFormView.swift index 1c9a037435..e9fd930a4a 100644 --- a/Stepic/Legacy/Controllers/Stories/StoryPartViews/FeedbackStoryView/FeedbackStoryFormView.swift +++ b/Stepic/Legacy/Controllers/Stories/StoryPartViews/FeedbackStoryView/FeedbackStoryFormView.swift @@ -45,7 +45,7 @@ final class FeedbackStoryFormView: UIView { textView.isScrollEnabled = true textView.isUserInteractionEnabled = true textView.dataDetectorTypes = [] - textView.setRoundedCorners(cornerRadius: self.appearance.inputTextViewCornerRadius) + textView.roundAllCorners(radius: self.appearance.inputTextViewCornerRadius) return textView }() @@ -144,7 +144,7 @@ final class FeedbackStoryFormView: UIView { extension FeedbackStoryFormView: ProgrammaticallyInitializableViewProtocol { func setupView() { - self.setRoundedCorners(cornerRadius: self.appearance.cornerRadius) + self.roundAllCorners(radius: self.appearance.cornerRadius) } func addSubviews() { diff --git a/Stepic/Legacy/Controllers/Stories/StoryPartViews/FeedbackStoryView/FeedbackStoryView.swift b/Stepic/Legacy/Controllers/Stories/StoryPartViews/FeedbackStoryView/FeedbackStoryView.swift index 93974e4c02..5374c015f1 100644 --- a/Stepic/Legacy/Controllers/Stories/StoryPartViews/FeedbackStoryView/FeedbackStoryView.swift +++ b/Stepic/Legacy/Controllers/Stories/StoryPartViews/FeedbackStoryView/FeedbackStoryView.swift @@ -43,7 +43,7 @@ final class FeedbackStoryView: UIView, UIStoryPartViewProtocol { button.addTarget(self, action: #selector(self.actionButtonClicked), for: .touchUpInside) let cornerRadius = self.appearance.actionButtonHeight / 2 - button.setRoundedCorners(cornerRadius: cornerRadius) + button.roundAllCorners(radius: cornerRadius) button.titleInsets = .init(top: 0, left: cornerRadius, bottom: 0, right: cornerRadius) diff --git a/Stepic/Legacy/Controllers/Stories/StoryPartViews/TextStoryView/TextStoryView.swift b/Stepic/Legacy/Controllers/Stories/StoryPartViews/TextStoryView/TextStoryView.swift index 025dd17cc2..470bf91ea3 100644 --- a/Stepic/Legacy/Controllers/Stories/StoryPartViews/TextStoryView/TextStoryView.swift +++ b/Stepic/Legacy/Controllers/Stories/StoryPartViews/TextStoryView/TextStoryView.swift @@ -157,7 +157,7 @@ final class TextStoryView: UIView, UIStoryPartViewProtocol { storyButton.setTitle(buttonModel.title, for: .normal) let cornerRadius = self.appearance.buttonHeight / 2.0 - storyButton.setRoundedCorners(cornerRadius: cornerRadius) + storyButton.roundAllCorners(radius: cornerRadius) storyButton.widthDelta = cornerRadius * 2 containerView.addSubview(storyButton) diff --git a/Stepic/Legacy/Controllers/VideoPlayer/StepikVideoPlayerViewController/StepikVideoPlayerViewController.swift b/Stepic/Legacy/Controllers/VideoPlayer/StepikVideoPlayerViewController/StepikVideoPlayerViewController.swift index e2fa392709..a6a1ea7c31 100644 --- a/Stepic/Legacy/Controllers/VideoPlayer/StepikVideoPlayerViewController/StepikVideoPlayerViewController.swift +++ b/Stepic/Legacy/Controllers/VideoPlayer/StepikVideoPlayerViewController/StepikVideoPlayerViewController.swift @@ -372,9 +372,9 @@ final class StepikVideoPlayerViewController: UIViewController { self.backButton.setTitle(NSLocalizedString("Done", comment: ""), for: .normal) // Set rounded corners for controls containers. - self.topContainerView.setRoundedCorners(cornerRadius: Appearance.topContainerViewCornerRadius) - self.bottomFullscreenControlsView.setRoundedCorners( - cornerRadius: Appearance.bottomFullscreenControlsCornerRadius + self.topContainerView.roundAllCorners(radius: Appearance.topContainerViewCornerRadius) + self.bottomFullscreenControlsView.roundAllCorners( + radius: Appearance.bottomFullscreenControlsCornerRadius ) self.rateButton.setTitle("\(self.currentVideoRate.rawValue)x", for: .normal) diff --git a/Stepic/Legacy/Extensions/UIViewExtensions.swift b/Stepic/Legacy/Extensions/UIViewExtensions.swift deleted file mode 100644 index 6080004f36..0000000000 --- a/Stepic/Legacy/Extensions/UIViewExtensions.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// UIViewExtensions.swift -// Stepic -// -// Created by Alexander Karpov on 04.11.15. -// Copyright © 2015 Alex Karpov. All rights reserved. -// - -import UIKit - -extension UIView { - func setRoundedBounds(width: CGFloat, color: UIColor = UIColor.white) { - self.layer.cornerRadius = self.bounds.width / 2 - self.layer.borderWidth = width - self.layer.borderColor = color.cgColor - self.layer.masksToBounds = true - self.clipsToBounds = true - } - - func setRoundedCorners(cornerRadius radius: CGFloat, borderWidth: CGFloat? = nil, borderColor: UIColor? = nil) { - self.layer.cornerRadius = radius - if let bw = borderWidth { - self.layer.borderWidth = bw - } - if let bc = borderColor { - self.layer.borderColor = bc.cgColor - } - self.layer.masksToBounds = true - self.clipsToBounds = true - } -} diff --git a/Stepic/Legacy/Views/AvatarImageView.swift b/Stepic/Legacy/Views/AvatarImageView.swift index 28914cc39f..9fac1b2d66 100644 --- a/Stepic/Legacy/Views/AvatarImageView.swift +++ b/Stepic/Legacy/Views/AvatarImageView.swift @@ -38,9 +38,9 @@ final class AvatarImageView: UIImageView { private func updateShape() { switch self.shape { case .circle(let borderWidth, let borderColor): - self.setRoundedBounds(width: borderWidth, color: borderColor) + self.roundBounds(width: borderWidth, color: borderColor) case .rectangle(let radius): - self.setRoundedCorners(cornerRadius: radius, borderWidth: 0) + self.roundAllCorners(radius: radius, borderWidth: 0) } } diff --git a/Stepic/Legacy/Views/PersonalDeadlinesSuggestionWidgetView/PersonalDeadlinesSuggestionWidgetView.swift b/Stepic/Legacy/Views/PersonalDeadlinesSuggestionWidgetView/PersonalDeadlinesSuggestionWidgetView.swift index 64ff1bd8bd..6627e8bac0 100644 --- a/Stepic/Legacy/Views/PersonalDeadlinesSuggestionWidgetView/PersonalDeadlinesSuggestionWidgetView.swift +++ b/Stepic/Legacy/Views/PersonalDeadlinesSuggestionWidgetView/PersonalDeadlinesSuggestionWidgetView.swift @@ -19,7 +19,7 @@ final class PersonalDeadlinesSuggestionWidgetView: NibInitializableView { override var nibName: String { "PersonalDeadlinesSuggestionWidgetView" } override func setupSubviews() { - self.view.setRoundedCorners(cornerRadius: 8) + self.view.roundAllCorners(radius: 8) yesButton.setRoundedCorners(cornerRadius: 8, borderWidth: 1, borderColor: .stepikLightBlue) localize() } diff --git a/Stepic/Legacy/Views/Skeleton/Views/CourseList/ContinueCourseSkeletonView.swift b/Stepic/Legacy/Views/Skeleton/Views/CourseList/ContinueCourseSkeletonView.swift index a055464cdb..c371aae5e2 100644 --- a/Stepic/Legacy/Views/Skeleton/Views/CourseList/ContinueCourseSkeletonView.swift +++ b/Stepic/Legacy/Views/Skeleton/Views/CourseList/ContinueCourseSkeletonView.swift @@ -11,15 +11,27 @@ import UIKit extension ContinueCourseSkeletonView { struct Appearance { - let mainInsets = UIEdgeInsets(top: 20, left: 20, bottom: 0, right: 20) - let cornerRadius: CGFloat = 8.0 + let labelsCornerRadius: CGFloat = 5 + let defaultInsets = LayoutInsets.default + + let coverCornerRadius: CGFloat = 8 + let coverSize = CGSize(width: 40, height: 40) + + let courseLabelInsets = LayoutInsets(left: 8, right: 8) + let courseLabelHeight: CGFloat = 17 + let courseLabelWidthRatio: CGFloat = 0.7 + + let statsViewHeight: CGFloat = 17 + let statsViewWidthRatio: CGFloat = 0.4 } } final class ContinueCourseSkeletonView: UIView { let appearance: Appearance - private lazy var largeView = UIView() + private lazy var courseCoverView = UIView() + private lazy var courseLabelView = UIView() + private lazy var courseStatsView = UIView() init(frame: CGRect = .zero, appearance: Appearance = Appearance()) { self.appearance = appearance @@ -39,21 +51,46 @@ extension ContinueCourseSkeletonView: ProgrammaticallyInitializableViewProtocol func setupView() { self.backgroundColor = .clear - self.largeView.clipsToBounds = true - self.largeView.layer.cornerRadius = self.appearance.cornerRadius + self.courseCoverView.clipsToBounds = true + self.courseCoverView.layer.cornerRadius = self.appearance.coverCornerRadius + + self.courseLabelView.clipsToBounds = true + self.courseLabelView.layer.cornerRadius = self.appearance.labelsCornerRadius + + self.courseStatsView.clipsToBounds = true + self.courseStatsView.layer.cornerRadius = self.appearance.labelsCornerRadius } func addSubviews() { - self.addSubview(self.largeView) + self.addSubviews([self.courseCoverView, self.courseLabelView, self.courseStatsView]) } func makeConstraints() { - self.largeView.translatesAutoresizingMaskIntoConstraints = false - self.largeView.snp.makeConstraints { make in - make.top.equalToSuperview().offset(self.appearance.mainInsets.top) - make.leading.equalToSuperview().offset(self.appearance.mainInsets.left) - make.trailing.equalToSuperview().offset(-self.appearance.mainInsets.right) - make.bottom.equalToSuperview().offset(self.appearance.mainInsets.bottom) + self.courseCoverView.translatesAutoresizingMaskIntoConstraints = false + self.courseCoverView.snp.makeConstraints { make in + make.leading + .equalTo(self.safeAreaLayoutGuide.snp.leading) + .offset(self.appearance.defaultInsets.left) + make.centerY.equalToSuperview() + make.size.equalTo(self.appearance.coverSize) + } + + self.courseLabelView.translatesAutoresizingMaskIntoConstraints = false + self.courseLabelView.snp.makeConstraints { make in + make.top.equalTo(self.courseCoverView.snp.top) + make.leading + .equalTo(self.courseCoverView.snp.trailing) + .offset(self.appearance.courseLabelInsets.left) + make.height.equalTo(self.appearance.courseLabelHeight) + make.width.equalToSuperview().multipliedBy(self.appearance.courseLabelWidthRatio) + } + + self.courseStatsView.translatesAutoresizingMaskIntoConstraints = false + self.courseStatsView.snp.makeConstraints { make in + make.leading.equalTo(self.courseLabelView.snp.leading) + make.bottom.equalTo(self.courseCoverView.snp.bottom) + make.height.equalTo(self.appearance.statsViewHeight) + make.width.equalToSuperview().multipliedBy(self.appearance.statsViewWidthRatio) } } } diff --git a/Stepic/Legacy/Views/StepikButton.swift b/Stepic/Legacy/Views/StepikButton.swift index fcc6d7f921..332dc680cf 100644 --- a/Stepic/Legacy/Views/StepikButton.swift +++ b/Stepic/Legacy/Views/StepikButton.swift @@ -60,7 +60,7 @@ class StepikButton: UIButton { ? UIColor.stepikLightSecondaryBackground : UIColor(hex6: 0x5d5d70, alpha: 1) self.setTitleColor(self.isLightBackground ? UIColor.stepikPrimaryText : UIColor.white, for: .normal) - self.setRoundedCorners(cornerRadius: 8, borderWidth: 0) + self.roundAllCorners(radius: 8, borderWidth: 0) } else { self.backgroundColor = self.isLightBackground ? UIColor.stepikGreen.withAlphaComponent(0.1) diff --git a/Stepic/Sources/Extensions/UIKit/UIViewExtensions.swift b/Stepic/Sources/Extensions/UIKit/UIViewExtensions.swift new file mode 100644 index 0000000000..7eaa20ad37 --- /dev/null +++ b/Stepic/Sources/Extensions/UIKit/UIViewExtensions.swift @@ -0,0 +1,57 @@ +import UIKit + +extension UIView { + /// Add array of subviews to view. + /// + /// - Parameter subviews: array of subviews to add to self. + func addSubviews(_ subviews: [UIView]) { + subviews.forEach { self.addSubview($0) } + } + + /// Set some or all corners radiuses of view. + /// + /// - Parameters: + /// - corners: array of corners to change (example: [.bottomLeft, .topRight]). + /// - radius: radius for selected corners. + func roundCorners(_ corners: UIRectCorner, radius: CGFloat) { + if corners.contains(.allCorners) { + self.roundAllCorners(radius: radius) + } else { + let maskPath = UIBezierPath( + roundedRect: self.bounds, + byRoundingCorners: corners, + cornerRadii: CGSize(width: radius, height: radius) + ) + + let shape = CAShapeLayer() + shape.path = maskPath.cgPath + self.layer.mask = shape + + self.layer.masksToBounds = true + self.clipsToBounds = true + } + } + + func roundAllCorners(radius: CGFloat, borderWidth: CGFloat? = nil, borderColor: UIColor? = nil ) { + self.layer.cornerRadius = radius + + if let borderWidth = borderWidth { + self.layer.borderWidth = borderWidth + } + + if let borderColor = borderColor { + self.layer.borderColor = borderColor.cgColor + } + + self.layer.masksToBounds = true + self.clipsToBounds = true + } + + func roundBounds(width: CGFloat, color: UIColor = UIColor.white) { + self.layer.cornerRadius = self.bounds.width / 2 + self.layer.borderWidth = width + self.layer.borderColor = color.cgColor + self.layer.masksToBounds = true + self.clipsToBounds = true + } +} diff --git a/Stepic/Sources/Modules/BaseExplore/BaseExploreView.swift b/Stepic/Sources/Modules/BaseExplore/BaseExploreView.swift index 77428c980c..d748dfff4e 100644 --- a/Stepic/Sources/Modules/BaseExplore/BaseExploreView.swift +++ b/Stepic/Sources/Modules/BaseExplore/BaseExploreView.swift @@ -29,7 +29,11 @@ final class BaseExploreView: UIView { } func removeBlockView(_ view: UIView) { - self.scrollableStackView.removeArrangedView(view) + if self.scrollableStackView.arrangedSubviews.contains(view) { + self.scrollableStackView.removeArrangedView(view) + } else { + view.removeFromSuperview() + } } func insertBlockView(_ view: UIView, at position: Int) { @@ -73,6 +77,15 @@ extension BaseExploreView: ProgrammaticallyInitializableViewProtocol { } extension BaseExploreView: ScrollableStackViewDelegate { + var contentInsets: UIEdgeInsets { + get { + self.scrollableStackView.contentInsets + } + set { + self.scrollableStackView.contentInsets = newValue + } + } + func scrollableStackViewRefreshControlDidRefresh(_ scrollableStackView: ScrollableStackView) { self.delegate?.refreshControlDidRefresh() } diff --git a/Stepic/Sources/Modules/BaseExplore/BaseExploreViewController.swift b/Stepic/Sources/Modules/BaseExplore/BaseExploreViewController.swift index 310e70ecaf..13e9a6ea3e 100644 --- a/Stepic/Sources/Modules/BaseExplore/BaseExploreViewController.swift +++ b/Stepic/Sources/Modules/BaseExplore/BaseExploreViewController.swift @@ -47,20 +47,29 @@ class BaseExploreViewController: UIViewController { // MARK: Modules func registerSubmodule(_ submodule: Submodule) { + defer { + submodule.viewController?.didMove(toParent: self) + } + self.submodules.append(submodule) if let viewController = submodule.viewController { self.addChild(viewController) } - // We have contract here: - // - subviews in exploreView have same position as in corresponding Submodule object - for module in self.submodules where module.type.position >= submodule.type.position { - self.exploreView?.insertBlockView( - submodule.view, - before: module.view - ) - return + if submodule.isArrangeable { + let arrangeableSubmodules = self.submodules.filter(\.isArrangeable) + // We have contract here: + // - subviews in exploreView have same position as in corresponding Submodule object + for module in arrangeableSubmodules where module.type.position >= submodule.type.position { + self.exploreView?.insertBlockView( + submodule.view, + before: module.view + ) + return + } + } else { + self.exploreView?.addSubview(submodule.view) } } @@ -71,6 +80,7 @@ class BaseExploreViewController: UIViewController { } func removeSubmodule(_ submodule: Submodule) { + submodule.viewController?.willMove(toParent: nil) self.exploreView?.removeBlockView(submodule.view) submodule.viewController?.removeFromParent() self.submodules = self.submodules.filter { submodule.view != $0.view } @@ -124,6 +134,7 @@ class BaseExploreViewController: UIViewController { struct Submodule { let viewController: UIViewController? let view: UIView + var isArrangeable = true let isLanguageDependent: Bool let type: SubmoduleType } diff --git a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoPurchaseModal/Views/CourseInfoPurchaseModalCourseCoverView.swift b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoPurchaseModal/Views/CourseInfoPurchaseModalCourseCoverView.swift index f70479f26a..2c8b13fc4c 100644 --- a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoPurchaseModal/Views/CourseInfoPurchaseModalCourseCoverView.swift +++ b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoPurchaseModal/Views/CourseInfoPurchaseModalCourseCoverView.swift @@ -18,7 +18,7 @@ final class CourseInfoPurchaseModalCourseCoverView: UIView { private lazy var coverImageView: CourseCoverImageView = { let view = CourseCoverImageView() - view.setRoundedCorners(cornerRadius: self.appearance.coverImageViewCornerRadius) + view.roundAllCorners(radius: self.appearance.coverImageViewCornerRadius) return view }() diff --git a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoPurchaseModal/Views/PromoCode/CourseInfoPurchaseModalPromoCodeView.swift b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoPurchaseModal/Views/PromoCode/CourseInfoPurchaseModalPromoCodeView.swift index 6ca8937896..78b809ba63 100644 --- a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoPurchaseModal/Views/PromoCode/CourseInfoPurchaseModalPromoCodeView.swift +++ b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoPurchaseModal/Views/PromoCode/CourseInfoPurchaseModalPromoCodeView.swift @@ -65,8 +65,8 @@ final class CourseInfoPurchaseModalPromoCodeView: UIView { textField.textColor = self.appearance.textFieldTextColor textField.font = self.appearance.textFieldFont textField.textInsets = self.appearance.textFieldInsets - textField.setRoundedCorners( - cornerRadius: self.appearance.textFieldCornerRadius, + textField.roundAllCorners( + radius: self.appearance.textFieldCornerRadius, borderWidth: self.appearance.textFieldBorderWidth, borderColor: self.appearance.textFieldBorderColor ) diff --git a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabNews/Views/Cell/Badge/CourseInfoTabNewsBadgeView.swift b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabNews/Views/Cell/Badge/CourseInfoTabNewsBadgeView.swift index e5fbdff2ba..4075eff364 100644 --- a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabNews/Views/Cell/Badge/CourseInfoTabNewsBadgeView.swift +++ b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabNews/Views/Cell/Badge/CourseInfoTabNewsBadgeView.swift @@ -72,7 +72,7 @@ final class CourseInfoTabNewsBadgeView: UIView { override func layoutSubviews() { super.layoutSubviews() - self.setRoundedCorners(cornerRadius: self.intrinsicContentSize.height / 2) + self.roundAllCorners(radius: self.intrinsicContentSize.height / 2) } func configure(type: BadgeType) { diff --git a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabNews/Views/Cell/CourseInfoTabNewsStatisticsView.swift b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabNews/Views/Cell/CourseInfoTabNewsStatisticsView.swift index aefb33989d..bd323f614a 100644 --- a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabNews/Views/Cell/CourseInfoTabNewsStatisticsView.swift +++ b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabNews/Views/Cell/CourseInfoTabNewsStatisticsView.swift @@ -109,7 +109,7 @@ final class CourseInfoTabNewsStatisticsView: UIView { extension CourseInfoTabNewsStatisticsView: ProgrammaticallyInitializableViewProtocol { func setupView() { self.backgroundColor = self.appearance.backgroundColor - self.setRoundedCorners(cornerRadius: self.appearance.cornerRadius) + self.roundAllCorners(radius: self.appearance.cornerRadius) } func addSubviews() { diff --git a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabReviews/Views/Header/CourseInfoTabReviewsReviewButton.swift b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabReviews/Views/Header/CourseInfoTabReviewsReviewButton.swift index 964e41d054..d50e12c3ce 100644 --- a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabReviews/Views/Header/CourseInfoTabReviewsReviewButton.swift +++ b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabReviews/Views/Header/CourseInfoTabReviewsReviewButton.swift @@ -68,7 +68,7 @@ final class CourseInfoTabReviewsReviewButton: UIControl { extension CourseInfoTabReviewsReviewButton: ProgrammaticallyInitializableViewProtocol { func setupView() { self.backgroundColor = self.appearance.backgroundColor - self.setRoundedCorners(cornerRadius: self.appearance.cornerRadius) + self.roundAllCorners(radius: self.appearance.cornerRadius) } func addSubviews() { diff --git a/Stepic/Sources/Modules/CourseList/Views/CourseListColorMode.swift b/Stepic/Sources/Modules/CourseList/Views/CourseListColorMode.swift index ff827aa09c..e9607696af 100644 --- a/Stepic/Sources/Modules/CourseList/Views/CourseListColorMode.swift +++ b/Stepic/Sources/Modules/CourseList/Views/CourseListColorMode.swift @@ -4,6 +4,7 @@ enum CourseListColorMode { case light case dark case grouped + case clearLight static var `default`: CourseListColorMode { .light } } @@ -11,7 +12,7 @@ enum CourseListColorMode { extension CourseListColorMode { var exploreBlockHeaderViewAppearance: ExploreBlockHeaderView.Appearance { switch self { - case .light, .grouped: + case .light, .grouped, .clearLight: return .init( titleLabelColor: .stepikSystemPrimaryText, showAllButtonColor: .stepikSystemSecondaryText @@ -26,7 +27,7 @@ extension CourseListColorMode { var exploreBlockContainerViewAppearance: ExploreBlockContainerView.Appearance { var appearance = ExploreBlockContainerView.Appearance() - appearance.backgroundColor = self.exploreBlockContainerViewBackgroundColor + appearance.background = .color(self.exploreBlockContainerViewBackgroundColor) return appearance } @@ -53,6 +54,8 @@ extension CourseListColorMode { return .stepikSecondaryBackground } return .stepikAccentFixed + case .clearLight: + return .clear } } } else { @@ -61,6 +64,8 @@ extension CourseListColorMode { return .white case .dark: return .stepikAccentFixed + case .clearLight: + return .clear } } } @@ -71,7 +76,7 @@ extension CourseListColorMode { var courseWidgetStatsViewAppearance: CourseWidgetStatsView.Appearance { switch self { - case .light, .grouped: + case .light, .grouped, .clearLight: return .init( imagesRenderingBackgroundColor: .stepikSystemSecondaryText, imagesRenderingTintColor: .stepikGreenFixed, @@ -95,7 +100,7 @@ extension CourseListColorMode { ) switch self { - case .light, .grouped: + case .light, .grouped, .clearLight: appearance.textColor = .stepikSystemPrimaryText case .dark: appearance.textColor = .white @@ -111,7 +116,7 @@ extension CourseListColorMode { ) switch self { - case .light, .grouped: + case .light, .grouped, .clearLight: appearance.textColor = .stepikSystemSecondaryText case .dark: appearance.textColor = UIColor.dynamic( @@ -125,7 +130,7 @@ extension CourseListColorMode { var courseWidgetBadgeTintColor: UIColor { switch self { - case .light, .grouped: + case .light, .grouped, .clearLight: return .stepikSystemSecondaryText case .dark: return .dynamic( @@ -137,7 +142,7 @@ extension CourseListColorMode { var courseWidgetBorderColor: UIColor { switch self { - case .light, .grouped: + case .light, .grouped, .clearLight: return .dynamic(light: .stepikGrey8Fixed, dark: .stepikSeparator) case .dark: if #available(iOS 13.0, *) { @@ -150,7 +155,7 @@ extension CourseListColorMode { var courseWidgetBackgroundColor: UIColor { switch self { - case .light: + case .light, .clearLight: return .dynamic(light: .white, dark: .stepikSecondaryBackground) case .grouped: return .stepikSecondaryGroupedBackground diff --git a/Stepic/Sources/Modules/CourseList/Views/CourseListView.swift b/Stepic/Sources/Modules/CourseList/Views/CourseListView.swift index 92ad0e7c4c..bebb19c857 100644 --- a/Stepic/Sources/Modules/CourseList/Views/CourseListView.swift +++ b/Stepic/Sources/Modules/CourseList/Views/CourseListView.swift @@ -150,6 +150,8 @@ class CourseListView: UIView { return self.appearance.darkModeBackgroundColor case .grouped: return self.appearance.groupedModeBackgroundColor + case .clearLight: + return .clear } } @@ -187,7 +189,7 @@ extension CourseListView: ProgrammaticallyInitializableViewProtocol { self.collectionView.translatesAutoresizingMaskIntoConstraints = false switch (self.colorMode, self.cardStyle) { - case (.light, .normal): + case (.light, .normal), (.clearLight, .normal): self.collectionView.register( LightCourseListCollectionViewCell.self, forCellWithReuseIdentifier: LightCourseListCollectionViewCell.defaultReuseIdentifier @@ -202,7 +204,7 @@ extension CourseListView: ProgrammaticallyInitializableViewProtocol { GroupedCourseListCollectionViewCell.self, forCellWithReuseIdentifier: GroupedCourseListCollectionViewCell.defaultReuseIdentifier ) - case (.light, .small): + case (.light, .small), (.clearLight, .small): self.collectionView.register( SmallLightCourseListCollectionViewCell.self, forCellWithReuseIdentifier: SmallLightCourseListCollectionViewCell.defaultReuseIdentifier diff --git a/Stepic/Sources/Modules/CourseList/Views/Widget/CourseWidgetStatsView.swift b/Stepic/Sources/Modules/CourseList/Views/Widget/CourseWidgetStatsView.swift index c0b25cffa3..f49125fff3 100644 --- a/Stepic/Sources/Modules/CourseList/Views/Widget/CourseWidgetStatsView.swift +++ b/Stepic/Sources/Modules/CourseList/Views/Widget/CourseWidgetStatsView.swift @@ -4,7 +4,7 @@ import UIKit extension CourseWidgetStatsView { struct Appearance { let statItemsSpacing: CGFloat = 8 - let leftInset: CGFloat = 2.0 + var leftInset: CGFloat = 2.0 let learnersViewImageViewSize = CGSize(width: 8, height: 10) let ratingViewImageViewSize = CGSize(width: 8, height: 12) @@ -134,6 +134,10 @@ final class CourseWidgetStatsView: UIView { fatalError("init(coder:) has not been implemented") } + func hideAllItems() { + self.stackView.arrangedSubviews.forEach { $0.isHidden = true } + } + func updateProgress(viewModel: CourseWidgetProgressViewModel) { let progressPie = ProgressCircleImage( progress: viewModel.progress, diff --git a/Stepic/Sources/Modules/CourseRevenueSubmodules/CourseRevenueTabMonthly/Views/Cell/CourseRevenueTabMonthlyTotalView.swift b/Stepic/Sources/Modules/CourseRevenueSubmodules/CourseRevenueTabMonthly/Views/Cell/CourseRevenueTabMonthlyTotalView.swift index 8e4bdf10d8..5b0a452918 100644 --- a/Stepic/Sources/Modules/CourseRevenueSubmodules/CourseRevenueTabMonthly/Views/Cell/CourseRevenueTabMonthlyTotalView.swift +++ b/Stepic/Sources/Modules/CourseRevenueSubmodules/CourseRevenueTabMonthly/Views/Cell/CourseRevenueTabMonthlyTotalView.swift @@ -140,7 +140,7 @@ extension CourseRevenueTabMonthlyTotalView: ProgrammaticallyInitializableViewPro self.layer.addSublayer(self.gradientLayer) self.updateStyle() - self.setRoundedCorners(cornerRadius: self.appearance.cornerRadius) + self.roundAllCorners(radius: self.appearance.cornerRadius) } func addSubviews() { diff --git a/Stepic/Sources/Modules/DebugMenuSubmodules/EditRemoteConfig/EditRemoteConfigValueViewController.swift b/Stepic/Sources/Modules/DebugMenuSubmodules/EditRemoteConfig/EditRemoteConfigValueViewController.swift index 7f648f9b4b..a3a854bb6b 100644 --- a/Stepic/Sources/Modules/DebugMenuSubmodules/EditRemoteConfig/EditRemoteConfigValueViewController.swift +++ b/Stepic/Sources/Modules/DebugMenuSubmodules/EditRemoteConfig/EditRemoteConfigValueViewController.swift @@ -98,8 +98,8 @@ final class EditRemoteConfigValueViewController: UIViewController { textView.font = self.appearance.textViewFont textView.textColor = self.appearance.textViewTextColor textView.textInsets = self.appearance.textViewTextInsets.edgeInsets - textView.setRoundedCorners( - cornerRadius: self.appearance.textViewBorderCornerRadius, + textView.roundAllCorners( + radius: self.appearance.textViewBorderCornerRadius, borderWidth: self.appearance.textViewBorderWidth, borderColor: self.appearance.textViewBorderColor ) diff --git a/Stepic/Sources/Modules/Explore/ExploreAssembly.swift b/Stepic/Sources/Modules/Explore/ExploreAssembly.swift index 0d583fbc18..b189d0de9f 100644 --- a/Stepic/Sources/Modules/Explore/ExploreAssembly.swift +++ b/Stepic/Sources/Modules/Explore/ExploreAssembly.swift @@ -7,10 +7,6 @@ class ExploreAssembly: Assembly { presenter: presenter, contentLanguageService: ContentLanguageService(), networkReachabilityService: NetworkReachabilityService(), - userAccountService: UserAccountService(), - personalOffersService: PersonalOffersService( - storageRecordsNetworkService: StorageRecordsNetworkService(storageRecordsAPI: StorageRecordsAPI()) - ), languageSwitchAvailabilityService: ContentLanguageSwitchAvailabilityService() ) let viewController = ExploreViewController(interactor: interactor, analytics: StepikAnalytics.shared) diff --git a/Stepic/Sources/Modules/Explore/ExploreDataFlow.swift b/Stepic/Sources/Modules/Explore/ExploreDataFlow.swift index 58d722fb05..3783c8bdf2 100644 --- a/Stepic/Sources/Modules/Explore/ExploreDataFlow.swift +++ b/Stepic/Sources/Modules/Explore/ExploreDataFlow.swift @@ -4,7 +4,6 @@ enum Explore { // MARK: Submodules identifiers enum Submodule: String, UniqueIdentifiable { - case stories case languageSwitch case catalogBlocks case visitedCourses @@ -40,19 +39,6 @@ enum Explore { } } - /// Update stories visibility - enum StoriesVisibilityUpdate { - @available(*, deprecated, message: "Should be refactored with VIP cycle as CheckLanguageSwitchAvailability") - struct Response { - let isHidden: Bool - } - - @available(*, deprecated, message: "Should be refactored with VIP cycle as CheckLanguageSwitchAvailability") - struct ViewModel { - let isHidden: Bool - } - } - // Refresh course block enum CourseListStateUpdate { enum State { @@ -71,17 +57,6 @@ enum Explore { } } - /// Update status bar style (called by stories module) - enum StatusBarStyleUpdate { - struct Response { - let statusBarStyle: UIStatusBarStyle - } - - struct ViewModel { - let statusBarStyle: UIStatusBarStyle - } - } - /// Start search for courses enum SearchCourses { struct ViewModel {} diff --git a/Stepic/Sources/Modules/Explore/ExploreInteractor.swift b/Stepic/Sources/Modules/Explore/ExploreInteractor.swift index 4fa1ad0bce..770e601d4b 100644 --- a/Stepic/Sources/Modules/Explore/ExploreInteractor.swift +++ b/Stepic/Sources/Modules/Explore/ExploreInteractor.swift @@ -12,8 +12,6 @@ protocol ExploreInteractorProtocol: BaseExploreInteractorProtocol { final class ExploreInteractor: BaseExploreInteractor, ExploreInteractorProtocol { private lazy var explorePresenter = self.presenter as? ExplorePresenterProtocol - private let userAccountService: UserAccountServiceProtocol - private let personalOffersService: PersonalOffersServiceProtocol private let contentLanguageSwitchAvailabilityService: ContentLanguageSwitchAvailabilityServiceProtocol private lazy var currentSearchResultsCourseListFilters = self.getDefaultSearchResultsCourseListFilters() @@ -22,12 +20,8 @@ final class ExploreInteractor: BaseExploreInteractor, ExploreInteractorProtocol presenter: ExplorePresenterProtocol, contentLanguageService: ContentLanguageServiceProtocol, networkReachabilityService: NetworkReachabilityServiceProtocol, - userAccountService: UserAccountServiceProtocol, - personalOffersService: PersonalOffersServiceProtocol, languageSwitchAvailabilityService: ContentLanguageSwitchAvailabilityServiceProtocol ) { - self.userAccountService = userAccountService - self.personalOffersService = personalOffersService self.contentLanguageSwitchAvailabilityService = languageSwitchAvailabilityService super.init( @@ -41,7 +35,6 @@ final class ExploreInteractor: BaseExploreInteractor, ExploreInteractorProtocol self.explorePresenter?.presentContent( response: .init(contentLanguage: self.contentLanguageService.globalContentLanguage) ) - self.syncPersonalOffers() } func doLanguageSwitchBlockLoad(request: Explore.LanguageSwitchAvailabilityCheck.Request) { @@ -117,29 +110,6 @@ final class ExploreInteractor: BaseExploreInteractor, ExploreInteractorProtocol } return nil } - - private func syncPersonalOffers() { - guard self.networkReachabilityService.isReachable else { - return - } - - guard let currentUser = self.userAccountService.currentUser, - !currentUser.isGuest && self.userAccountService.isAuthorized else { - return - } - - self.personalOffersService.syncPersonalOffers(userID: currentUser.id).cauterize() - } -} - -extension ExploreInteractor: StoriesOutputProtocol { - func hideStories() { - self.explorePresenter?.presentStoriesBlock(response: .init(isHidden: true)) - } - - func handleStoriesStatusBarStyleUpdate(_ statusBarStyle: UIStatusBarStyle) { - self.explorePresenter?.presentStatusBarStyle(response: .init(statusBarStyle: statusBarStyle)) - } } extension ExploreInteractor: CourseListFilterOutputProtocol { diff --git a/Stepic/Sources/Modules/Explore/ExplorePresenter.swift b/Stepic/Sources/Modules/Explore/ExplorePresenter.swift index 3f12c1f162..bba97b3a5d 100644 --- a/Stepic/Sources/Modules/Explore/ExplorePresenter.swift +++ b/Stepic/Sources/Modules/Explore/ExplorePresenter.swift @@ -3,9 +3,7 @@ import UIKit protocol ExplorePresenterProtocol: BaseExplorePresenterProtocol { func presentContent(response: Explore.ContentLoad.Response) func presentLanguageSwitchBlock(response: Explore.LanguageSwitchAvailabilityCheck.Response) - func presentStoriesBlock(response: Explore.StoriesVisibilityUpdate.Response) func presentCourseListState(response: Explore.CourseListStateUpdate.Response) - func presentStatusBarStyle(response: Explore.StatusBarStyleUpdate.Response) func presentExploreCourseListFilter(response: Explore.ExploreCourseListFilterPresentation.Response) func presentSearchResultsCourseListFilter(response: Explore.SearchResultsCourseListFilterPresentation.Response) func presentSearchResultsCourseListFiltersUpdateResult( @@ -28,12 +26,6 @@ final class ExplorePresenter: BaseExplorePresenter, ExplorePresenterProtocol { ) } - func presentStoriesBlock(response: Explore.StoriesVisibilityUpdate.Response) { - self.exploreViewController?.displayStoriesBlock( - viewModel: .init(isHidden: response.isHidden) - ) - } - func presentCourseListState(response: Explore.CourseListStateUpdate.Response) { self.exploreViewController?.displayModuleErrorState( viewModel: .init( @@ -43,10 +35,6 @@ final class ExplorePresenter: BaseExplorePresenter, ExplorePresenterProtocol { ) } - func presentStatusBarStyle(response: Explore.StatusBarStyleUpdate.Response) { - self.exploreViewController?.displayStatusBarStyle(viewModel: .init(statusBarStyle: response.statusBarStyle)) - } - func presentSearchResultsCourseListFilter(response: Explore.SearchResultsCourseListFilterPresentation.Response) { let presentationDescription = CourseListFilter.PresentationDescription( availableFilters: .all, diff --git a/Stepic/Sources/Modules/Explore/ExploreViewController.swift b/Stepic/Sources/Modules/Explore/ExploreViewController.swift index 6380e3b8bc..fdc6efc30c 100644 --- a/Stepic/Sources/Modules/Explore/ExploreViewController.swift +++ b/Stepic/Sources/Modules/Explore/ExploreViewController.swift @@ -4,9 +4,7 @@ import UIKit protocol ExploreViewControllerProtocol: BaseExploreViewControllerProtocol { func displayContent(viewModel: Explore.ContentLoad.ViewModel) func displayLanguageSwitchBlock(viewModel: Explore.LanguageSwitchAvailabilityCheck.ViewModel) - func displayStoriesBlock(viewModel: Explore.StoriesVisibilityUpdate.ViewModel) func displayModuleErrorState(viewModel: Explore.CourseListStateUpdate.ViewModel) - func displayStatusBarStyle(viewModel: Explore.StatusBarStyleUpdate.ViewModel) func displaySearchCourses(viewModel: Explore.SearchCourses.ViewModel) func displayExploreCourseListFilter(viewModel: Explore.ExploreCourseListFilterPresentation.ViewModel) func displaySearchResultsCourseListFilter(viewModel: Explore.SearchResultsCourseListFilterPresentation.ViewModel) @@ -22,7 +20,6 @@ final class ExploreViewController: BaseExploreViewController { } static let submodulesOrder: [Explore.Submodule] = [ - .stories, .languageSwitch, .catalogBlocks, .visitedCourses @@ -32,7 +29,6 @@ final class ExploreViewController: BaseExploreViewController { private lazy var exploreInteractor = self.interactor as? ExploreInteractorProtocol private var currentContentLanguage: ContentLanguage? - private var currentStoriesSubmoduleState = StoriesState.shown // SearchResults private var searchResultsModuleInput: SearchResultsModuleInputProtocol? private var searchResultsController: UIViewController? @@ -149,13 +145,6 @@ final class ExploreViewController: BaseExploreViewController { } private func initLanguageDependentSubmodules(contentLanguage: ContentLanguage) { - // Stories - let shouldRefreshStories = self.currentStoriesSubmoduleState == .shown - || (self.currentStoriesSubmoduleState == .hidden && self.currentContentLanguage != contentLanguage) - if shouldRefreshStories { - self.refreshStateForStories(state: .shown) - } - // Catalog blocks let catalogBlocksAssembly = CatalogBlocksAssembly( contentLanguage: contentLanguage, @@ -174,40 +163,6 @@ final class ExploreViewController: BaseExploreViewController { self.currentContentLanguage = contentLanguage } - // MARK: Stories - - private enum StoriesState { - case shown - case hidden - } - - private func refreshStateForStories(state: StoriesState) { - switch state { - case .shown: - let storiesAssembly = StoriesAssembly( - output: self.exploreInteractor as? StoriesOutputProtocol - ) - let storiesViewController = storiesAssembly.makeModule() - let storiesContainerView = ExploreStoriesContainerView( - contentView: storiesViewController.view - ) - self.registerSubmodule( - .init( - viewController: storiesViewController, - view: storiesContainerView, - isLanguageDependent: true, - type: Explore.Submodule.stories - ) - ) - case .hidden: - if let submodule = self.getSubmodule(type: Explore.Submodule.stories) { - self.removeSubmodule(submodule) - } - } - - self.currentStoriesSubmoduleState = state - } - // MARK: - Visited courses submodule private enum VisitedCourseListState { @@ -353,10 +308,6 @@ extension ExploreViewController: ExploreViewControllerProtocol { ) } - func displayStoriesBlock(viewModel: Explore.StoriesVisibilityUpdate.ViewModel) { - self.refreshStateForStories(state: viewModel.isHidden ? .hidden : .shown) - } - func displayModuleErrorState(viewModel: Explore.CourseListStateUpdate.ViewModel) { switch viewModel.module { case .visitedCourses: @@ -370,10 +321,6 @@ extension ExploreViewController: ExploreViewControllerProtocol { } } - func displayStatusBarStyle(viewModel: Explore.StatusBarStyleUpdate.ViewModel) { - self.styledNavigationController?.changeStatusBarStyle(viewModel.statusBarStyle, sender: self) - } - func displaySearchCourses(viewModel: Explore.SearchCourses.ViewModel) { self.searchBar.becomeFirstResponder() self.searchBarTextDidBeginEditing(self.searchBar) diff --git a/Stepic/Sources/Modules/Explore/View/CourseListContainerViewFactory.swift b/Stepic/Sources/Modules/Explore/View/CourseListContainerViewFactory.swift index 635f5a61b5..d8fe504d1f 100644 --- a/Stepic/Sources/Modules/Explore/View/CourseListContainerViewFactory.swift +++ b/Stepic/Sources/Modules/Explore/View/CourseListContainerViewFactory.swift @@ -1,6 +1,10 @@ import UIKit final class CourseListContainerViewFactory { + struct HorizontalContainerDescription { + let background: ExploreBlockContainerView.Appearance.Background + } + struct HorizontalHeaderDescription { var title: String? var summary: String? @@ -80,6 +84,7 @@ final class CourseListContainerViewFactory { func makeHorizontalContainerView( for contentView: UIView, + containerDescription: HorizontalContainerDescription? = nil, headerDescription: HorizontalHeaderDescription, headerViewInsets: UIEdgeInsets? = nil, contentViewInsets: UIEdgeInsets? = Appearance.horizontalContentInsets @@ -94,6 +99,7 @@ final class CourseListContainerViewFactory { return self.makeHorizontalContainerView( headerView: headerView, contentView: contentView, + containerDescription: containerDescription, headerViewInsets: headerViewInsets, contentViewInsets: contentViewInsets ) @@ -101,6 +107,7 @@ final class CourseListContainerViewFactory { func makeHorizontalCoursesCollectionContainerView( for contentView: UIView, + containerDescription: HorizontalContainerDescription? = nil, headerDescription: HorizontalCoursesCollectionHeaderDescription, headerViewInsets: UIEdgeInsets? = nil, contentViewInsets: UIEdgeInsets? = Appearance.horizontalContentInsets @@ -115,6 +122,7 @@ final class CourseListContainerViewFactory { return self.makeHorizontalContainerView( headerView: headerView, contentView: contentView, + containerDescription: containerDescription, headerViewInsets: headerViewInsets, contentViewInsets: contentViewInsets ) @@ -157,11 +165,16 @@ final class CourseListContainerViewFactory { private func makeHorizontalContainerView( headerView: UIView & ExploreBlockHeaderViewProtocol, contentView: UIView, + containerDescription: HorizontalContainerDescription?, headerViewInsets: UIEdgeInsets?, contentViewInsets: UIEdgeInsets? ) -> ExploreBlockContainerView { var appearance = self.colorMode.exploreBlockContainerViewAppearance + if let containerDescription = containerDescription { + appearance.background = containerDescription.background + } + if let headerViewInsets = headerViewInsets { appearance.headerViewInsets = headerViewInsets } diff --git a/Stepic/Sources/Modules/Explore/View/ExploreBlockContainerView/ExploreBlockContainerView.swift b/Stepic/Sources/Modules/Explore/View/ExploreBlockContainerView/ExploreBlockContainerView.swift index 71245120ac..39b1084219 100644 --- a/Stepic/Sources/Modules/Explore/View/ExploreBlockContainerView/ExploreBlockContainerView.swift +++ b/Stepic/Sources/Modules/Explore/View/ExploreBlockContainerView/ExploreBlockContainerView.swift @@ -4,11 +4,18 @@ import UIKit extension ExploreBlockContainerView { struct Appearance { let separatorColor = UIColor.stepikSeparator - var backgroundColor = UIColor.stepikBackground + var background = Background.default var headerViewInsets = UIEdgeInsets(top: 20, left: 20, bottom: 0, right: 20) var contentViewInsets = UIEdgeInsets(top: 20, left: 0, bottom: 0, right: 0) let separatorViewInsets = UIEdgeInsets(top: 20, left: 0, bottom: 0, right: 0) + + enum Background { + case color(UIColor) + case image(UIImage?) + + static var `default`: Background { .color(.stepikBackground) } + } } } @@ -18,6 +25,18 @@ final class ExploreBlockContainerView: UIView { private let contentView: UIView private let shouldShowSeparator: Bool + private lazy var backgroundImageView: UIImageView = { + let image: UIImage? = { + if case .image(let backgroundImage) = self.appearance.background { + return backgroundImage + } + return nil + }() + let imageView = UIImageView(image: image) + imageView.contentMode = .scaleAspectFill + return imageView + }() + private lazy var separatorView: UIView = { let view = UIView() view.backgroundColor = self.appearance.separatorColor @@ -69,33 +88,36 @@ final class ExploreBlockContainerView: UIView { super.layoutSubviews() self.invalidateIntrinsicContentSize() } - - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - - self.performBlockIfAppearanceChanged(from: previousTraitCollection) { - self.updateViewColor() - } - } - - private func updateViewColor() { - self.backgroundColor = self.appearance.backgroundColor - } } extension ExploreBlockContainerView: ProgrammaticallyInitializableViewProtocol { func setupView() { self.contentView.clipsToBounds = false - self.updateViewColor() + + if case .color(let backgroundColor) = self.appearance.background { + self.backgroundColor = backgroundColor + } } func addSubviews() { + if case .image = self.appearance.background { + self.addSubview(self.backgroundImageView) + } + self.addSubview(self.headerView) self.addSubview(self.contentView) self.addSubview(self.separatorView) } func makeConstraints() { + if case .image = self.appearance.background { + self.backgroundImageView.translatesAutoresizingMaskIntoConstraints = false + self.backgroundImageView.snp.makeConstraints { make in + make.top.leading.trailing.equalToSuperview() + make.bottom.equalToSuperview().priority(.low) + } + } + self.headerView.translatesAutoresizingMaskIntoConstraints = false self.headerView.snp.makeConstraints { make in make.top.equalToSuperview().offset(self.appearance.headerViewInsets.top) diff --git a/Stepic/Sources/Modules/Explore/View/ExploreBlockPlaceholderView.swift b/Stepic/Sources/Modules/Explore/View/ExploreBlockPlaceholderView/ExploreBlockPlaceholderView.swift similarity index 100% rename from Stepic/Sources/Modules/Explore/View/ExploreBlockPlaceholderView.swift rename to Stepic/Sources/Modules/Explore/View/ExploreBlockPlaceholderView/ExploreBlockPlaceholderView.swift diff --git a/Stepic/Sources/Modules/Explore/View/ExploreBlockPlaceholderView/ExplorePlaceholderActionButton.swift b/Stepic/Sources/Modules/Explore/View/ExploreBlockPlaceholderView/ExplorePlaceholderActionButton.swift new file mode 100644 index 0000000000..b90d39329b --- /dev/null +++ b/Stepic/Sources/Modules/Explore/View/ExploreBlockPlaceholderView/ExplorePlaceholderActionButton.swift @@ -0,0 +1,117 @@ +import SnapKit +import UIKit + +extension ExplorePlaceholderActionButton { + struct Appearance { + let imageSize = CGSize(width: 20, height: 20) + let insets = LayoutInsets(inset: 12) + + let tintColor = UIColor.stepikVioletFixed + let font = Typography.bodyFont + + let cornerRadius: CGFloat = 8 + let borderWidth: CGFloat = 1 + let borderColor = UIColor(hex6: 0x6C7BDF).withAlphaComponent(0.12) + } +} + +final class ExplorePlaceholderActionButton: UIControl { + let appearance: Appearance + + private lazy var titleLabel: UILabel = { + let label = UILabel() + label.font = self.appearance.font + label.textColor = self.appearance.tintColor + label.textAlignment = .center + label.numberOfLines = 1 + return label + }() + + private lazy var imageView: UIImageView = { + let imageView = UIImageView() + imageView.tintColor = self.appearance.tintColor + imageView.contentMode = .scaleAspectFit + imageView.isHidden = true + return imageView + }() + + var title: String? { + didSet { + self.titleLabel.text = self.title + } + } + + var image: UIImage? { + didSet { + self.imageView.image = self.image + self.imageView.isHidden = self.image == nil + } + } + + override var isHighlighted: Bool { + didSet { + self.titleLabel.alpha = self.isHighlighted ? 0.5 : 1.0 + self.imageView.alpha = self.isHighlighted ? 0.5 : 1.0 + } + } + + override var intrinsicContentSize: CGSize { + let imageWidthWithInsets = self.appearance.insets.left + self.appearance.imageSize.width + let width = imageWidthWithInsets + + self.appearance.insets.left + + self.titleLabel.intrinsicContentSize.width + + self.appearance.insets.right + + let height = max( + self.appearance.imageSize.height, + self.titleLabel.intrinsicContentSize.height + ) + self.appearance.insets.top + self.appearance.insets.bottom + + return CGSize(width: width, 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") + } +} + +extension ExplorePlaceholderActionButton: ProgrammaticallyInitializableViewProtocol { + func setupView() { + self.layer.cornerRadius = self.appearance.cornerRadius + self.layer.borderWidth = self.appearance.borderWidth + self.layer.borderColor = self.appearance.borderColor.cgColor + self.clipsToBounds = true + } + + func addSubviews() { + self.addSubview(self.imageView) + self.addSubview(self.titleLabel) + } + + func makeConstraints() { + self.imageView.translatesAutoresizingMaskIntoConstraints = false + self.imageView.snp.makeConstraints { make in + make.leading.equalToSuperview().offset(self.appearance.insets.left) + make.size.equalTo(self.appearance.imageSize) + make.centerY.equalToSuperview() + } + + self.titleLabel.translatesAutoresizingMaskIntoConstraints = false + self.titleLabel.snp.makeConstraints { make in + make.centerY.centerX.equalToSuperview() + } + } +} diff --git a/Stepic/Sources/Modules/Explore/View/ExploreBlockPlaceholderView/ExplorePlaceholderView.swift b/Stepic/Sources/Modules/Explore/View/ExploreBlockPlaceholderView/ExplorePlaceholderView.swift new file mode 100644 index 0000000000..26f70bb164 --- /dev/null +++ b/Stepic/Sources/Modules/Explore/View/ExploreBlockPlaceholderView/ExplorePlaceholderView.swift @@ -0,0 +1,119 @@ +import SnapKit +import UIKit + +extension ExplorePlaceholderView { + struct Appearance { + let titleFont: UIFont + let titleTextColor: UIColor + let titleTextAlignment: NSTextAlignment + + let actionButtonHeight: CGFloat = 44 + + let insets = LayoutInsets.default + } +} + +final class ExplorePlaceholderView: UIView { + let appearance: Appearance + + private lazy var titleLabel: UILabel = { + let label = UILabel() + label.font = self.appearance.titleFont + label.textColor = self.appearance.titleTextColor + label.textAlignment = self.appearance.titleTextAlignment + label.numberOfLines = 0 + return label + }() + + private lazy var actionButton: ExplorePlaceholderActionButton = { + let button = ExplorePlaceholderActionButton() + button.addTarget(self, action: #selector(self.actionButtonClicked), for: .touchUpInside) + return button + }() + + private var actionButtonWidthToSuperviewConstraint: Constraint? + + var title: String? { + didSet { + self.titleLabel.text = self.title + } + } + + var buttonTitle: String? { + didSet { + self.actionButton.title = self.buttonTitle + } + } + + var buttonImage: UIImage? { + didSet { + self.actionButton.image = self.buttonImage + + if self.buttonImage != nil { + self.actionButtonWidthToSuperviewConstraint?.activate() + } else { + self.actionButtonWidthToSuperviewConstraint?.deactivate() + } + } + } + + var onActionButtonClick: (() -> Void)? { + didSet { + self.actionButton.isEnabled = self.onActionButtonClick != nil + } + } + + override var intrinsicContentSize: CGSize { + let height = self.titleLabel.intrinsicContentSize.height + + self.appearance.insets.top + + self.appearance.actionButtonHeight + return CGSize(width: UIView.noIntrinsicMetric, height: height) + } + + init(frame: CGRect = .zero, appearance: Appearance) { + self.appearance = appearance + super.init(frame: frame) + + self.addSubviews() + self.makeConstraints() + } + + @available(*, unavailable) + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + self.invalidateIntrinsicContentSize() + } + + @objc + private func actionButtonClicked() { + self.onActionButtonClick?() + } +} + +extension ExplorePlaceholderView: ProgrammaticallyInitializableViewProtocol { + func addSubviews() { + self.addSubview(self.titleLabel) + self.addSubview(self.actionButton) + } + + func makeConstraints() { + self.titleLabel.translatesAutoresizingMaskIntoConstraints = false + self.titleLabel.snp.makeConstraints { make in + make.top.leading.trailing.equalToSuperview() + } + + self.actionButton.translatesAutoresizingMaskIntoConstraints = false + self.actionButton.snp.makeConstraints { make in + make.top.equalTo(self.titleLabel.snp.bottom).offset(self.appearance.insets.top) + make.bottom.centerX.equalToSuperview() + make.height.equalTo(self.appearance.actionButtonHeight) + + self.actionButtonWidthToSuperviewConstraint = make.width.equalToSuperview().constraint + self.actionButtonWidthToSuperviewConstraint?.deactivate() + } + } +} diff --git a/Stepic/Sources/Modules/Explore/View/ExploreBlockPlaceholderView/NewExploreBlockPlaceholderView.swift b/Stepic/Sources/Modules/Explore/View/ExploreBlockPlaceholderView/NewExploreBlockPlaceholderView.swift new file mode 100644 index 0000000000..f48a178282 --- /dev/null +++ b/Stepic/Sources/Modules/Explore/View/ExploreBlockPlaceholderView/NewExploreBlockPlaceholderView.swift @@ -0,0 +1,130 @@ +import SnapKit +import UIKit + +extension NewExploreBlockPlaceholderView { + struct Appearance { + let insets = UIEdgeInsets(top: 20, left: 20, bottom: 12, right: 20) + } +} + +final class NewExploreBlockPlaceholderView: UIView { + let appearance: Appearance + private let placeholderStyle: PlaceholderStyle + + private lazy var placeholderView: ExplorePlaceholderView = { + let view = ExplorePlaceholderView(appearance: self.placeholderStyle.appearance) + view.title = self.placeholderStyle.title + view.buttonTitle = self.placeholderStyle.actionButtonTitle + view.buttonImage = self.placeholderStyle.actionButtonImage + return view + }() + + var onActionButtonClick: (() -> Void)? { + get { + self.placeholderView.onActionButtonClick + } + set { + self.placeholderView.onActionButtonClick = newValue + } + } + + override var intrinsicContentSize: CGSize { + let height = self.appearance.insets.top + + self.placeholderView.intrinsicContentSize.height + + self.appearance.insets.bottom + return CGSize(width: UIView.noIntrinsicMetric, height: height) + } + + init( + frame: CGRect = .zero, + placeholderStyle: PlaceholderStyle, + appearance: Appearance = Appearance() + ) { + self.placeholderStyle = placeholderStyle + self.appearance = appearance + super.init(frame: frame) + + self.addSubviews() + self.makeConstraints() + } + + @available(*, unavailable) + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + self.invalidateIntrinsicContentSize() + } + + enum PlaceholderStyle { + case enrolledEmpty + case error + case anonymous + + fileprivate var title: String { + switch self { + case .enrolledEmpty: + return NSLocalizedString("NewHomePlaceholderEmptyEnrolledTitle", comment: "") + case .error: + return NSLocalizedString("NewHomePlaceholderErrorTitle", comment: "") + case .anonymous: + return NSLocalizedString("NewHomePlaceholderAnonymousTitle", comment: "") + } + } + + fileprivate var actionButtonTitle: String { + switch self { + case .enrolledEmpty: + return NSLocalizedString("NewHomePlaceholderEmptyEnrolledButtonTitle", comment: "") + case .error: + return NSLocalizedString("NewHomePlaceholderErrorButtonTitle", comment: "") + case .anonymous: + return NSLocalizedString("NewHomePlaceholderAnonymousButtonTitle", comment: "") + } + } + + fileprivate var actionButtonImage: UIImage? { + switch self { + case .enrolledEmpty: + return UIImage(named: "plus")?.withRenderingMode(.alwaysTemplate) + case .error, .anonymous: + return nil + } + } + + fileprivate var appearance: ExplorePlaceholderView.Appearance { + switch self { + case .enrolledEmpty: + return .init( + titleFont: Typography.title1Font, + titleTextColor: UIColor.stepikVioletFixed.withAlphaComponent(0.38), + titleTextAlignment: .left + ) + case .error, .anonymous: + return .init( + titleFont: Typography.bodyFont, + titleTextColor: UIColor.stepikMaterialSecondaryText, + titleTextAlignment: .center + ) + } + } + } +} + +extension NewExploreBlockPlaceholderView: ProgrammaticallyInitializableViewProtocol { + func addSubviews() { + self.addSubview(self.placeholderView) + } + + func makeConstraints() { + self.placeholderView.translatesAutoresizingMaskIntoConstraints = false + self.placeholderView.snp.makeConstraints { make in + make.top.equalToSuperview().offset(self.appearance.insets.top) + make.leading.equalToSuperview().offset(self.appearance.insets.left) + make.bottom.equalToSuperview().offset(-self.appearance.insets.bottom) + make.trailing.equalToSuperview().offset(-self.appearance.insets.right) + } + } +} diff --git a/Stepic/Sources/Modules/Explore/View/ExploreStoriesContainerView.swift b/Stepic/Sources/Modules/Explore/View/ExploreStoriesContainerView.swift index 8d5baec07c..85763ec2f6 100644 --- a/Stepic/Sources/Modules/Explore/View/ExploreStoriesContainerView.swift +++ b/Stepic/Sources/Modules/Explore/View/ExploreStoriesContainerView.swift @@ -4,7 +4,7 @@ import UIKit extension ExploreStoriesContainerView { struct Appearance { let storiesViewHeight: CGFloat = 98 - let storiesViewInsets = UIEdgeInsets(top: 16, left: 0, bottom: 0, right: 0) + let storiesViewInsets = UIEdgeInsets(top: 16, left: 0, bottom: 16, right: 0) } } @@ -50,7 +50,7 @@ extension ExploreStoriesContainerView: ProgrammaticallyInitializableViewProtocol self.contentView.translatesAutoresizingMaskIntoConstraints = false self.contentView.snp.makeConstraints { make in make.top.equalToSuperview().offset(self.appearance.storiesViewInsets.top) - make.bottom.equalToSuperview() + make.bottom.equalToSuperview().offset(-self.appearance.storiesViewInsets.bottom) make.leading.trailing.equalToSuperview() make.height.equalTo(self.appearance.storiesViewHeight) } diff --git a/Stepic/Sources/Modules/ExploreSubmodules/ContinueCourse/ContinueCourseAssembly.swift b/Stepic/Sources/Modules/ExploreSubmodules/ContinueCourse/ContinueCourseAssembly.swift index bf4bb23898..3cd1727d2e 100644 --- a/Stepic/Sources/Modules/ExploreSubmodules/ContinueCourse/ContinueCourseAssembly.swift +++ b/Stepic/Sources/Modules/ExploreSubmodules/ContinueCourse/ContinueCourseAssembly.swift @@ -9,11 +9,9 @@ final class ContinueCourseAssembly: Assembly { func makeModule() -> UIViewController { let provider = ContinueCourseProvider( - userCoursesAPI: UserCoursesAPI(), - coursesAPI: CoursesAPI(), - progressesNetworkService: ProgressesNetworkService( - progressesAPI: ProgressesAPI() - ) + userCoursesNetworkService: UserCoursesNetworkService(userCoursesAPI: UserCoursesAPI()), + coursesNetworkService: CoursesNetworkService(coursesAPI: CoursesAPI()), + progressesNetworkService: ProgressesNetworkService(progressesAPI: ProgressesAPI()) ) let presenter = ContinueCoursePresenter() diff --git a/Stepic/Sources/Modules/ExploreSubmodules/ContinueCourse/ContinueCourseDataFlow.swift b/Stepic/Sources/Modules/ExploreSubmodules/ContinueCourse/ContinueCourseDataFlow.swift index 7eebcc3e09..3bcaa02e29 100644 --- a/Stepic/Sources/Modules/ExploreSubmodules/ContinueCourse/ContinueCourseDataFlow.swift +++ b/Stepic/Sources/Modules/ExploreSubmodules/ContinueCourse/ContinueCourseDataFlow.swift @@ -8,7 +8,7 @@ enum ContinueCourse { struct Request {} struct Response { - let result: Course + let result: StepikResult } struct ViewModel { @@ -21,6 +21,11 @@ enum ContinueCourse { struct Request {} } + /// Go to catalog + enum ContinueCourseEmptyAction { + struct Request {} + } + /// Check for tooltip enum TooltipAvailabilityCheck { struct Request {} @@ -58,6 +63,7 @@ enum ContinueCourse { enum ViewControllerState { case loading + case empty case result(data: ContinueCourseViewModel) } } diff --git a/Stepic/Sources/Modules/ExploreSubmodules/ContinueCourse/ContinueCourseInteractor.swift b/Stepic/Sources/Modules/ExploreSubmodules/ContinueCourse/ContinueCourseInteractor.swift index 29af0e86df..3b2d7ee8f9 100644 --- a/Stepic/Sources/Modules/ExploreSubmodules/ContinueCourse/ContinueCourseInteractor.swift +++ b/Stepic/Sources/Modules/ExploreSubmodules/ContinueCourse/ContinueCourseInteractor.swift @@ -4,6 +4,7 @@ import PromiseKit protocol ContinueCourseInteractorProtocol { func doLastCourseRefresh(request: ContinueCourse.LastCourseLoad.Request) func doContinueLastCourseAction(request: ContinueCourse.ContinueCourseAction.Request) + func doContinueCourseEmptyAction(request: ContinueCourse.ContinueCourseEmptyAction.Request) func doTooltipAvailabilityCheck(request: ContinueCourse.TooltipAvailabilityCheck.Request) func doSiriButtonAvailabilityCheck(request: ContinueCourse.SiriButtonAvailabilityCheck.Request) func doSiriButtonAction(request: ContinueCourse.SiriButtonAction.Request) @@ -50,12 +51,14 @@ final class ContinueCourseInteractor: ContinueCourseInteractorProtocol { self.provider.fetchLastCourse().done { course in if let course = course { self.currentCourse = course - self.presenter.presentLastCourse(response: .init(result: course)) + self.presenter.presentLastCourse(response: .init(result: .success(course))) } else { - self.moduleOutput?.hideContinueCourse() + self.presenter.presentLastCourse(response: .init(result: .failure(Error.noLastCourse))) } }.catch { _ in - self.moduleOutput?.hideContinueCourse() + if self.currentCourse == nil { + self.moduleOutput?.hideContinueCourse() + } } } @@ -82,6 +85,10 @@ final class ContinueCourseInteractor: ContinueCourseInteractorProtocol { } } + func doContinueCourseEmptyAction(request: ContinueCourse.ContinueCourseEmptyAction.Request) { + self.moduleOutput?.presentCatalog() + } + func doTooltipAvailabilityCheck(request: ContinueCourse.TooltipAvailabilityCheck.Request) { self.presenter.presentTooltip( response: .init( @@ -106,6 +113,10 @@ final class ContinueCourseInteractor: ContinueCourseInteractorProtocol { self.siriShortcutsStorageManager.didClickAddToSiriOnHomeWidget = true } } + + enum Error: Swift.Error { + case noLastCourse + } } extension ContinueCourseInteractor: DataBackUpdateServiceDelegate { @@ -120,7 +131,7 @@ extension ContinueCourseInteractor: DataBackUpdateServiceDelegate { } self.currentCourse = course - self.presenter.presentLastCourse(response: .init(result: course)) + self.presenter.presentLastCourse(response: .init(result: .success(course))) } func dataBackUpdateService( diff --git a/Stepic/Sources/Modules/ExploreSubmodules/ContinueCourse/ContinueCourseOutputProtocol.swift b/Stepic/Sources/Modules/ExploreSubmodules/ContinueCourse/ContinueCourseOutputProtocol.swift index d420a84055..af89a0cf3f 100644 --- a/Stepic/Sources/Modules/ExploreSubmodules/ContinueCourse/ContinueCourseOutputProtocol.swift +++ b/Stepic/Sources/Modules/ExploreSubmodules/ContinueCourse/ContinueCourseOutputProtocol.swift @@ -8,4 +8,5 @@ protocol ContinueCourseOutputProtocol: AnyObject { source: AnalyticsEvent.CourseContinueSource, viewSource: AnalyticsEvent.CourseViewSource ) + func presentCatalog() } diff --git a/Stepic/Sources/Modules/ExploreSubmodules/ContinueCourse/ContinueCoursePresenter.swift b/Stepic/Sources/Modules/ExploreSubmodules/ContinueCourse/ContinueCoursePresenter.swift index 480bd7ebd6..45544b7603 100644 --- a/Stepic/Sources/Modules/ExploreSubmodules/ContinueCourse/ContinueCoursePresenter.swift +++ b/Stepic/Sources/Modules/ExploreSubmodules/ContinueCourse/ContinueCoursePresenter.swift @@ -10,13 +10,15 @@ final class ContinueCoursePresenter: ContinueCoursePresenterProtocol { weak var viewController: ContinueCourseViewControllerProtocol? func presentLastCourse(response: ContinueCourse.LastCourseLoad.Response) { - var viewModel: ContinueCourse.LastCourseLoad.ViewModel - - viewModel = ContinueCourse.LastCourseLoad.ViewModel( - state: .result(data: self.makeViewModel(course: response.result)) - ) - - self.viewController?.displayLastCourse(viewModel: viewModel) + switch response.result { + case .success(let course): + let viewModel = self.makeViewModel(course: course) + self.viewController?.displayLastCourse(viewModel: .init(state: .result(data: viewModel))) + case .failure(let error): + if case ContinueCourseInteractor.Error.noLastCourse = error { + self.viewController?.displayLastCourse(viewModel: .init(state: .empty)) + } + } } func presentTooltip(response: ContinueCourse.TooltipAvailabilityCheck.Response) { diff --git a/Stepic/Sources/Modules/ExploreSubmodules/ContinueCourse/ContinueCourseProvider.swift b/Stepic/Sources/Modules/ExploreSubmodules/ContinueCourse/ContinueCourseProvider.swift index 84f4ec27ee..a890e395d7 100644 --- a/Stepic/Sources/Modules/ExploreSubmodules/ContinueCourse/ContinueCourseProvider.swift +++ b/Stepic/Sources/Modules/ExploreSubmodules/ContinueCourse/ContinueCourseProvider.swift @@ -6,49 +6,44 @@ protocol ContinueCourseProviderProtocol { } final class ContinueCourseProvider: ContinueCourseProviderProtocol { - private let userCoursesAPI: UserCoursesAPI - private let coursesAPI: CoursesAPI + private let userCoursesNetworkService: UserCoursesNetworkServiceProtocol + private let coursesNetworkService: CoursesNetworkServiceProtocol private let progressesNetworkService: ProgressesNetworkServiceProtocol init( - userCoursesAPI: UserCoursesAPI, - coursesAPI: CoursesAPI, + userCoursesNetworkService: UserCoursesNetworkServiceProtocol, + coursesNetworkService: CoursesNetworkServiceProtocol, progressesNetworkService: ProgressesNetworkServiceProtocol ) { - self.userCoursesAPI = userCoursesAPI - self.coursesAPI = coursesAPI + self.userCoursesNetworkService = userCoursesNetworkService + self.coursesNetworkService = coursesNetworkService self.progressesNetworkService = progressesNetworkService } func fetchLastCourse() -> Promise { - Promise { seal in - self.userCoursesAPI.retrieve(page: 1).then { result -> Promise<[Course]> in - let lastCourse = result.0 - .sorted(by: { $0.lastViewed > $1.lastViewed }) - .prefix(1) - // [] or [id] - let coursesIDs = lastCourse.compactMap { $0.courseID } - return self.coursesAPI.retrieve(ids: coursesIDs) - }.then { courses -> Promise<(Course?, Progress?)> in - if let course = courses.first, - let progressID = course.progressID { - return self.progressesNetworkService - .fetch(id: progressID) - .map { (course, $0) } - } else { - return Promise.value((nil, nil)) - } - }.done { course, progress in - course?.progress = progress + self.userCoursesNetworkService.fetch().then { userCoursesFetchResult -> Promise<[Course]> in + let lastCourse = userCoursesFetchResult.0 + .sorted(by: { $0.lastViewed > $1.lastViewed }) + .prefix(1) + // [] or [id] + let coursesIDs = lastCourse.compactMap { $0.courseID } + return self.coursesNetworkService.fetch(ids: coursesIDs) + }.then { courses -> Promise<(Course?, Progress?)> in + if let course = courses.first, + let progressID = course.progressID { + return self.progressesNetworkService + .fetch(id: progressID) + .map { (course, $0) } + } else { + return .value((nil, nil)) + } + }.then { course, progress -> Promise in + if let course = course { + course.progress = progress CoreDataHelper.shared.save() - seal.fulfill(course) - }.catch { error in - seal.reject(error) } - } - } - enum Error: Swift.Error { - case lastCourseFetchFailed + return .value(course) + } } } diff --git a/Stepic/Sources/Modules/ExploreSubmodules/ContinueCourse/ContinueCourseViewController.swift b/Stepic/Sources/Modules/ExploreSubmodules/ContinueCourse/ContinueCourseViewController.swift index 3c41085665..b3f50ed374 100644 --- a/Stepic/Sources/Modules/ExploreSubmodules/ContinueCourse/ContinueCourseViewController.swift +++ b/Stepic/Sources/Modules/ExploreSubmodules/ContinueCourse/ContinueCourseViewController.swift @@ -9,11 +9,7 @@ protocol ContinueCourseViewControllerProtocol: AnyObject { final class ContinueCourseViewController: UIViewController { private let interactor: ContinueCourseInteractorProtocol - private var state: ContinueCourse.ViewControllerState { - didSet { - self.updateState() - } - } + private var state: ContinueCourse.ViewControllerState lazy var continueCourseView = self.view as? ContinueCourseView private lazy var continueLearningTooltip = TooltipFactory.continueLearningWidget @@ -36,25 +32,38 @@ final class ContinueCourseViewController: UIViewController { // MARK: ViewController lifecycle override func loadView() { - let view = ContinueCourseView( - frame: UIScreen.main.bounds - ) + let view = ContinueCourseView(frame: UIScreen.main.bounds) view.delegate = self self.view = view } override func viewDidLoad() { super.viewDidLoad() + self.updateState(newState: self.state) + } - self.updateState() + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) self.interactor.doLastCourseRefresh(request: .init()) } - private func updateState() { - if case .loading = self.state { + private func updateState(newState: ContinueCourse.ViewControllerState) { + defer { + self.state = newState + } + + self.continueCourseView?.hideLoading() + self.continueCourseView?.hideEmpty() + + switch newState { + case .loading: self.continueCourseView?.showLoading() - } else { - self.continueCourseView?.hideLoading() + case .empty: + self.continueCourseView?.showEmpty() + case .result(let viewModel): + self.continueCourseView?.configure(viewModel: viewModel) + self.interactor.doTooltipAvailabilityCheck(request: .init()) + self.interactor.doSiriButtonAvailabilityCheck(request: .init()) } } } @@ -63,17 +72,12 @@ final class ContinueCourseViewController: UIViewController { extension ContinueCourseViewController: ContinueCourseViewControllerProtocol { func displayLastCourse(viewModel: ContinueCourse.LastCourseLoad.ViewModel) { - if case .result(let result) = viewModel.state { - self.continueCourseView?.configure(viewModel: result) - self.interactor.doTooltipAvailabilityCheck(request: .init()) - self.interactor.doSiriButtonAvailabilityCheck(request: .init()) - } - - self.state = viewModel.state + self.updateState(newState: viewModel.state) } func displayTooltip(viewModel: ContinueCourse.TooltipAvailabilityCheck.ViewModel) { - guard let continueCourseView = self.continueCourseView else { + guard let continueCourseView = self.continueCourseView, + let parentView = self.parent?.view else { return } @@ -83,8 +87,8 @@ extension ContinueCourseViewController: ContinueCourseViewControllerProtocol { continueCourseView.setNeedsLayout() continueCourseView.layoutIfNeeded() self?.continueLearningTooltip.show( - direction: .up, - in: continueCourseView, + direction: .down, + in: parentView, from: continueCourseView.tooltipAnchorView ) } @@ -109,10 +113,14 @@ extension ContinueCourseViewController: ContinueCourseViewControllerProtocol { // MARK: - ContinueCourseViewController: ContinueCourseViewDelegate - extension ContinueCourseViewController: ContinueCourseViewDelegate { - func continueCourseContinueButtonDidClick(_ continueCourseView: ContinueCourseView) { + func continueCourseDidClickContinue(_ continueCourseView: ContinueCourseView) { self.interactor.doContinueLastCourseAction(request: .init()) } + func continueCourseDidClickEmpty(_ continueCourseView: ContinueCourseView) { + self.interactor.doContinueCourseEmptyAction(request: .init()) + } + func continueCourseSiriButtonDidClick(_ continueCourseView: ContinueCourseView) { self.interactor.doSiriButtonAction(request: .init()) } diff --git a/Stepic/Sources/Modules/ExploreSubmodules/ContinueCourse/Views/ContinueCourseBackgroundView.swift b/Stepic/Sources/Modules/ExploreSubmodules/ContinueCourse/Views/ContinueCourseBackgroundView.swift new file mode 100644 index 0000000000..c92fb9b2be --- /dev/null +++ b/Stepic/Sources/Modules/ExploreSubmodules/ContinueCourse/Views/ContinueCourseBackgroundView.swift @@ -0,0 +1,63 @@ +import SnapKit +import UIKit + +extension ContinueCourseBackgroundView { + struct Appearance { + let backgroundColor = UIColor.stepikSecondaryBackground + } +} + +final class ContinueCourseBackgroundView: UIView { + let appearance: Appearance + + private lazy var imageView: UIImageView = { + let imageView = UIImageView(image: UIImage(named: "continue_learning_gradient")) + imageView.contentMode = .scaleAspectFill + return imageView + }() + + 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 traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + + self.performBlockIfAppearanceChanged(from: previousTraitCollection) { + self.updateAppearance() + } + } + + private func updateAppearance() { + self.backgroundColor = self.appearance.backgroundColor + self.imageView.isHidden = self.isDarkInterfaceStyle + } +} + +extension ContinueCourseBackgroundView: ProgrammaticallyInitializableViewProtocol { + func setupView() { + self.updateAppearance() + } + + func addSubviews() { + self.addSubview(self.imageView) + } + + func makeConstraints() { + self.imageView.translatesAutoresizingMaskIntoConstraints = false + self.imageView.snp.makeConstraints { $0.edges.equalToSuperview() } + } +} diff --git a/Stepic/Sources/Modules/ExploreSubmodules/ContinueCourse/Views/ContinueCourseEmptyView.swift b/Stepic/Sources/Modules/ExploreSubmodules/ContinueCourse/Views/ContinueCourseEmptyView.swift new file mode 100644 index 0000000000..f9945065ff --- /dev/null +++ b/Stepic/Sources/Modules/ExploreSubmodules/ContinueCourse/Views/ContinueCourseEmptyView.swift @@ -0,0 +1,101 @@ +import SnapKit +import UIKit + +extension ContinueCourseEmptyView { + struct Appearance { + let primaryColor: UIColor + let defaultInsets = LayoutInsets.default + + let plusIconSize = CGSize(width: 20, height: 20) + let plusContainerCornerRadius: CGFloat = 8 + let plusContainerSize = CGSize(width: 40, height: 40) + var plusContainerBackgroundColor: UIColor { + .dynamic(light: self.primaryColor.withAlphaComponent(0.12), dark: .stepikTertiaryBackground) + } + + let titleFont = UIFont.systemFont(ofSize: 16, weight: .medium) + let titleInsets = LayoutInsets(left: 8) + } +} + +final class ContinueCourseEmptyView: UIControl { + let appearance: Appearance + + private lazy var plusIconImageView: UIImageView = { + let image = UIImage(named: "plus")?.withRenderingMode(.alwaysTemplate) + let imageView = UIImageView(image: image) + imageView.tintColor = self.appearance.primaryColor + imageView.contentMode = .scaleAspectFit + return imageView + }() + private lazy var plusIconContainerView = UIView() + + private lazy var titleLabel: UILabel = { + let label = UILabel() + label.textColor = self.appearance.primaryColor + label.font = self.appearance.titleFont + label.numberOfLines = 1 + label.text = NSLocalizedString("ContinueCourseEmptyTitle", comment: "") + return label + }() + + override var isHighlighted: Bool { + didSet { + self.plusIconImageView.alpha = self.isHighlighted ? 0.5 : 1.0 + self.titleLabel.alpha = self.isHighlighted ? 0.5 : 1.0 + } + } + + init(frame: CGRect = .zero, 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") + } +} + +extension ContinueCourseEmptyView: ProgrammaticallyInitializableViewProtocol { + func setupView() { + self.plusIconContainerView.backgroundColor = self.appearance.plusContainerBackgroundColor + self.plusIconContainerView.roundAllCorners(radius: self.appearance.plusContainerCornerRadius) + } + + func addSubviews() { + self.addSubview(self.plusIconContainerView) + self.plusIconContainerView.addSubview(self.plusIconImageView) + + self.addSubview(self.titleLabel) + } + + func makeConstraints() { + self.plusIconContainerView.translatesAutoresizingMaskIntoConstraints = false + self.plusIconContainerView.snp.makeConstraints { make in + make.leading + .equalTo(self.safeAreaLayoutGuide.snp.leading) + .offset(self.appearance.defaultInsets.left) + make.centerY.equalToSuperview() + make.size.equalTo(self.appearance.plusContainerSize) + } + + self.plusIconImageView.translatesAutoresizingMaskIntoConstraints = false + self.plusIconImageView.snp.makeConstraints { make in + make.size.equalTo(self.appearance.plusIconSize) + make.center.equalToSuperview() + } + + self.titleLabel.translatesAutoresizingMaskIntoConstraints = false + self.titleLabel.snp.makeConstraints { make in + make.leading + .equalTo(self.plusIconContainerView.snp.trailing) + .offset(self.appearance.titleInsets.left) + make.centerY.equalTo(self.plusIconContainerView.snp.centerY) + } + } +} diff --git a/Stepic/Sources/Modules/ExploreSubmodules/ContinueCourse/Views/ContinueCourseView.swift b/Stepic/Sources/Modules/ExploreSubmodules/ContinueCourse/Views/ContinueCourseView.swift new file mode 100644 index 0000000000..1618ef34af --- /dev/null +++ b/Stepic/Sources/Modules/ExploreSubmodules/ContinueCourse/Views/ContinueCourseView.swift @@ -0,0 +1,122 @@ +import SnapKit +import UIKit + +protocol ContinueCourseViewDelegate: AnyObject { + func continueCourseDidClickContinue(_ continueCourseView: ContinueCourseView) + func continueCourseDidClickEmpty(_ continueCourseView: ContinueCourseView) + func continueCourseSiriButtonDidClick(_ continueCourseView: ContinueCourseView) +} + +extension ContinueCourseView { + struct Appearance { + let cornerRadius: CGFloat = 13 + let primaryColor = UIColor.dynamic(light: .stepikVioletFixed, dark: .stepikSystemPrimaryText) + } +} + +final class ContinueCourseView: UIView { + weak var delegate: ContinueCourseViewDelegate? + + let appearance: Appearance + + private lazy var backgroundView = ContinueCourseBackgroundView() + + private lazy var emptyView: ContinueCourseEmptyView = { + let view = ContinueCourseEmptyView(appearance: .init(primaryColor: self.appearance.primaryColor)) + view.addTarget(self, action: #selector(self.emptyViewClicked), for: .touchUpInside) + view.isHidden = true + return view + }() + + private lazy var lastStepView: ContinueLastStepView = { + let view = ContinueLastStepView(appearance: .init(primaryColor: self.appearance.primaryColor)) + view.addTarget(self, action: #selector(self.lastStepViewClicked), for: .touchUpInside) + return view + }() + + var tooltipAnchorView: UIView { self.lastStepView.tooltipAnchorView } + + init(frame: CGRect, appearance: Appearance = Appearance()) { + self.appearance = appearance + super.init(frame: frame) + + self.addSubviews() + self.makeConstraints() + } + + @available(*, unavailable) + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + self.roundCorners([.topLeft, .topRight], radius: self.appearance.cornerRadius) + } + + func configure(viewModel: ContinueCourseViewModel) { + self.lastStepView.courseTitle = viewModel.title + + if let progressDescription = viewModel.progress?.description, + let progressValue = viewModel.progress?.value { + self.lastStepView.progressText = progressDescription + self.lastStepView.progress = progressValue + } + self.lastStepView.coverImageURL = viewModel.coverImageURL + } + + @available(iOS 12.0, *) + func configureSiriButton(contentConfiguration: SiriButtonContentConfiguration?) { + //self.lastStepView.configureSiriButton(contentConfiguration: contentConfiguration) + } + + func showLoading() { + self.lastStepView.isHidden = true + self.skeleton.viewBuilder = { ContinueCourseSkeletonView() } + self.skeleton.show() + } + + func hideLoading() { + self.lastStepView.isHidden = false + self.skeleton.hide() + } + + func showEmpty() { + self.lastStepView.isHidden = true + self.emptyView.isHidden = false + } + + func hideEmpty() { + self.lastStepView.isHidden = false + self.emptyView.isHidden = true + } + + @objc + private func lastStepViewClicked() { + self.delegate?.continueCourseDidClickContinue(self) + } + + @objc + private func emptyViewClicked() { + self.delegate?.continueCourseDidClickEmpty(self) + } +} + +extension ContinueCourseView: ProgrammaticallyInitializableViewProtocol { + func addSubviews() { + self.addSubview(self.backgroundView) + self.addSubview(self.emptyView) + self.addSubview(self.lastStepView) + } + + func makeConstraints() { + [ + self.backgroundView, + self.emptyView, + self.lastStepView + ].forEach { view in + view.translatesAutoresizingMaskIntoConstraints = false + view.snp.makeConstraints { $0.edges.equalToSuperview() } + } + } +} diff --git a/Stepic/Sources/Modules/ExploreSubmodules/ContinueCourse/Views/ContinueLastStepView.swift b/Stepic/Sources/Modules/ExploreSubmodules/ContinueCourse/Views/ContinueLastStepView.swift new file mode 100644 index 0000000000..ef326a8bcc --- /dev/null +++ b/Stepic/Sources/Modules/ExploreSubmodules/ContinueCourse/Views/ContinueLastStepView.swift @@ -0,0 +1,167 @@ +import Intents +import IntentsUI +import SnapKit +import UIKit + +extension ContinueLastStepView { + struct Appearance { + let primaryColor: UIColor + let defaultInsets = LayoutInsets.default + + let coverCornerRadius: CGFloat = 8 + let coverSize = CGSize(width: 40, height: 40) + + let courseLabelFont = UIFont.systemFont(ofSize: 16, weight: .medium) + let courseLabelInsets = LayoutInsets(left: 8, right: 8) + + let statsViewHeight: CGFloat = 17 + let progressFillColor = UIColor.stepikGreenFixed + let progressLabelTextColor = UIColor.white + + let rightDetailImageSize = CGSize(width: 20, height: 30) + } +} + +@available(iOS 12.0, *) +struct SiriButtonContentConfiguration { + var shortcut: INShortcut? + weak var delegate: INUIAddVoiceShortcutButtonDelegate? + + var isEmpty: Bool { self.shortcut == nil && self.delegate == nil } +} + +final class ContinueLastStepView: UIControl { + let appearance: Appearance + + private lazy var coverImageView: CourseCoverImageView = { + let view = CourseCoverImageView() + view.clipsToBounds = true + view.layer.cornerRadius = self.appearance.coverCornerRadius + return view + }() + + private lazy var courseNameLabel: UILabel = { + let label = UILabel() + label.textColor = self.appearance.primaryColor + label.font = self.appearance.courseLabelFont + label.numberOfLines = 1 + return label + }() + + private lazy var statsView: CourseWidgetStatsView = { + let appearance = CourseWidgetStatsView.Appearance( + leftInset: 0, + imagesRenderingBackgroundColor: self.appearance.primaryColor, + imagesRenderingTintColor: self.appearance.progressFillColor, + itemTextColor: self.appearance.primaryColor, + itemImageTintColor: self.appearance.primaryColor + ) + let view = CourseWidgetStatsView(appearance: appearance) + view.hideAllItems() + return view + }() + + private lazy var rightDetailImageView: UIImageView = { + let image = UIImage(named: "continue_learning_arrow_right")?.withRenderingMode(.alwaysTemplate) + let imageView = UIImageView(image: image) + imageView.tintColor = self.appearance.primaryColor + imageView.contentMode = .scaleAspectFit + return imageView + }() + + var courseTitle: String? { + didSet { + self.courseNameLabel.text = self.courseTitle + } + } + + var progressText: String? { + didSet { + self.updateProgress() + } + } + + var progress: Float = 0 { + didSet { + self.updateProgress() + } + } + + var coverImageURL: URL? { + didSet { + self.coverImageView.loadImage(url: self.coverImageURL) + } + } + + var tooltipAnchorView: UIView { self.rightDetailImageView } + + override var isHighlighted: Bool { + didSet { + self.subviews.forEach { $0.alpha = self.isHighlighted ? 0.5 : 1.0 } + } + } + + init(frame: CGRect = .zero, appearance: Appearance) { + self.appearance = appearance + super.init(frame: frame) + + self.addSubviews() + self.makeConstraints() + } + + @available(*, unavailable) + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func updateProgress() { + self.statsView.updateProgress( + viewModel: .init(progress: self.progress, progressLabelText: self.progressText ?? "") + ) + } +} + +extension ContinueLastStepView: ProgrammaticallyInitializableViewProtocol { + func addSubviews() { + self.addSubviews([self.coverImageView, self.courseNameLabel, self.statsView, self.rightDetailImageView]) + } + + func makeConstraints() { + self.coverImageView.translatesAutoresizingMaskIntoConstraints = false + self.coverImageView.snp.makeConstraints { make in + make.leading + .equalTo(self.safeAreaLayoutGuide.snp.leading) + .offset(self.appearance.defaultInsets.left) + make.centerY.equalToSuperview() + make.size.equalTo(self.appearance.coverSize) + } + + self.rightDetailImageView.translatesAutoresizingMaskIntoConstraints = false + self.rightDetailImageView.snp.makeConstraints { make in + make.trailing + .equalTo(self.safeAreaLayoutGuide.snp.trailing) + .offset(-self.appearance.defaultInsets.right) + make.centerY.equalTo(self.coverImageView.snp.centerY) + make.size.equalTo(self.appearance.rightDetailImageSize) + } + + self.courseNameLabel.translatesAutoresizingMaskIntoConstraints = false + self.courseNameLabel.snp.makeConstraints { make in + make.top.equalTo(self.coverImageView.snp.top) + make.leading + .equalTo(self.coverImageView.snp.trailing) + .offset(self.appearance.courseLabelInsets.left) + make.trailing + .equalTo(self.rightDetailImageView.snp.leading) + .offset(-self.appearance.courseLabelInsets.right) + } + + self.statsView.translatesAutoresizingMaskIntoConstraints = false + self.statsView.snp.makeConstraints { make in + make.leading.equalTo(self.courseNameLabel.snp.leading) + make.bottom.equalTo(self.coverImageView.snp.bottom) + make.trailing.equalTo(self.courseNameLabel.snp.trailing) + make.height.equalTo(self.appearance.statsViewHeight) + } + } +} diff --git a/Stepic/Sources/Modules/Home/HomeAssembly.swift b/Stepic/Sources/Modules/Home/HomeAssembly.swift index a5002687b4..7628c3fd99 100644 --- a/Stepic/Sources/Modules/Home/HomeAssembly.swift +++ b/Stepic/Sources/Modules/Home/HomeAssembly.swift @@ -9,7 +9,10 @@ final class HomeAssembly: Assembly { provider: provider, userAccountService: UserAccountService(), networkReachabilityService: NetworkReachabilityService(), - contentLanguageService: ContentLanguageService() + contentLanguageService: ContentLanguageService(), + personalOffersService: PersonalOffersService( + storageRecordsNetworkService: StorageRecordsNetworkService(storageRecordsAPI: StorageRecordsAPI()) + ) ) let viewController = HomeViewController(interactor: interactor, analytics: StepikAnalytics.shared) diff --git a/Stepic/Sources/Modules/Home/HomeDataFlow.swift b/Stepic/Sources/Modules/Home/HomeDataFlow.swift index c27998a357..f1069cd93d 100644 --- a/Stepic/Sources/Modules/Home/HomeDataFlow.swift +++ b/Stepic/Sources/Modules/Home/HomeDataFlow.swift @@ -4,6 +4,7 @@ enum Home { // MARK: Submodules identifiers enum Submodule: String, UniqueIdentifiable { + case stories case streakActivity case continueCourse case enrolledCourses @@ -73,4 +74,32 @@ enum Home { let result: State } } + + /// Update stories visibility + enum StoriesVisibilityUpdate { + struct Response { + let isHidden: Bool + } + + struct ViewModel { + let isHidden: Bool + } + } + + /// Update status bar style (called by stories module) + enum StatusBarStyleUpdate { + struct Response { + let statusBarStyle: UIStatusBarStyle + } + + struct ViewModel { + let statusBarStyle: UIStatusBarStyle + } + } + + enum CatalogPresentation { + struct Response {} + + struct ViewModel {} + } } diff --git a/Stepic/Sources/Modules/Home/HomeInteractor.swift b/Stepic/Sources/Modules/Home/HomeInteractor.swift index 143f488d4c..4fa7dd8e97 100644 --- a/Stepic/Sources/Modules/Home/HomeInteractor.swift +++ b/Stepic/Sources/Modules/Home/HomeInteractor.swift @@ -9,6 +9,7 @@ protocol HomeInteractorProtocol: BaseExploreInteractorProtocol { final class HomeInteractor: BaseExploreInteractor, HomeInteractorProtocol { private let provider: HomeProviderProtocol private let userAccountService: UserAccountServiceProtocol + private let personalOffersService: PersonalOffersServiceProtocol private lazy var homePresenter = self.presenter as? HomePresenterProtocol @@ -17,10 +18,13 @@ final class HomeInteractor: BaseExploreInteractor, HomeInteractorProtocol { provider: HomeProviderProtocol, userAccountService: UserAccountServiceProtocol, networkReachabilityService: NetworkReachabilityServiceProtocol, - contentLanguageService: ContentLanguageServiceProtocol + contentLanguageService: ContentLanguageServiceProtocol, + personalOffersService: PersonalOffersServiceProtocol ) { self.provider = provider self.userAccountService = userAccountService + self.personalOffersService = personalOffersService + super.init( presenter: presenter, contentLanguageService: contentLanguageService, @@ -55,6 +59,7 @@ final class HomeInteractor: BaseExploreInteractor, HomeInteractorProtocol { contentLanguage: self.contentLanguageService.globalContentLanguage ) ) + self.syncPersonalOffers() } override func presentEmptyState(sourceModule: CourseListInputProtocol) { @@ -86,6 +91,29 @@ final class HomeInteractor: BaseExploreInteractor, HomeInteractorProtocol { fatalError("Unrecognized submodule") } } + + private func syncPersonalOffers() { + guard self.networkReachabilityService.isReachable else { + return + } + + guard let currentUser = self.userAccountService.currentUser, + !currentUser.isGuest && self.userAccountService.isAuthorized else { + return + } + + self.personalOffersService.syncPersonalOffers(userID: currentUser.id).cauterize() + } +} + +extension HomeInteractor: StoriesOutputProtocol { + func hideStories() { + self.homePresenter?.presentStoriesBlock(response: .init(isHidden: true)) + } + + func handleStoriesStatusBarStyleUpdate(_ statusBarStyle: UIStatusBarStyle) { + self.homePresenter?.presentStatusBarStyle(response: .init(statusBarStyle: statusBarStyle)) + } } extension HomeInteractor: ContinueCourseOutputProtocol { @@ -93,8 +121,12 @@ extension HomeInteractor: ContinueCourseOutputProtocol { self.homePresenter?.presentCourseListState( response: .init( module: Home.Submodule.continueCourse, - result: .empty + result: .error ) ) } + + func presentCatalog() { + self.homePresenter?.presentCatalog(response: .init()) + } } diff --git a/Stepic/Sources/Modules/Home/HomePresenter.swift b/Stepic/Sources/Modules/Home/HomePresenter.swift index cb12f9b5b3..70f429d34a 100644 --- a/Stepic/Sources/Modules/Home/HomePresenter.swift +++ b/Stepic/Sources/Modules/Home/HomePresenter.swift @@ -4,6 +4,9 @@ protocol HomePresenterProtocol: BaseExplorePresenterProtocol { func presentStreakActivity(response: Home.StreakLoad.Response) func presentContent(response: Home.ContentLoad.Response) func presentCourseListState(response: Home.CourseListStateUpdate.Response) + func presentStoriesBlock(response: Home.StoriesVisibilityUpdate.Response) + func presentStatusBarStyle(response: Home.StatusBarStyleUpdate.Response) + func presentCatalog(response: Home.CatalogPresentation.Response) } final class HomePresenter: BaseExplorePresenter, HomePresenterProtocol { @@ -52,6 +55,18 @@ final class HomePresenter: BaseExplorePresenter, HomePresenterProtocol { ) } + func presentStoriesBlock(response: Home.StoriesVisibilityUpdate.Response) { + self.homeViewController?.displayStoriesBlock(viewModel: .init(isHidden: response.isHidden)) + } + + func presentStatusBarStyle(response: Home.StatusBarStyleUpdate.Response) { + self.homeViewController?.displayStatusBarStyle(viewModel: .init(statusBarStyle: response.statusBarStyle)) + } + + func presentCatalog(response: Home.CatalogPresentation.Response) { + self.homeViewController?.displayCatalog(viewModel: .init()) + } + private func makeStreakActivityMessage(days: Int, needsToSolveToday: Bool) -> String { let pluralizedDaysCnt = StringHelper.pluralize( number: days, diff --git a/Stepic/Sources/Modules/Home/HomeViewController.swift b/Stepic/Sources/Modules/Home/HomeViewController.swift index 9c48350163..e2c3b5b2b3 100644 --- a/Stepic/Sources/Modules/Home/HomeViewController.swift +++ b/Stepic/Sources/Modules/Home/HomeViewController.swift @@ -1,21 +1,30 @@ import PromiseKit +import SnapKit import UIKit +// swiftlint:disable file_length protocol HomeViewControllerProtocol: BaseExploreViewControllerProtocol { func displayStreakInfo(viewModel: Home.StreakLoad.ViewModel) func displayContent(viewModel: Home.ContentLoad.ViewModel) func displayModuleErrorState(viewModel: Home.CourseListStateUpdate.ViewModel) + func displayStoriesBlock(viewModel: Home.StoriesVisibilityUpdate.ViewModel) + func displayStatusBarStyle(viewModel: Home.StatusBarStyleUpdate.ViewModel) + func displayCatalog(viewModel: Home.CatalogPresentation.ViewModel) } final class HomeViewController: BaseExploreViewController { + enum Appearance { + static let continueCourseHeight: CGFloat = 72 + } + enum Animation { static let startRefreshDelay: TimeInterval = 1.0 static let modulesRefreshDelay: TimeInterval = 0.3 } fileprivate static let submodulesOrder: [Home.Submodule] = [ + .stories, .streakActivity, - .continueCourse, .enrolledCourses, .reviewsAndWishlist, .visitedCourses, @@ -25,6 +34,7 @@ final class HomeViewController: BaseExploreViewController { private var lastContentLanguage: ContentLanguage? private var lastIsAuthorizedFlag = false + private var currentStoriesSubmoduleState = StoriesState.shown private var currentEnrolledCourseListState: EnrolledCourseListState? private var currentReviewsAndWishlistState: ReviewsAndWishlistState? @@ -86,6 +96,44 @@ final class HomeViewController: BaseExploreViewController { self.homeInteractor?.doContentLoad(request: .init()) } + // MARK: - Stories + + private enum StoriesState { + case shown + case hidden + } + + private func refreshStateForStories(state: StoriesState) { + defer { + self.currentStoriesSubmoduleState = state + } + + if let submodule = self.getSubmodule(type: Home.Submodule.stories) { + self.removeSubmodule(submodule) + } + + guard case .shown = state else { + return + } + + let storiesAssembly = StoriesAssembly( + storyOpenSource: .home, + output: self.homeInteractor as? StoriesOutputProtocol + ) + let storiesViewController = storiesAssembly.makeModule() + let storiesContainerView = ExploreStoriesContainerView( + contentView: storiesViewController.view + ) + self.registerSubmodule( + .init( + viewController: storiesViewController, + view: storiesContainerView, + isLanguageDependent: true, + type: Home.Submodule.stories + ) + ) + } + // MARK: - Streak activity private enum StreakActivityState { @@ -124,10 +172,19 @@ final class HomeViewController: BaseExploreViewController { } private func refreshContinueCourse(state: ContinueCourseState) { + var contentInsets = self.exploreView?.contentInsets ?? .zero + if let submodule = self.getSubmodule(type: Home.Submodule.continueCourse) { self.removeSubmodule(submodule) } + defer { + contentInsets.bottom = state == .shown + ? (Appearance.continueCourseHeight + LayoutInsets.default.bottom) + : 0 + self.exploreView?.contentInsets = contentInsets + } + guard case .shown = state else { return } @@ -136,14 +193,22 @@ final class HomeViewController: BaseExploreViewController { output: self.interactor as? ContinueCourseOutputProtocol ) let continueCourseViewController = continueCourseAssembly.makeModule() + self.registerSubmodule( .init( viewController: continueCourseViewController, view: continueCourseViewController.view, + isArrangeable: false, isLanguageDependent: false, type: Home.Submodule.continueCourse ) ) + + continueCourseViewController.view.snp.makeConstraints { make in + make.leading.trailing.equalToSuperview() + make.bottom.equalTo(self.view.safeAreaLayoutGuide) + make.height.equalTo(Appearance.continueCourseHeight) + } } // MARK: - Fullscreen displaying @@ -185,6 +250,17 @@ final class HomeViewController: BaseExploreViewController { case error case empty + var containerDescription: CourseListContainerViewFactory.HorizontalContainerDescription { + switch self { + case .normal: + return .init(background: .image(UIImage(named: "new_courses_gradient"))) + case .empty: + return .init(background: .image(UIImage(named: "new_courses_placeholder_gradient_large"))) + case .anonymous, .error: + return .init(background: .image(UIImage(named: "new_courses_placeholder_gradient_small"))) + } + } + var headerDescription: CourseListContainerViewFactory.HorizontalHeaderDescription { CourseListContainerViewFactory.HorizontalHeaderDescription( title: NSLocalizedString("Enrolled", comment: ""), @@ -193,12 +269,12 @@ final class HomeViewController: BaseExploreViewController { ) } - var message: GradientCoursesPlaceholderViewFactory.InfoPlaceholderMessage { + var placeholderStyle: NewExploreBlockPlaceholderView.PlaceholderStyle { switch self { case .anonymous: - return .login + return .anonymous case .error: - return .enrolledError + return .error case .empty: return .enrolledEmpty default: @@ -211,7 +287,7 @@ final class HomeViewController: BaseExploreViewController { let courseListType = EnrolledCourseListType() let enrolledCourseListAssembly = HorizontalCourseListAssembly( type: courseListType, - colorMode: .light, + colorMode: .clearLight, courseViewSource: .myCourses, output: self.interactor as? CourseListOutputProtocol ) @@ -239,16 +315,20 @@ final class HomeViewController: BaseExploreViewController { (view, viewController) = self.makeEnrolledCourseListSubmodule() } else { // Build placeholder - let placeholderView = ExploreBlockPlaceholderView(message: state.message) + let placeholderView = NewExploreBlockPlaceholderView(placeholderStyle: state.placeholderStyle) switch state { case .anonymous: - placeholderView.onPlaceholderClick = { [weak self] in + placeholderView.onActionButtonClick = { [weak self] in self?.displayAuthorization(viewModel: .init()) } case .error: - placeholderView.onPlaceholderClick = { [weak self] in + placeholderView.onActionButtonClick = { [weak self] in self?.refreshStateForEnrolledCourses(state: .normal) } + case .empty: + placeholderView.onActionButtonClick = { [weak self] in + self?.displayCatalog(viewModel: .init()) + } default: break } @@ -262,6 +342,7 @@ final class HomeViewController: BaseExploreViewController { let containerView = CourseListContainerViewFactory(colorMode: .light) .makeHorizontalContainerView( for: view, + containerDescription: state.containerDescription, headerDescription: state.headerDescription, contentViewInsets: contentViewInsets ) @@ -491,10 +572,7 @@ extension HomeViewController: HomeViewControllerProtocol { func displayModuleErrorState(viewModel: Home.CourseListStateUpdate.ViewModel) { switch viewModel.module { case .continueCourse: - switch viewModel.result { - default: - self.refreshContinueCourse(state: .hidden) - } + self.refreshContinueCourse(state: .hidden) case .enrolledCourses: switch viewModel.result { case .empty: @@ -533,20 +611,37 @@ extension HomeViewController: HomeViewControllerProtocol { return } - strongSelf.lastContentLanguage = viewModel.contentLanguage - strongSelf.lastIsAuthorizedFlag = viewModel.isAuthorized - - let shouldDisplayContinueCourse = viewModel.isAuthorized + let shouldDisplayStories = strongSelf.currentStoriesSubmoduleState == .shown + || (strongSelf.currentStoriesSubmoduleState == .hidden + && strongSelf.lastContentLanguage != viewModel.contentLanguage) let shouldDisplayAnonymousPlaceholder = !viewModel.isAuthorized let shouldDisplayReviewsAndWishlist = viewModel.isAuthorized - strongSelf.refreshContinueCourse(state: shouldDisplayContinueCourse ? .shown : .hidden) + strongSelf.lastContentLanguage = viewModel.contentLanguage + strongSelf.lastIsAuthorizedFlag = viewModel.isAuthorized + + strongSelf.refreshStateForStories(state: shouldDisplayStories ? .shown : .hidden) + strongSelf.refreshContinueCourse(state: .shown) strongSelf.refreshStateForEnrolledCourses(state: shouldDisplayAnonymousPlaceholder ? .anonymous : .normal) strongSelf.refreshReviewsAndWishlist(state: shouldDisplayReviewsAndWishlist ? .shown : .hidden) strongSelf.refreshStateForVisitedCourses(state: .shown) strongSelf.refreshStateForPopularCourses(state: .normal) } } + + func displayStoriesBlock(viewModel: Home.StoriesVisibilityUpdate.ViewModel) { + self.refreshStateForStories(state: viewModel.isHidden ? .hidden : .shown) + } + + func displayStatusBarStyle(viewModel: Home.StatusBarStyleUpdate.ViewModel) { + if let styledNavigationController = self.navigationController as? StyledNavigationController { + styledNavigationController.changeStatusBarStyle(viewModel.statusBarStyle, sender: self) + } + } + + func displayCatalog(viewModel: Home.CatalogPresentation.ViewModel) { + DeepLinkRouter.routeToCatalog() + } } extension HomeViewController: BaseExploreViewDelegate { diff --git a/Stepic/Sources/Modules/HomeSubmodules/UserCoursesReviewsWidget/UserCoursesReviewsWidgetView.swift b/Stepic/Sources/Modules/HomeSubmodules/UserCoursesReviewsWidget/UserCoursesReviewsWidgetView.swift index 68ec4ccf28..572d428376 100644 --- a/Stepic/Sources/Modules/HomeSubmodules/UserCoursesReviewsWidget/UserCoursesReviewsWidgetView.swift +++ b/Stepic/Sources/Modules/HomeSubmodules/UserCoursesReviewsWidget/UserCoursesReviewsWidgetView.swift @@ -82,7 +82,7 @@ final class UserCoursesReviewsWidgetView: UIControl { let view = UIView() view.backgroundColor = self.appearance.accentIndicatorViewBackgroundColor view.isHidden = true - view.setRoundedCorners(cornerRadius: self.appearance.accentIndicatorViewCornerRadius) + view.roundAllCorners(radius: self.appearance.accentIndicatorViewCornerRadius) return view }() diff --git a/Stepic/Sources/Modules/Quizzes/NewFreeAnswerQuiz/NewFreeAnswerQuizView.swift b/Stepic/Sources/Modules/Quizzes/NewFreeAnswerQuiz/NewFreeAnswerQuizView.swift index 7452a96592..7c2afa5b7c 100644 --- a/Stepic/Sources/Modules/Quizzes/NewFreeAnswerQuiz/NewFreeAnswerQuizView.swift +++ b/Stepic/Sources/Modules/Quizzes/NewFreeAnswerQuiz/NewFreeAnswerQuizView.swift @@ -39,8 +39,8 @@ final class NewFreeAnswerQuizView: UIView, TitlePresentable { private lazy var textView: TableInputTextView = { let textView = TableInputTextView() textView.textInsets = self.appearance.textFieldInsets - textView.setRoundedCorners( - cornerRadius: self.appearance.textFieldBorderCornerRadius, + textView.roundAllCorners( + radius: self.appearance.textFieldBorderCornerRadius, borderWidth: self.appearance.textFieldBorderWidth, borderColor: self.appearance.textFieldBorderColor ) diff --git a/Stepic/Sources/Modules/Quizzes/NewStringQuiz/NewStringQuizView.swift b/Stepic/Sources/Modules/Quizzes/NewStringQuiz/NewStringQuizView.swift index 9a12099d28..ff8d264b58 100644 --- a/Stepic/Sources/Modules/Quizzes/NewStringQuiz/NewStringQuizView.swift +++ b/Stepic/Sources/Modules/Quizzes/NewStringQuiz/NewStringQuizView.swift @@ -53,8 +53,8 @@ final class NewStringQuizView: UIView, TitlePresentable { bottom: self.appearance.textFieldInsets.bottom, right: self.appearance.textFieldDefaultOffset ) - field.setRoundedCorners( - cornerRadius: self.appearance.textFieldBorderCornerRadius, + field.roundAllCorners( + radius: self.appearance.textFieldBorderCornerRadius, borderWidth: self.appearance.textFieldBorderWidth, borderColor: self.appearance.textFieldBorderColor ) diff --git a/Stepic/Sources/Modules/StepQuizReview/Views/StepQuizReviewMessageView.swift b/Stepic/Sources/Modules/StepQuizReview/Views/StepQuizReviewMessageView.swift index 0f9f0e81a4..aa6ff33e1c 100644 --- a/Stepic/Sources/Modules/StepQuizReview/Views/StepQuizReviewMessageView.swift +++ b/Stepic/Sources/Modules/StepQuizReview/Views/StepQuizReviewMessageView.swift @@ -65,7 +65,7 @@ final class StepQuizReviewMessageView: UIView { extension StepQuizReviewMessageView: ProgrammaticallyInitializableViewProtocol { func setupView() { self.backgroundColor = self.appearance.backgroundColor - self.setRoundedCorners(cornerRadius: self.appearance.cornerRadius) + self.roundAllCorners(radius: self.appearance.cornerRadius) } func addSubviews() { diff --git a/Stepic/Sources/Modules/Submissions/Views/Cell/SubmissionView.swift b/Stepic/Sources/Modules/Submissions/Views/Cell/SubmissionView.swift index b763ae62f3..9bbd841f15 100644 --- a/Stepic/Sources/Modules/Submissions/Views/Cell/SubmissionView.swift +++ b/Stepic/Sources/Modules/Submissions/Views/Cell/SubmissionView.swift @@ -27,7 +27,7 @@ final class SubmissionView: UIView { private lazy var statusView: UIView = { let view = UIView() - view.setRoundedCorners(cornerRadius: self.appearance.statusViewSize.height / 2) + view.roundAllCorners(radius: self.appearance.statusViewSize.height / 2) return view }() diff --git a/Stepic/Sources/Services/ApplicationShortcuts/ApplicationShortcutService.swift b/Stepic/Sources/Services/ApplicationShortcuts/ApplicationShortcutService.swift index cfa2554e9e..92898808da 100644 --- a/Stepic/Sources/Services/ApplicationShortcuts/ApplicationShortcutService.swift +++ b/Stepic/Sources/Services/ApplicationShortcuts/ApplicationShortcutService.swift @@ -20,6 +20,11 @@ final class ApplicationShortcutService: ApplicationShortcutServiceProtocol { init( lastCourseDataShortcutService: LastCourseDataShortcutServiceProtocol = LastCourseDataShortcutService(), adaptiveStorageManager: AdaptiveStorageManagerProtocol = AdaptiveStorageManager(), + continueCourseProvider: ContinueCourseProviderProtocol = ContinueCourseProvider( + userCoursesNetworkService: UserCoursesNetworkService(userCoursesAPI: UserCoursesAPI()), + coursesNetworkService: CoursesNetworkService(coursesAPI: CoursesAPI()), + progressesNetworkService: ProgressesNetworkService(progressesAPI: ProgressesAPI()) + ), userAccountService: UserAccountServiceProtocol = UserAccountService(), sourcelessRouter: SourcelessRouter = SourcelessRouter(), analytics: Analytics = StepikAnalytics.shared diff --git a/Stepic/Sources/Services/ApplicationShortcuts/LastCourseDataShortcutService.swift b/Stepic/Sources/Services/ApplicationShortcuts/LastCourseDataShortcutService.swift index 44d3f6421a..f0799e89ab 100644 --- a/Stepic/Sources/Services/ApplicationShortcuts/LastCourseDataShortcutService.swift +++ b/Stepic/Sources/Services/ApplicationShortcuts/LastCourseDataShortcutService.swift @@ -16,8 +16,8 @@ final class LastCourseDataShortcutService: LastCourseDataShortcutServiceProtocol coursesPersistenceService: CoursesPersistenceServiceProtocol = CoursesPersistenceService(), adaptiveStorageManager: AdaptiveStorageManagerProtocol = AdaptiveStorageManager(), continueCourseProvider: ContinueCourseProviderProtocol = ContinueCourseProvider( - userCoursesAPI: UserCoursesAPI(), - coursesAPI: CoursesAPI(), + userCoursesNetworkService: UserCoursesNetworkService(userCoursesAPI: UserCoursesAPI()), + coursesNetworkService: CoursesNetworkService(coursesAPI: CoursesAPI()), progressesNetworkService: ProgressesNetworkService(progressesAPI: ProgressesAPI()) ) ) { diff --git a/Stepic/Sources/Views/ContinueLastStepView/ContinueActionButton.swift b/Stepic/Sources/Views/ContinueActionButton.swift similarity index 100% rename from Stepic/Sources/Views/ContinueLastStepView/ContinueActionButton.swift rename to Stepic/Sources/Views/ContinueActionButton.swift diff --git a/Stepic/en.lproj/Localizable.strings b/Stepic/en.lproj/Localizable.strings index 137e5e53bd..830e0fe710 100644 --- a/Stepic/en.lproj/Localizable.strings +++ b/Stepic/en.lproj/Localizable.strings @@ -313,6 +313,13 @@ Enrolled = "Enrolled"; Popular = "Popular"; VisitedCourses = "Visited courses"; Home = "Home"; +RecommendedCategory = "Recommended courses"; +NewHomePlaceholderAnonymousTitle = "Sign in and start learning right now"; +NewHomePlaceholderAnonymousButtonTitle = "Sign In"; +NewHomePlaceholderErrorTitle = "Connection is lost"; +NewHomePlaceholderErrorButtonTitle = "Try Again"; +NewHomePlaceholderEmptyEnrolledTitle = "Your courses will be here"; +NewHomePlaceholderEmptyEnrolledButtonTitle = "Find Courses in Catalog"; HomePlaceholderAnonymous = "Sign in and start learning right now"; HomePlaceholderEmptyEnrolled = "Enroll for free courses and they will be here"; HomePlaceholderEmptyPopular = "Open courses are on vacation. Please, see later"; @@ -1275,6 +1282,8 @@ CourseWidgetEnrolled = "Enrolled"; CourseWidgetPriceFree = "Free"; CourseWidgetSeeAllTitle = "See All"; +ContinueCourseEmptyTitle = "Find your first course"; + /* Course List Filter */ CourseListFilterTitle = "Filters"; CourseListFilterSectionCourseLanguageTitle = "Course Language"; diff --git a/Stepic/ru.lproj/Localizable.strings b/Stepic/ru.lproj/Localizable.strings index 95d8aad081..22d516d72a 100644 --- a/Stepic/ru.lproj/Localizable.strings +++ b/Stepic/ru.lproj/Localizable.strings @@ -314,6 +314,13 @@ Enrolled = "Мои курсы"; Popular = "Популярные"; VisitedCourses = "Посещённые курсы"; Home = "Обучение"; +RecommendedCategory = "Подборка"; +NewHomePlaceholderAnonymousTitle = "Войдите и начните учиться прямо сейчас"; +NewHomePlaceholderAnonymousButtonTitle = "Войти"; +NewHomePlaceholderErrorTitle = "Соединение потеряно"; +NewHomePlaceholderErrorButtonTitle = "Повторить"; +NewHomePlaceholderEmptyEnrolledTitle = "Здесь появятся ваши Курсы"; +NewHomePlaceholderEmptyEnrolledButtonTitle = "Найти курсы в Каталоге"; HomePlaceholderAnonymous = "Войдите и начните учиться прямо сейчас"; HomePlaceholderEmptyEnrolled = "Запишитесь на курсы и они будут показаны здесь"; HomePlaceholderEmptyPopular = "Открытые курсы сейчас в отпуске. Пожалуйста, зайдите позже"; @@ -1276,6 +1283,8 @@ CourseWidgetEnrolled = "Вы записаны"; CourseWidgetPriceFree = "Бесплатно"; CourseWidgetSeeAllTitle = "Посмотреть все"; +ContinueCourseEmptyTitle = "Найдите свой первый курс"; + /* Course List Filter */ CourseListFilterTitle = "Фильтры"; CourseListFilterSectionCourseLanguageTitle = "Язык курса";