diff --git a/Stepic.xcodeproj/project.pbxproj b/Stepic.xcodeproj/project.pbxproj index 99f912477e..5492d08339 100644 --- a/Stepic.xcodeproj/project.pbxproj +++ b/Stepic.xcodeproj/project.pbxproj @@ -4017,6 +4017,7 @@ 62E981CC08A57CBEE4878767 /* ProfileHeaderInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E98FC406CB025F46041481 /* ProfileHeaderInfoView.swift */; }; 62E981CC646675C80868210C /* ServiceFactoryImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E98003E33487A15450E427 /* ServiceFactoryImpl.swift */; }; 62E981DDEADDC9E1E7E21CB3 /* ProfileViewController+StreakNotificationsControlView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E9888C0199CE31C6B4E91D /* ProfileViewController+StreakNotificationsControlView.swift */; }; + 62E981F1071760511CFB0A61 /* CourseInfoTabInfoSkeletonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E98454B9A6D4DB423DB94F /* CourseInfoTabInfoSkeletonView.swift */; }; 62E981F7AD9105506CB98E36 /* CourseListsCollectionAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E981AB7A6095813B5B3E1A /* CourseListsCollectionAssembly.swift */; }; 62E9820491170A9FF2E00442 /* StepsPagerDataSourceImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E98366860D3BBBCC872942 /* StepsPagerDataSourceImpl.swift */; }; 62E9822D92CA621E09BA5995 /* StepsPagerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E98849F0870E88D6D8F2E7 /* StepsPagerViewController.swift */; }; @@ -4026,6 +4027,7 @@ 62E982769C19C7852BCD59C4 /* TooltipStorageManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E98117E7E58E81E4BF6E24 /* TooltipStorageManager.swift */; }; 62E9829488F863D10CF5544C /* UITableView+TableHeaderLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E98D9D45E6DA6099AF1C0B /* UITableView+TableHeaderLayout.swift */; }; 62E982CD54C2C86F14DF84EB /* ProfileHeaderInfoView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 62E98801330B94B34FA0D7B9 /* ProfileHeaderInfoView.xib */; }; + 62E982D9472B24E0A6C95984 /* CourseInfoTabInfoHeaderBlockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E984497C7DD6033AC0B914 /* CourseInfoTabInfoHeaderBlockView.swift */; }; 62E982F7B4EDC6C94D1283FE /* ProgressesNetworkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E982F6B023E2C827407B1B /* ProgressesNetworkService.swift */; }; 62E983397736699787D8DD35 /* UIView+fromNib.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E982BB5246C2BBC5E88D06 /* UIView+fromNib.swift */; }; 62E9834EF86EA5676FD4686E /* ProfileInfoPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E98275C20698D3770CD6D8 /* ProfileInfoPresenter.swift */; }; @@ -4041,6 +4043,7 @@ 62E983EF0CF69961675F4285 /* BounceButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E98143194A6707C5D04BA4 /* BounceButton.swift */; }; 62E98400F19C6E781027F2A7 /* AlamofireDefaultSessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E98D998C0EBCA801E4BD65 /* AlamofireDefaultSessionManager.swift */; }; 62E98401FDC2476D5205E32D /* ProfileMenuBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E980E9DFE93B405A0E48AD /* ProfileMenuBlock.swift */; }; + 62E984095A966E6F531E0ECF /* CourseInfoTabInfoInstructorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E98114DF11CFD4F5F75E79 /* CourseInfoTabInfoInstructorView.swift */; }; 62E98423FB0D93C3534C9520 /* StepsPagerRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E9897F76726E9D4EF78C1A /* StepsPagerRouter.swift */; }; 62E98438E8DCED37E1AC2A8D /* CodeEditorPreferencesContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E98148BF3C6F51F47F2BA9 /* CodeEditorPreferencesContainer.swift */; }; 62E9843D0803C988BB6473E3 /* BaseCourseListFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E9855F408D287FF5652C21 /* BaseCourseListFlowLayout.swift */; }; @@ -4093,10 +4096,12 @@ 62E98760B61DB08132C71EE9 /* ContentLanguageService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E98C116ED420A46A8D2642 /* ContentLanguageService.swift */; }; 62E987790F995FE9E728A308 /* CourseListOutputProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E9854FAF9EE4F6763E65F8 /* CourseListOutputProtocol.swift */; }; 62E9877EE8A8612EDBD7A6BE /* ProfileViewController+StreakNotificationsControlView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E9888C0199CE31C6B4E91D /* ProfileViewController+StreakNotificationsControlView.swift */; }; + 62E9877FB66EF5FA8531AFDC /* CourseInfoTabInfoViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E985797A50F6A4FFBEAA91 /* CourseInfoTabInfoViewModel.swift */; }; 62E9879A89A2EFFA5CCAED86 /* StepikURLSessionConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E98DC7237B8757A9F58935 /* StepikURLSessionConfiguration.swift */; }; 62E987A1CC2316014302E314 /* ProfileMenuBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E980E9DFE93B405A0E48AD /* ProfileMenuBlock.swift */; }; 62E987AE77EBF9995848ED55 /* HeaderEmptyAuthView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E9848BCD3343E626596F54 /* HeaderEmptyAuthView.swift */; }; 62E987CB113F6D20B108D070 /* AchievementPopupAlertManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E9851360F1490A8311EC41 /* AchievementPopupAlertManager.swift */; }; + 62E987D46233B89387139183 /* CourseInfoTabInfoInstructorSkeletonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E9877223D790EAA5F8A291 /* CourseInfoTabInfoInstructorSkeletonView.swift */; }; 62E987E79C773F8C291F49F3 /* PinsMapBlockContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E985F73C3288F8D0045327 /* PinsMapBlockContentView.swift */; }; 62E987EE86AB48E30FF5EC79 /* ProgressServiceImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E98DD43356F9ABC3A351B8 /* ProgressServiceImpl.swift */; }; 62E987FD412017016C6AD457 /* HighlightFakeButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E98D8F640B8F40930878C2 /* HighlightFakeButton.swift */; }; @@ -4110,6 +4115,7 @@ 62E98880A7BEE7F43E48325B /* PaginationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E9803B15F1155BEDF36837 /* PaginationView.swift */; }; 62E98881AEF0EC80AAF1D41D /* LegacyAssemblies.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E98E120FBE6E7B7C30BCB0 /* LegacyAssemblies.swift */; }; 62E988866D3362A65DC7BFD1 /* StepsPagerDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E981476A50554ED5B93B30 /* StepsPagerDataSource.swift */; }; + 62E98889B9D4C66875D98BB6 /* CourseInfoTabInfoInstructorsBlockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E981C3381EF87D844F8E11 /* CourseInfoTabInfoInstructorsBlockView.swift */; }; 62E9888C86E6F2C4C8E8B89D /* ProfileMenuBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E980E9DFE93B405A0E48AD /* ProfileMenuBlock.swift */; }; 62E988920ADE4E716DC451C6 /* CourseListInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E9846DC5987C75FCD9D69E /* CourseListInteractor.swift */; }; 62E9889B64C038694C9566DE /* UIView+fromNib.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E982BB5246C2BBC5E88D06 /* UIView+fromNib.swift */; }; @@ -4117,6 +4123,7 @@ 62E988EC90FF20DE3926BCE5 /* CourseReviewSummariesNetworkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E989338C0834107DD43B60 /* CourseReviewSummariesNetworkService.swift */; }; 62E98905245F33B16D020586 /* ContinueActionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E9807E3506DAF40179593F /* ContinueActionButton.swift */; }; 62E9893ECE0FA9E7AEC2B6DD /* CourseListCollectionOutputProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E986B46F417BDAA7E660CF /* CourseListCollectionOutputProtocol.swift */; }; + 62E989608052E8BA824C9A21 /* CourseInfoTabInfoHeaderBlockSkeletonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E9876C6770A639F8C20BB8 /* CourseInfoTabInfoHeaderBlockSkeletonView.swift */; }; 62E98967D33DB824B8BF27E0 /* StepikURLSessionConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E98DC7237B8757A9F58935 /* StepikURLSessionConfiguration.swift */; }; 62E98975B396C752B07036D8 /* CodeEditorSettingsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E989579A70D89D559F7520 /* CodeEditorSettingsPresenter.swift */; }; 62E9897FBB60991C568BFDBE /* UITableView+TableHeaderLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E98D9D45E6DA6099AF1C0B /* UITableView+TableHeaderLayout.swift */; }; @@ -4160,6 +4167,7 @@ 62E98C0BC0F2F1C04047799F /* ProfileInfoPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E98275C20698D3770CD6D8 /* ProfileInfoPresenter.swift */; }; 62E98C1377148A52D15AAEBF /* CodeEditorPreferencesContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E98148BF3C6F51F47F2BA9 /* CodeEditorPreferencesContainer.swift */; }; 62E98C1F76B23AFDE100CF6B /* ProfileHeaderInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E98FC406CB025F46041481 /* ProfileHeaderInfoView.swift */; }; + 62E98C27BDE26D9EAC473BF4 /* CourseInfoTabInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E98B7DBB7C240C8383AF31 /* CourseInfoTabInfoView.swift */; }; 62E98C3BA370F328680994B8 /* CourseListContainerViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E9819456E7F199C837919A /* CourseListContainerViewFactory.swift */; }; 62E98C55E38F9010869F670F /* ButtonDescriptionFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E9888A338CA4289A954A20 /* ButtonDescriptionFactory.swift */; }; 62E98C56BF13948A9D009226 /* CourseListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E986AC9C1AEF85E04C2916 /* CourseListViewController.swift */; }; @@ -4168,6 +4176,7 @@ 62E98C88FB132BCDFD88F8AE /* PinsMapPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E98750EE80BFD97E4C7072 /* PinsMapPresenter.swift */; }; 62E98C89C492DD7D50D5AB24 /* WrappingNavigationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E98E55BD59203DE01307B1 /* WrappingNavigationViewController.swift */; }; 62E98C975482E46A95E173AB /* AlamofireDefaultSessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E98D998C0EBCA801E4BD65 /* AlamofireDefaultSessionManager.swift */; }; + 62E98C98C821248326D854AA /* CourseInfoTabInfoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E98523A1880164ADD69B1E /* CourseInfoTabInfoViewController.swift */; }; 62E98CAAD81ACCE00DA1A3FA /* ExploreBlockHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E98D1E8445FCC5FB8A2EB2 /* ExploreBlockHeaderView.swift */; }; 62E98CAB9EA33B2E0B56156F /* PinsMapPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E98750EE80BFD97E4C7072 /* PinsMapPresenter.swift */; }; 62E98CE7008D80E6AA70A5D6 /* ProfileInfoPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E98275C20698D3770CD6D8 /* ProfileInfoPresenter.swift */; }; @@ -4179,6 +4188,7 @@ 62E98D245B55B6ED2239D6D8 /* Reusable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E9842C04E29BCAAE9FAB20 /* Reusable.swift */; }; 62E98D42A45F833D1371E4A6 /* UIView+fromNib.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E982BB5246C2BBC5E88D06 /* UIView+fromNib.swift */; }; 62E98D686165FCA5E588FDD5 /* CustomNotificationBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E9810DF187ACA3C26F5396 /* CustomNotificationBanner.swift */; }; + 62E98D796A09691AF36E4693 /* CourseInfoTabInfoTextBlockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E98229BCE62A9522A60FF7 /* CourseInfoTabInfoTextBlockView.swift */; }; 62E98D7EC355992DB8889003 /* UITableView+TableHeaderLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E98D9D45E6DA6099AF1C0B /* UITableView+TableHeaderLayout.swift */; }; 62E98D9D4008CA06FC1E5446 /* ProfileViewController+StreakNotificationsControlView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E9888C0199CE31C6B4E91D /* ProfileViewController+StreakNotificationsControlView.swift */; }; 62E98DAF50E43BFA050E9E0D /* CourseListCollectionViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E98CA784B36866F15461A4 /* CourseListCollectionViewDelegate.swift */; }; @@ -4187,6 +4197,7 @@ 62E98DE7FA0EE5EB23136B52 /* AchievementsListPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E98AE968B12714D825650A /* AchievementsListPresenter.swift */; }; 62E98E0265217C52B54CF3DA /* CourseListNetworkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E984D4640A1A2101054F5E /* CourseListNetworkService.swift */; }; 62E98E207924FBDBAD355E57 /* WrappingNavigationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E98E55BD59203DE01307B1 /* WrappingNavigationViewController.swift */; }; + 62E98E2254E014AE47F7E4C2 /* CourseInfoTabInfoIntroVideoBlockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E9809AEE7ED05C92852758 /* CourseInfoTabInfoIntroVideoBlockView.swift */; }; 62E98E2FC787CFDAAF38CB52 /* PinsMapPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E98750EE80BFD97E4C7072 /* PinsMapPresenter.swift */; }; 62E98E4026C84605AD645F5A /* CourseListsCollectionDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E98CEC3723B6677A47112B /* CourseListsCollectionDataFlow.swift */; }; 62E98E53B559035BD0E4D903 /* CodeEditorSettingsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E989579A70D89D559F7520 /* CodeEditorSettingsPresenter.swift */; }; @@ -5987,10 +5998,12 @@ 62E98059417D9BC0015460B7 /* CourseListsCollectionProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CourseListsCollectionProvider.swift; sourceTree = ""; }; 62E9805D78A599B2B48A3B9D /* ExploreStoriesContainerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExploreStoriesContainerView.swift; sourceTree = ""; }; 62E9807E3506DAF40179593F /* ContinueActionButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContinueActionButton.swift; sourceTree = ""; }; + 62E9809AEE7ED05C92852758 /* CourseInfoTabInfoIntroVideoBlockView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CourseInfoTabInfoIntroVideoBlockView.swift; sourceTree = ""; }; 62E980ADD37451FFE6C89788 /* ExploreBlockPlaceholderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExploreBlockPlaceholderView.swift; sourceTree = ""; }; 62E980E9DFE93B405A0E48AD /* ProfileMenuBlock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProfileMenuBlock.swift; sourceTree = ""; }; 62E98100445A2042DE023DE9 /* ContinueCourseViewModelAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContinueCourseViewModelAdapter.swift; sourceTree = ""; }; 62E9810DF187ACA3C26F5396 /* CustomNotificationBanner.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomNotificationBanner.swift; sourceTree = ""; }; + 62E98114DF11CFD4F5F75E79 /* CourseInfoTabInfoInstructorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CourseInfoTabInfoInstructorView.swift; sourceTree = ""; }; 62E98117E7E58E81E4BF6E24 /* TooltipStorageManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TooltipStorageManager.swift; sourceTree = ""; }; 62E9813B202A8B78F131E4A3 /* CourseListAssembly.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CourseListAssembly.swift; sourceTree = ""; }; 62E9813EBFE07789C577FA45 /* CourseListsCollectionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CourseListsCollectionView.swift; sourceTree = ""; }; @@ -6000,9 +6013,11 @@ 62E9816CE0B5030624365711 /* CourseWidgetViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CourseWidgetViewModel.swift; sourceTree = ""; }; 62E9819456E7F199C837919A /* CourseListContainerViewFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CourseListContainerViewFactory.swift; sourceTree = ""; }; 62E981AB7A6095813B5B3E1A /* CourseListsCollectionAssembly.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CourseListsCollectionAssembly.swift; sourceTree = ""; }; + 62E981C3381EF87D844F8E11 /* CourseInfoTabInfoInstructorsBlockView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CourseInfoTabInfoInstructorsBlockView.swift; sourceTree = ""; }; 62E981C85CF2927E9FEF4EF4 /* CourseListsCollectionViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CourseListsCollectionViewModel.swift; sourceTree = ""; }; 62E981D7FD7BD900738BB677 /* UICollectionViewExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UICollectionViewExtensions.swift; sourceTree = ""; }; 62E981EDE8C762C38B5DA237 /* TagsOutputProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TagsOutputProtocol.swift; sourceTree = ""; }; + 62E98229BCE62A9522A60FF7 /* CourseInfoTabInfoTextBlockView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CourseInfoTabInfoTextBlockView.swift; sourceTree = ""; }; 62E98275C20698D3770CD6D8 /* ProfileInfoPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProfileInfoPresenter.swift; sourceTree = ""; }; 62E9827B52EA42AD5A939265 /* ExploreBlockContainerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExploreBlockContainerView.swift; sourceTree = ""; }; 62E982A58091854427D5396C /* CourseWidgetButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CourseWidgetButton.swift; sourceTree = ""; }; @@ -6017,6 +6032,8 @@ 62E983ECB91B2F753DC02E22 /* AchievementsListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AchievementsListViewController.swift; sourceTree = ""; }; 62E9840661E3EE43A4552946 /* StreakNotificationsControlPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StreakNotificationsControlPresenter.swift; sourceTree = ""; }; 62E9842C04E29BCAAE9FAB20 /* Reusable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Reusable.swift; sourceTree = ""; }; + 62E984497C7DD6033AC0B914 /* CourseInfoTabInfoHeaderBlockView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CourseInfoTabInfoHeaderBlockView.swift; sourceTree = ""; }; + 62E98454B9A6D4DB423DB94F /* CourseInfoTabInfoSkeletonView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CourseInfoTabInfoSkeletonView.swift; sourceTree = ""; }; 62E9846DC5987C75FCD9D69E /* CourseListInteractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CourseListInteractor.swift; sourceTree = ""; }; 62E98480E472319CCB60BE83 /* UITableViewExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UITableViewExtensions.swift; sourceTree = ""; }; 62E9848BCD3343E626596F54 /* HeaderEmptyAuthView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HeaderEmptyAuthView.swift; sourceTree = ""; }; @@ -6024,12 +6041,14 @@ 62E984E654267333681BD459 /* ContentLanguageSwitchButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentLanguageSwitchButton.swift; sourceTree = ""; }; 62E98500A0C0811D21357E27 /* StepsPagerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StepsPagerView.swift; sourceTree = ""; }; 62E9851360F1490A8311EC41 /* AchievementPopupAlertManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AchievementPopupAlertManager.swift; sourceTree = ""; }; + 62E98523A1880164ADD69B1E /* CourseInfoTabInfoViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CourseInfoTabInfoViewController.swift; sourceTree = ""; }; 62E98528BE31E1679ABC7A56 /* ContentLanguageSwitchProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentLanguageSwitchProvider.swift; sourceTree = ""; }; 62E98531F389566315A3D2EE /* CourseListDataFlow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CourseListDataFlow.swift; sourceTree = ""; }; 62E9854FAF9EE4F6763E65F8 /* CourseListOutputProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CourseListOutputProtocol.swift; sourceTree = ""; }; 62E9855F408D287FF5652C21 /* BaseCourseListFlowLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseCourseListFlowLayout.swift; sourceTree = ""; }; 62E98564B972DA13D0F00047 /* CourseListInputProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CourseListInputProtocol.swift; sourceTree = ""; }; 62E9856D3E54568A1415665B /* ContinueCourseSkeletonView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContinueCourseSkeletonView.swift; sourceTree = ""; }; + 62E985797A50F6A4FFBEAA91 /* CourseInfoTabInfoViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CourseInfoTabInfoViewModel.swift; sourceTree = ""; }; 62E985E77CD63F5564E0D042 /* CourseWidgetViewModelAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CourseWidgetViewModelAdapter.swift; sourceTree = ""; }; 62E985F73C3288F8D0045327 /* PinsMapBlockContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PinsMapBlockContentView.swift; sourceTree = ""; }; 62E98614E194A5CFD78CFF63 /* ExploreSearchBar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExploreSearchBar.swift; sourceTree = ""; }; @@ -6047,6 +6066,8 @@ 62E98719354649AAFC238BDA /* StepsServiceImpl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StepsServiceImpl.swift; sourceTree = ""; }; 62E98750EE80BFD97E4C7072 /* PinsMapPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PinsMapPresenter.swift; sourceTree = ""; }; 62E9875D99B2EB1592347745 /* CourseListsCollectionInteractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CourseListsCollectionInteractor.swift; sourceTree = ""; }; + 62E9876C6770A639F8C20BB8 /* CourseInfoTabInfoHeaderBlockSkeletonView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CourseInfoTabInfoHeaderBlockSkeletonView.swift; sourceTree = ""; }; + 62E9877223D790EAA5F8A291 /* CourseInfoTabInfoInstructorSkeletonView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CourseInfoTabInfoInstructorSkeletonView.swift; sourceTree = ""; }; 62E98778AE17D820A3E7A98B /* StepsPagerPresenterImpl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StepsPagerPresenterImpl.swift; sourceTree = ""; }; 62E987B9D418CC2175A64EC2 /* UserAgentTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserAgentTests.swift; sourceTree = ""; }; 62E98801330B94B34FA0D7B9 /* ProfileHeaderInfoView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ProfileHeaderInfoView.xib; sourceTree = ""; }; @@ -6074,6 +6095,7 @@ 62E98B29C9DE0ACE4A4CDF93 /* ContinueCourseViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContinueCourseViewModel.swift; sourceTree = ""; }; 62E98B474F4BBD5AC5AABB75 /* GradientCoursesPlaceholderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GradientCoursesPlaceholderView.swift; sourceTree = ""; }; 62E98B7A4296E04D057D679A /* SearchResultsLegacy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchResultsLegacy.swift; sourceTree = ""; }; + 62E98B7DBB7C240C8383AF31 /* CourseInfoTabInfoView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CourseInfoTabInfoView.swift; sourceTree = ""; }; 62E98B98A07DF176019D6089 /* CourseListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CourseListView.swift; sourceTree = ""; }; 62E98BB44346C9AD2F93EECC /* NibLoadable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NibLoadable.swift; sourceTree = ""; }; 62E98BB58C18BE0D002B4D91 /* CAGradientLayer+Locations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CAGradientLayer+Locations.swift"; sourceTree = ""; }; @@ -8215,6 +8237,7 @@ 100D33CD3AAC03A84EA5B18A /* Modules */ = { isa = PBXGroup; children = ( + 62E9829DC9329C319264FF39 /* CourseInfoTab */, 58D4DD2E993574238E9882A7 /* Explore */, DBA8A1094AD2004867D29D96 /* Tags */, 24FC00F459F731ED1F82F46F /* ContinueCourse */, @@ -8830,6 +8853,7 @@ 2C22042E20E27E120060117A /* Views */ = { isa = PBXGroup; children = ( + 62E988531B4005E22572A8CB /* CourseInfo */, 2C22042F20E27E400060117A /* ProfileCellSkeletonPlaceholderView.xib */, 2C22043120E2804F0060117A /* AchievementSkeletonPlaceholderView.xib */, 2C22043320E28AD50060117A /* AchievementListSkeletonPlaceholderView.xib */, @@ -10484,6 +10508,18 @@ name = StepikPlaceholderView; sourceTree = ""; }; + 2CF39A062195EF1700688A24 /* Blocks */ = { + isa = PBXGroup; + children = ( + 62E984497C7DD6033AC0B914 /* CourseInfoTabInfoHeaderBlockView.swift */, + 62E981C3381EF87D844F8E11 /* CourseInfoTabInfoInstructorsBlockView.swift */, + 62E98114DF11CFD4F5F75E79 /* CourseInfoTabInfoInstructorView.swift */, + 62E9809AEE7ED05C92852758 /* CourseInfoTabInfoIntroVideoBlockView.swift */, + 62E98229BCE62A9522A60FF7 /* CourseInfoTabInfoTextBlockView.swift */, + ); + path = Blocks; + sourceTree = ""; + }; 2CF79D332131A96800DA6953 /* LessonTableHeaderView */ = { isa = PBXGroup; children = ( @@ -10603,6 +10639,16 @@ path = CourseListsCollection; sourceTree = ""; }; + 62E9829DC9329C319264FF39 /* CourseInfoTab */ = { + isa = PBXGroup; + children = ( + 62E98523A1880164ADD69B1E /* CourseInfoTabInfoViewController.swift */, + 62E98A5F7CD1CD63FB4E169C /* View */, + 62E98BE667F37BA52271E8E9 /* ViewModel */, + ); + path = CourseInfoTab; + sourceTree = ""; + }; 62E982A1EDB07B5DD63EC36C /* PinsMap */ = { isa = PBXGroup; children = ( @@ -10688,6 +10734,16 @@ name = StreakNotificationsControl; sourceTree = ""; }; + 62E988531B4005E22572A8CB /* CourseInfo */ = { + isa = PBXGroup; + children = ( + 62E98454B9A6D4DB423DB94F /* CourseInfoTabInfoSkeletonView.swift */, + 62E9876C6770A639F8C20BB8 /* CourseInfoTabInfoHeaderBlockSkeletonView.swift */, + 62E9877223D790EAA5F8A291 /* CourseInfoTabInfoInstructorSkeletonView.swift */, + ); + name = CourseInfo; + sourceTree = ""; + }; 62E9892E87AAA27BEF0FCDF5 /* CourseList */ = { isa = PBXGroup; children = ( @@ -10745,6 +10801,15 @@ path = Views; sourceTree = ""; }; + 62E98A5F7CD1CD63FB4E169C /* View */ = { + isa = PBXGroup; + children = ( + 2CF39A062195EF1700688A24 /* Blocks */, + 62E98B7DBB7C240C8383AF31 /* CourseInfoTabInfoView.swift */, + ); + path = View; + sourceTree = ""; + }; 62E98AC3C03DF7EDB2FD4AB6 /* List */ = { isa = PBXGroup; children = ( @@ -10803,6 +10868,14 @@ path = View; sourceTree = ""; }; + 62E98BE667F37BA52271E8E9 /* ViewModel */ = { + isa = PBXGroup; + children = ( + 62E985797A50F6A4FFBEAA91 /* CourseInfoTabInfoViewModel.swift */, + ); + path = ViewModel; + sourceTree = ""; + }; 62E98BFBFFC145B059B56903 /* Protocols */ = { isa = PBXGroup; children = ( @@ -15036,6 +15109,17 @@ 62E982769C19C7852BCD59C4 /* TooltipStorageManager.swift in Sources */, 62E985DD90444821A835C02F /* CourseListsCollectionSkeletonView.swift in Sources */, 62E98A53567525055BCCA089 /* AppDelegate.swift in Sources */, + 62E98C27BDE26D9EAC473BF4 /* CourseInfoTabInfoView.swift in Sources */, + 62E982D9472B24E0A6C95984 /* CourseInfoTabInfoHeaderBlockView.swift in Sources */, + 62E98D796A09691AF36E4693 /* CourseInfoTabInfoTextBlockView.swift in Sources */, + 62E98889B9D4C66875D98BB6 /* CourseInfoTabInfoInstructorsBlockView.swift in Sources */, + 62E984095A966E6F531E0ECF /* CourseInfoTabInfoInstructorView.swift in Sources */, + 62E9877FB66EF5FA8531AFDC /* CourseInfoTabInfoViewModel.swift in Sources */, + 62E98E2254E014AE47F7E4C2 /* CourseInfoTabInfoIntroVideoBlockView.swift in Sources */, + 62E981F1071760511CFB0A61 /* CourseInfoTabInfoSkeletonView.swift in Sources */, + 62E989608052E8BA824C9A21 /* CourseInfoTabInfoHeaderBlockSkeletonView.swift in Sources */, + 62E987D46233B89387139183 /* CourseInfoTabInfoInstructorSkeletonView.swift in Sources */, + 62E98C98C821248326D854AA /* CourseInfoTabInfoViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Stepic/CourseInfoTabInfoHeaderBlockSkeletonView.swift b/Stepic/CourseInfoTabInfoHeaderBlockSkeletonView.swift new file mode 100644 index 0000000000..b8a78c78e4 --- /dev/null +++ b/Stepic/CourseInfoTabInfoHeaderBlockSkeletonView.swift @@ -0,0 +1,84 @@ +// +// CourseInfoTabInfoHeaderBlockSkeletonView.swift +// stepik-ios +// +// Created by Ivan Magda on 11/9/18. +// Copyright (c) 2018 Alex Karpov. All rights reserved. +// + +import UIKit +import SnapKit + +extension CourseInfoTabInfoHeaderBlockSkeletonView { + struct Appearance { + let imageViewSize = CGSize(width: 12, height: 12) + let imageViewCornerRadius: CGFloat = 1 + + let titleLabelHeight: CGFloat = 17 + let titleLabelLeadingOffset: CGFloat = 27 + var titleLabelCornerRadius: CGFloat = 1 + } +} + +final class CourseInfoTabInfoHeaderBlockSkeletonView: UIView { + let appearance: Appearance + + private lazy var imageViewSkeleton: UIView = { + let view = UIView() + view.clipsToBounds = true + view.layer.cornerRadius = self.appearance.imageViewCornerRadius + return view + }() + + private lazy var titleLabelSkeleton: UIView = { + let view = UIView() + view.clipsToBounds = true + view.layer.cornerRadius = self.appearance.titleLabelCornerRadius + return view + }() + + init( + frame: CGRect = .zero, + appearance: Appearance = Appearance() + ) { + self.appearance = appearance + super.init(frame: frame) + + self.setupView() + self.addSubviews() + self.makeConstraints() + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +extension CourseInfoTabInfoHeaderBlockSkeletonView: ProgrammaticallyInitializableViewProtocol { + func setupView() { + self.backgroundColor = .clear + } + + func addSubviews() { + self.addSubview(self.imageViewSkeleton) + self.addSubview(self.titleLabelSkeleton) + } + + func makeConstraints() { + self.imageViewSkeleton.translatesAutoresizingMaskIntoConstraints = false + self.imageViewSkeleton.snp.makeConstraints { make in + make.leading.equalToSuperview() + make.size.equalTo(self.appearance.imageViewSize) + make.centerY.equalTo(self.titleLabelSkeleton.snp.centerY) + } + + self.titleLabelSkeleton.translatesAutoresizingMaskIntoConstraints = false + self.titleLabelSkeleton.snp.makeConstraints { make in + make.top.trailing.bottom.equalToSuperview() + make.height.equalTo(self.appearance.titleLabelHeight) + make.leading + .equalToSuperview() + .offset(self.appearance.titleLabelLeadingOffset) + } + } +} diff --git a/Stepic/CourseInfoTabInfoInstructorSkeletonView.swift b/Stepic/CourseInfoTabInfoInstructorSkeletonView.swift new file mode 100644 index 0000000000..0f9939ec65 --- /dev/null +++ b/Stepic/CourseInfoTabInfoInstructorSkeletonView.swift @@ -0,0 +1,103 @@ +// +// CourseInfoTabInfoInstructorSkeletonView.swift +// stepik-ios +// +// Created by Ivan Magda on 11/9/18. +// Copyright (c) 2018 Alex Karpov. All rights reserved. +// + +import UIKit +import SnapKit + +extension CourseInfoTabInfoInstructorSkeletonView { + struct Appearance { + var labelsCornerRadius: CGFloat = 2 + + let imageViewCornerRadius: CGFloat = 5 + let imageViewSize = CGSize(width: 30, height: 30) + + let titleLabelHeight: CGFloat = 17 + let titleLabelOffsetLeading: CGFloat = 17 + + let descriptionLabelOffsetTop: CGFloat = 20 + let descriptionLabelHeight: CGFloat = 80 + } +} + +final class CourseInfoTabInfoInstructorSkeletonView: UIView { + let appearance: Appearance + + private lazy var imageViewSkeleton: UIView = { + let view = UIView() + view.clipsToBounds = true + view.layer.cornerRadius = self.appearance.imageViewCornerRadius + return view + }() + + private lazy var titleLabelSkeleton: UIView = { + let view = UIView() + view.clipsToBounds = true + view.layer.cornerRadius = self.appearance.labelsCornerRadius + return view + }() + + private lazy var descriptionLabelSkeleton: UIView = { + let view = UIView() + view.clipsToBounds = true + view.layer.cornerRadius = self.appearance.labelsCornerRadius + return view + }() + + init(frame: CGRect = .zero, appearance: Appearance = Appearance()) { + self.appearance = appearance + super.init(frame: frame) + + self.setupView() + self.addSubviews() + self.makeConstraints() + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +extension CourseInfoTabInfoInstructorSkeletonView: ProgrammaticallyInitializableViewProtocol { + func setupView() { + self.backgroundColor = .clear + } + + func addSubviews() { + self.addSubview(self.imageViewSkeleton) + self.addSubview(self.titleLabelSkeleton) + self.addSubview(self.descriptionLabelSkeleton) + } + + func makeConstraints() { + self.imageViewSkeleton.translatesAutoresizingMaskIntoConstraints = false + self.imageViewSkeleton.snp.makeConstraints { make in + make.size.equalTo(self.appearance.imageViewSize) + make.leading.top.equalToSuperview() + } + + self.titleLabelSkeleton.translatesAutoresizingMaskIntoConstraints = false + self.titleLabelSkeleton.snp.makeConstraints { make in + make.centerY.equalTo(self.imageViewSkeleton.snp.centerY) + make.trailing.equalToSuperview() + make.height.equalTo(self.appearance.titleLabelHeight) + make.leading + .equalTo(self.imageViewSkeleton.snp.trailing) + .offset(self.appearance.titleLabelOffsetLeading) + } + + self.descriptionLabelSkeleton.translatesAutoresizingMaskIntoConstraints = false + self.descriptionLabelSkeleton.snp.makeConstraints { make in + make.leading.equalTo(self.imageViewSkeleton.snp.leading) + make.trailing.bottom.equalToSuperview() + make.height.equalTo(self.appearance.descriptionLabelHeight) + make.top + .equalTo(self.imageViewSkeleton.snp.bottom) + .offset(self.appearance.descriptionLabelOffsetTop) + } + } +} diff --git a/Stepic/CourseInfoTabInfoSkeletonView.swift b/Stepic/CourseInfoTabInfoSkeletonView.swift new file mode 100644 index 0000000000..2e708f7b04 --- /dev/null +++ b/Stepic/CourseInfoTabInfoSkeletonView.swift @@ -0,0 +1,302 @@ +// +// CourseInfoTabInfoSkeletonView.swift +// stepik-ios +// +// Created by Ivan Magda on 11/7/18. +// Copyright (c) 2018 Alex Karpov. All rights reserved. +// + +import UIKit +import SnapKit + +extension CourseInfoTabInfoSkeletonView { + struct Appearance { + let labelsCornerRadius: CGFloat = 2 + let messageLabelsHeight: CGFloat = 37 + + let blockInsets = UIEdgeInsets(top: 40, left: 20, bottom: 0, right: 47) + let innerInsets = UIEdgeInsets(top: 20, left: 47, bottom: 0, right: 47) + + let authorViewInsets = UIEdgeInsets(top: 20, left: 20, bottom: 0, right: 47) + + let introVideoViewHeight: CGFloat = 203 + } +} + +final class CourseInfoTabInfoSkeletonView: UIView { + let appearance: Appearance + private let countInstructors: Int + + private lazy var authorViewSkeleton: UIView = { + CourseInfoTabInfoHeaderBlockSkeletonView( + appearance: .init(titleLabelCornerRadius: self.appearance.labelsCornerRadius) + ) + }() + + private lazy var introVideoViewSkeleton = UIView() + + private lazy var aboutHeaderViewSkeleton: UIView = { + CourseInfoTabInfoHeaderBlockSkeletonView( + appearance: .init(titleLabelCornerRadius: self.appearance.labelsCornerRadius) + ) + }() + private lazy var aboutMessageLabelSkeleton = UIView() + + private lazy var requirementsHeaderViewSkeleton: UIView = { + CourseInfoTabInfoHeaderBlockSkeletonView( + appearance: .init(titleLabelCornerRadius: self.appearance.labelsCornerRadius) + ) + }() + private lazy var requirementsMessageLabelSkeleton = UIView() + + private lazy var targetAudienceHeaderViewSkeleton: UIView = { + CourseInfoTabInfoHeaderBlockSkeletonView( + appearance: .init(titleLabelCornerRadius: self.appearance.labelsCornerRadius) + ) + }() + private lazy var targetAudienceMessageLabelSkeleton = UIView() + + private lazy var instructorsHeaderViewSkeleton: UIView = { + CourseInfoTabInfoHeaderBlockSkeletonView( + appearance: .init(titleLabelCornerRadius: self.appearance.labelsCornerRadius) + ) + }() + + private lazy var timeToCompleteHeaderViewSkeleton: UIView = { + CourseInfoTabInfoHeaderBlockSkeletonView( + appearance: .init(titleLabelCornerRadius: self.appearance.labelsCornerRadius) + ) + }() + private lazy var timeToCompleteMessageLabelSkeleton = UIView() + + private lazy var languageHeaderViewSkeleton: UIView = { + CourseInfoTabInfoHeaderBlockSkeletonView( + appearance: .init(titleLabelCornerRadius: self.appearance.labelsCornerRadius) + ) + }() + private lazy var languageMessageLabelSkeleton = UIView() + + private lazy var certificateHeaderViewSkeleton: UIView = { + CourseInfoTabInfoHeaderBlockSkeletonView( + appearance: .init(titleLabelCornerRadius: self.appearance.labelsCornerRadius) + ) + }() + private lazy var certificateMessageLabelSkeleton = UIView() + + private lazy var certificateDetailsHeaderViewSkeleton: UIView = { + CourseInfoTabInfoHeaderBlockSkeletonView( + appearance: .init(titleLabelCornerRadius: self.appearance.labelsCornerRadius) + ) + }() + private lazy var certificateDetailsMessageLabelSkeleton = UIView() + + // MARK: Init + + init(frame: CGRect = .zero, appearance: Appearance = Appearance(), countInstructors: Int = 2) { + self.appearance = appearance + self.countInstructors = min(countInstructors, 2) + super.init(frame: frame) + + self.setupView() + self.addSubviews() + self.makeConstraints() + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +extension CourseInfoTabInfoSkeletonView: ProgrammaticallyInitializableViewProtocol { + func setupView() { + func setCornerRadius(_ radius: CGFloat, views: [UIView]) { + views.forEach { view in + view.clipsToBounds = true + view.layer.cornerRadius = radius + } + } + + self.backgroundColor = .clear + + setCornerRadius( + self.appearance.labelsCornerRadius, + views: [ + self.aboutMessageLabelSkeleton, + self.requirementsMessageLabelSkeleton, + self.targetAudienceMessageLabelSkeleton, + self.timeToCompleteMessageLabelSkeleton, + self.languageMessageLabelSkeleton, + self.certificateMessageLabelSkeleton, + self.certificateDetailsMessageLabelSkeleton + ] + ) + } + + func addSubviews() { + self.addSubview(self.authorViewSkeleton) + + self.addSubview(self.introVideoViewSkeleton) + + self.addSubview(self.aboutHeaderViewSkeleton) + self.addSubview(self.aboutMessageLabelSkeleton) + + self.addSubview(self.requirementsHeaderViewSkeleton) + self.addSubview(self.requirementsMessageLabelSkeleton) + + self.addSubview(self.targetAudienceHeaderViewSkeleton) + self.addSubview(self.targetAudienceMessageLabelSkeleton) + + self.addSubview(self.instructorsHeaderViewSkeleton) + + self.addSubview(self.timeToCompleteHeaderViewSkeleton) + self.addSubview(self.timeToCompleteMessageLabelSkeleton) + + self.addSubview(self.languageHeaderViewSkeleton) + self.addSubview(self.languageMessageLabelSkeleton) + + self.addSubview(self.certificateHeaderViewSkeleton) + self.addSubview(self.certificateMessageLabelSkeleton) + + self.addSubview(self.certificateDetailsHeaderViewSkeleton) + self.addSubview(self.certificateDetailsMessageLabelSkeleton) + } + + func makeConstraints() { + self.authorViewSkeleton.translatesAutoresizingMaskIntoConstraints = false + self.authorViewSkeleton.snp.makeConstraints { make in + make.leading.equalToSuperview().offset(self.appearance.authorViewInsets.left) + make.top.equalToSuperview().offset(self.appearance.authorViewInsets.top) + make.trailing.equalToSuperview().offset(-self.appearance.authorViewInsets.right) + } + + self.introVideoViewSkeleton.translatesAutoresizingMaskIntoConstraints = false + self.introVideoViewSkeleton.snp.makeConstraints { make in + make.leading.trailing.equalToSuperview() + make.height.equalTo(self.appearance.introVideoViewHeight) + make.top + .equalTo(self.authorViewSkeleton.snp.bottom) + .offset(self.appearance.innerInsets.top) + } + + self.makeConstraintsForTextBlockView( + headerView: self.aboutHeaderViewSkeleton, + messageView: self.aboutMessageLabelSkeleton, + headerTopPinView: self.introVideoViewSkeleton + ) + + self.makeConstraintsForTextBlockView( + headerView: self.requirementsHeaderViewSkeleton, + messageView: self.requirementsMessageLabelSkeleton, + headerTopPinView: self.aboutMessageLabelSkeleton + ) + + self.makeConstraintsForTextBlockView( + headerView: self.targetAudienceHeaderViewSkeleton, + messageView: self.targetAudienceMessageLabelSkeleton, + headerTopPinView: self.requirementsMessageLabelSkeleton + ) + + self.instructorsHeaderViewSkeleton.translatesAutoresizingMaskIntoConstraints = false + self.instructorsHeaderViewSkeleton.snp.makeConstraints { make in + make.leading + .equalToSuperview() + .offset(self.appearance.blockInsets.left) + make.trailing + .equalToSuperview() + .offset(-self.appearance.blockInsets.right) + make.top + .equalTo(self.targetAudienceMessageLabelSkeleton.snp.bottom) + .offset(self.appearance.blockInsets.top) + } + + let lastInstructorView = self.addInstructorSkeletonViews()[self.countInstructors - 1] + + self.makeConstraintsForTextBlockView( + headerView: self.timeToCompleteHeaderViewSkeleton, + messageView: self.timeToCompleteMessageLabelSkeleton, + headerTopPinView: lastInstructorView + ) + + self.makeConstraintsForTextBlockView( + headerView: self.languageHeaderViewSkeleton, + messageView: self.languageMessageLabelSkeleton, + headerTopPinView: self.timeToCompleteMessageLabelSkeleton + ) + + self.makeConstraintsForTextBlockView( + headerView: self.certificateHeaderViewSkeleton, + messageView: self.certificateMessageLabelSkeleton, + headerTopPinView: self.languageMessageLabelSkeleton + ) + + self.makeConstraintsForTextBlockView( + headerView: self.certificateDetailsHeaderViewSkeleton, + messageView: self.certificateDetailsMessageLabelSkeleton, + headerTopPinView: self.certificateMessageLabelSkeleton + ) + } + + // MARK: Private Helpers + + private func makeConstraintsForTextBlockView( + headerView: UIView, + messageView: UIView, + headerTopPinView: UIView + ) { + headerView.translatesAutoresizingMaskIntoConstraints = false + headerView.snp.makeConstraints { make in + make.leading.equalToSuperview().offset(self.appearance.blockInsets.left) + make.trailing.equalToSuperview().offset(-self.appearance.blockInsets.right) + make.top + .equalTo(headerTopPinView.snp.bottom) + .offset(self.appearance.blockInsets.top) + } + + messageView.translatesAutoresizingMaskIntoConstraints = false + messageView.snp.makeConstraints { make in + make.leading.equalToSuperview().offset(self.appearance.innerInsets.left) + make.trailing.equalToSuperview().offset(-self.appearance.innerInsets.right) + make.height.equalTo(self.appearance.messageLabelsHeight) + make.top + .equalTo(headerView.snp.bottom) + .offset(self.appearance.innerInsets.top) + } + } + + private func addInstructorSkeletonViews() -> [UIView] { + var instructors = [UIView]() + var previous: UIView? + + for i in 0.. CourseInfoTabInfoViewModel { + return CourseInfoTabInfoViewModel( + author: "Yandex", + introVideoURL: URL(string: "https://player.vimeo.com/external/161974070.hd.mp4?s=19ff926134e7cbbc7e8ce161e3af9c3bb87d5c1a&profile_id=174&oauth2_token_id=3605157?playsinline=1"), + aboutText: "This course was designed for beginner java developers and people who'd like to learn functional approach to programming. If you are an expert in java or functional programming this course will seem too simple for you. It would be better for you to proceed to a more advanced course.", + requirementsText: "Basic knowledge of Java syntax, collections, OOP and pre-installed JDK 8+.", + targetAudienceText: "People who would like to improve their skills in java programming and to learn functional programming", + timeToCompleteText: "11 hours", + languageText: "English", + certificateText: "Yes", + certificateDetailsText: "Certificate condition: 50 points\nWith distinction: 75 points", + instructors: [ + .init( + avatarImageURL: URL(string: "https://www.w3schools.com/howto/img_avatar.png"), + title: "Artyom Burylov", + description: "Kotlin backend developer, online education enthusiast. I graduated from PNRPU with a BSc in Computer Science (2014) and MSc in Software Engineering (2016). During the learning, I took an active part in scientific conferences and educational events." + ), + .init( + avatarImageURL: URL(string: "https://www.w3schools.com/w3images/avatar2.png"), + title: "Tom Tom", + description: "Kotlin backend developer, online education enthusiast. I graduated from PNRPU with a BSc in Computer Science (2014) and MSc in Software Engineering (2016). During the learning, I took an active part in scientific conferences and educational events." + ) + ] + ) + } +} diff --git a/Stepic/Modules/CourseInfoTab/View/Blocks/CourseInfoTabInfoHeaderBlockView.swift b/Stepic/Modules/CourseInfoTab/View/Blocks/CourseInfoTabInfoHeaderBlockView.swift new file mode 100644 index 0000000000..e06613013e --- /dev/null +++ b/Stepic/Modules/CourseInfoTab/View/Blocks/CourseInfoTabInfoHeaderBlockView.swift @@ -0,0 +1,94 @@ +// +// CourseInfoTabInfoHeaderBlockView.swift +// stepik-ios +// +// Created by Ivan Magda on 11/1/18. +// Copyright © 2018 Alex Karpov. All rights reserved. +// + +import UIKit +import SnapKit + +extension CourseInfoTabInfoHeaderBlockView { + struct Appearance { + let imageViewSize = CGSize(width: 12, height: 12) + let imageViewTintColor = UIColor.mainDark + + let titleLabelFont = UIFont.systemFont(ofSize: 14, weight: .medium) + let titleLabelTextColor = UIColor.mainDark + let titleLabelLeadingSpace: CGFloat = 27 + } +} + +final class CourseInfoTabInfoHeaderBlockView: UIView { + let appearance: Appearance + + var icon: UIImage? { + didSet { + self.iconImageView.image = self.icon?.withRenderingMode(.alwaysTemplate) + } + } + + var title: String? { + didSet { + self.titleLabel.text = self.title + } + } + + private lazy var iconImageView: UIImageView = { + let imageView = UIImageView() + imageView.contentMode = .scaleAspectFit + imageView.tintColor = self.appearance.imageViewTintColor + return imageView + }() + + private lazy var titleLabel: UILabel = { + let label = UILabel() + label.font = self.appearance.titleLabelFont + label.textColor = self.appearance.titleLabelTextColor + return label + }() + + init( + frame: CGRect = .zero, + appearance: Appearance = Appearance() + ) { + self.appearance = appearance + super.init(frame: frame) + + self.setupView() + self.addSubviews() + self.makeConstraints() + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +extension CourseInfoTabInfoHeaderBlockView: ProgrammaticallyInitializableViewProtocol { + func setupView() { + self.backgroundColor = .white + } + + func addSubviews() { + self.addSubview(self.iconImageView) + self.addSubview(self.titleLabel) + } + + func makeConstraints() { + self.iconImageView.translatesAutoresizingMaskIntoConstraints = false + self.iconImageView.snp.makeConstraints { make in + make.height.equalTo(self.appearance.imageViewSize.height) + make.width.equalTo(self.appearance.imageViewSize.width) + make.leading.equalToSuperview() + make.centerY.equalToSuperview() + } + + self.titleLabel.translatesAutoresizingMaskIntoConstraints = false + self.titleLabel.snp.makeConstraints { make in + make.leading.equalToSuperview().offset(self.appearance.titleLabelLeadingSpace) + make.top.trailing.bottom.equalToSuperview() + } + } +} diff --git a/Stepic/Modules/CourseInfoTab/View/Blocks/CourseInfoTabInfoInstructorView.swift b/Stepic/Modules/CourseInfoTab/View/Blocks/CourseInfoTabInfoInstructorView.swift new file mode 100644 index 0000000000..646667afe8 --- /dev/null +++ b/Stepic/Modules/CourseInfoTab/View/Blocks/CourseInfoTabInfoInstructorView.swift @@ -0,0 +1,130 @@ +// +// CourseInfoTabInfoInstructorView.swift +// stepik-ios +// +// Created by Ivan Magda on 11/2/18. +// Copyright © 2018 Alex Karpov. All rights reserved. +// + +import UIKit +import SnapKit + +extension CourseInfoTabInfoInstructorView { + struct Appearance { + let imageFadeInDuration: TimeInterval = 0.15 + let imageViewSize = CGSize(width: 30, height: 30) + let imageViewCornerRadius: CGFloat = 5 + + let titleLabelInsets = UIEdgeInsets(top: 0, left: 17, bottom: 0, right: 0) + let titleLabelFont = UIFont.systemFont(ofSize: 14, weight: .medium) + let titleLabelTextColor = UIColor.mainDark + + let descriptionLabelInsets = UIEdgeInsets(top: 20, left: 0, bottom: 0, right: 0) + let descriptionLabelFont = UIFont.systemFont(ofSize: 12, weight: .light) + let descriptionLabelTextColor = UIColor.mainDark + } +} + +final class CourseInfoTabInfoInstructorView: UIView { + let appearance: Appearance + + var title: String? { + didSet { + self.titleLabel.text = self.title + } + } + + var summary: String? { + didSet { + self.descriptionLabel.text = self.summary + } + } + + var avatarImageURL: URL? { + didSet { + self.imageView.loadImage(url: self.avatarImageURL) + } + } + + private lazy var imageView: CourseCoverImageView = { + let imageView = CourseCoverImageView( + appearance: .init( + placeholderImage: UIImage(), + cornerRadius: self.appearance.imageViewCornerRadius, + imageFadeInDuration: self.appearance.imageFadeInDuration + ) + ) + imageView.contentMode = .scaleAspectFit + return imageView + }() + + private lazy var titleLabel: UILabel = { + let label = UILabel() + label.numberOfLines = 1 + label.font = self.appearance.titleLabelFont + label.textColor = self.appearance.titleLabelTextColor + return label + }() + + private lazy var descriptionLabel: UILabel = { + let label = UILabel() + label.numberOfLines = 0 + label.font = self.appearance.descriptionLabelFont + label.textColor = self.appearance.descriptionLabelTextColor + return label + }() + + init( + frame: CGRect = .zero, + appearance: Appearance = Appearance() + ) { + self.appearance = appearance + super.init(frame: frame) + + self.setupView() + self.addSubviews() + self.makeConstraints() + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +extension CourseInfoTabInfoInstructorView: ProgrammaticallyInitializableViewProtocol { + func setupView() { + self.backgroundColor = .white + } + + func addSubviews() { + self.addSubview(self.imageView) + self.addSubview(self.titleLabel) + self.addSubview(self.descriptionLabel) + } + + func makeConstraints() { + self.imageView.translatesAutoresizingMaskIntoConstraints = false + self.imageView.snp.makeConstraints { make in + make.size.equalTo(self.appearance.imageViewSize) + make.leading.top.equalToSuperview() + } + + self.titleLabel.translatesAutoresizingMaskIntoConstraints = false + self.titleLabel.snp.makeConstraints { make in + make.trailing.equalToSuperview() + make.centerY.equalTo(self.imageView.snp.centerY) + make.leading + .equalTo(self.imageView.snp.trailing) + .offset(self.appearance.titleLabelInsets.left) + } + + self.descriptionLabel.translatesAutoresizingMaskIntoConstraints = false + self.descriptionLabel.snp.makeConstraints { make in + make.trailing.bottom.equalToSuperview() + make.leading.equalTo(self.imageView.snp.leading) + make.top + .equalTo(self.imageView.snp.bottom) + .offset(self.appearance.descriptionLabelInsets.top) + } + } +} diff --git a/Stepic/Modules/CourseInfoTab/View/Blocks/CourseInfoTabInfoInstructorsBlockView.swift b/Stepic/Modules/CourseInfoTab/View/Blocks/CourseInfoTabInfoInstructorsBlockView.swift new file mode 100644 index 0000000000..6351f84b20 --- /dev/null +++ b/Stepic/Modules/CourseInfoTab/View/Blocks/CourseInfoTabInfoInstructorsBlockView.swift @@ -0,0 +1,96 @@ +// +// CourseInfoTabInfoInstructorsBlockView.swift +// stepik-ios +// +// Created by Ivan Magda on 11/1/18. +// Copyright © 2018 Alex Karpov. All rights reserved. +// + +import UIKit +import SnapKit + +extension CourseInfoTabInfoInstructorsBlockView { + struct Appearance { + var headerViewInsets = UIEdgeInsets(top: 40, left: 20, bottom: 0, right: 47) + + let stackViewInsets = UIEdgeInsets(top: 20, left: 47, bottom: 0, right: 47) + let stackViewSpacing: CGFloat = 20 + } +} + +final class CourseInfoTabInfoInstructorsBlockView: UIView { + let appearance: Appearance + + private lazy var headerView = CourseInfoTabInfoHeaderBlockView() + + private lazy var stackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .vertical + stackView.spacing = self.appearance.stackViewSpacing + return stackView + }() + + init( + frame: CGRect = .zero, + appearance: Appearance = Appearance() + ) { + self.appearance = appearance + super.init(frame: frame) + + self.setupView() + self.addSubviews() + self.makeConstraints() + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func configure(instructors: [CourseInfoTabInfoInstructorViewModel]) { + self.headerView.icon = CourseInfoTabInfoView.Block.instructors.icon + self.headerView.title = CourseInfoTabInfoView.Block.instructors.title + + if !self.stackView.arrangedSubviews.isEmpty { + self.stackView.removeAllArrangedSubviews() + } + + instructors.forEach { instructor in + let view = CourseInfoTabInfoInstructorView() + view.avatarImageURL = instructor.avatarImageURL + view.title = instructor.title + view.summary = instructor.description + + self.stackView.addArrangedSubview(view) + } + } +} + +extension CourseInfoTabInfoInstructorsBlockView: ProgrammaticallyInitializableViewProtocol { + func setupView() { + self.backgroundColor = .white + } + + func addSubviews() { + self.addSubview(self.headerView) + self.addSubview(self.stackView) + } + + func makeConstraints() { + self.headerView.translatesAutoresizingMaskIntoConstraints = false + self.headerView.snp.makeConstraints { make in + make.leading.equalToSuperview().offset(self.appearance.headerViewInsets.left) + make.top.equalToSuperview().offset(self.appearance.headerViewInsets.top) + make.trailing.equalToSuperview().offset(-self.appearance.headerViewInsets.right) + } + + self.stackView.translatesAutoresizingMaskIntoConstraints = false + self.stackView.snp.makeConstraints { make in + make.leading.equalToSuperview().offset(self.appearance.stackViewInsets.left) + make.trailing.equalToSuperview().offset(-self.appearance.stackViewInsets.right) + make.bottom.equalToSuperview().offset(self.appearance.stackViewInsets.bottom) + make.top + .equalTo(self.headerView.snp.bottom) + .offset(self.appearance.stackViewInsets.top) + } + } +} diff --git a/Stepic/Modules/CourseInfoTab/View/Blocks/CourseInfoTabInfoIntroVideoBlockView.swift b/Stepic/Modules/CourseInfoTab/View/Blocks/CourseInfoTabInfoIntroVideoBlockView.swift new file mode 100644 index 0000000000..b24c73c6a1 --- /dev/null +++ b/Stepic/Modules/CourseInfoTab/View/Blocks/CourseInfoTabInfoIntroVideoBlockView.swift @@ -0,0 +1,61 @@ +// +// CourseInfoTabInfoIntroVideoBlockView.swift +// stepik-ios +// +// Created by Ivan Magda on 11/9/18. +// Copyright (c) 2018 Alex Karpov. All rights reserved. +// + +import UIKit +import SnapKit + +extension CourseInfoTabInfoIntroVideoBlockView { + struct Appearance { + let introVideoHeight: CGFloat = 203 + } +} + +final class CourseInfoTabInfoIntroVideoBlockView: UIView { + let appearance: Appearance + + private lazy var previewImageView: UIImageView = { + let imageView = UIImageView(image: UIImage(named: "new-coursepics-python-xl")) + imageView.contentMode = .scaleAspectFill + imageView.clipsToBounds = true + return imageView + }() + + init( + frame: CGRect = .zero, + appearance: Appearance = Appearance() + ) { + self.appearance = appearance + super.init(frame: frame) + + self.setupView() + self.addSubviews() + self.makeConstraints() + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +extension CourseInfoTabInfoIntroVideoBlockView: ProgrammaticallyInitializableViewProtocol { + func setupView() { + self.backgroundColor = .white + } + + func addSubviews() { + self.addSubview(self.previewImageView) + } + + func makeConstraints() { + self.previewImageView.translatesAutoresizingMaskIntoConstraints = false + self.previewImageView.snp.makeConstraints { make in + make.edges.equalToSuperview() + make.height.equalTo(self.appearance.introVideoHeight) + } + } +} diff --git a/Stepic/Modules/CourseInfoTab/View/Blocks/CourseInfoTabInfoTextBlockView.swift b/Stepic/Modules/CourseInfoTab/View/Blocks/CourseInfoTabInfoTextBlockView.swift new file mode 100644 index 0000000000..9a96735fb9 --- /dev/null +++ b/Stepic/Modules/CourseInfoTab/View/Blocks/CourseInfoTabInfoTextBlockView.swift @@ -0,0 +1,87 @@ +// +// CourseInfoTabInfoTextBlockView.swift +// stepik-ios +// +// Created by Ivan Magda on 11/1/18. +// Copyright © 2018 Alex Karpov. All rights reserved. +// + +import UIKit +import SnapKit + +extension CourseInfoTabInfoTextBlockView { + struct Appearance { + var headerViewInsets = UIEdgeInsets(top: 40, left: 20, bottom: 0, right: 47) + + let messageLabelInsets = UIEdgeInsets(top: 20, left: 47, bottom: 0, right: 47) + let messageLabelFont = UIFont.systemFont(ofSize: 12, weight: .light) + let messageLabelTextColor = UIColor.mainDark + } +} + +final class CourseInfoTabInfoTextBlockView: UIView { + let appearance: Appearance + + var message: String? { + didSet { + self.messageLabel.text = self.message + } + } + + // TODO: Make private + lazy var headerView = CourseInfoTabInfoHeaderBlockView() + + private lazy var messageLabel: UILabel = { + let label = UILabel() + label.numberOfLines = 0 + label.font = self.appearance.messageLabelFont + label.textColor = self.appearance.messageLabelTextColor + return label + }() + + init( + frame: CGRect = .zero, + appearance: Appearance = Appearance() + ) { + self.appearance = appearance + super.init(frame: frame) + + self.setupView() + self.addSubviews() + self.makeConstraints() + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +extension CourseInfoTabInfoTextBlockView: ProgrammaticallyInitializableViewProtocol { + func setupView() { + self.backgroundColor = .white + } + + func addSubviews() { + self.addSubview(self.headerView) + self.addSubview(self.messageLabel) + } + + func makeConstraints() { + self.headerView.translatesAutoresizingMaskIntoConstraints = false + self.headerView.snp.makeConstraints { make in + make.leading.equalToSuperview().offset(self.appearance.headerViewInsets.left) + make.top.equalToSuperview().offset(self.appearance.headerViewInsets.top) + make.trailing.equalToSuperview().offset(-self.appearance.headerViewInsets.right) + } + + self.messageLabel.translatesAutoresizingMaskIntoConstraints = false + self.messageLabel.snp.makeConstraints { make in + make.leading.equalToSuperview().offset(self.appearance.messageLabelInsets.left) + make.bottom.equalToSuperview().offset(self.appearance.messageLabelInsets.bottom) + make.trailing.equalTo(self.headerView) + make.top + .equalTo(self.headerView.snp.bottom) + .offset(self.appearance.messageLabelInsets.top) + } + } +} diff --git a/Stepic/Modules/CourseInfoTab/View/CourseInfoTabInfoView.swift b/Stepic/Modules/CourseInfoTab/View/CourseInfoTabInfoView.swift new file mode 100644 index 0000000000..b53b55d8b3 --- /dev/null +++ b/Stepic/Modules/CourseInfoTab/View/CourseInfoTabInfoView.swift @@ -0,0 +1,261 @@ +// +// CourseInfoTabInfoView.swift +// stepik-ios +// +// Created by Ivan Magda on 11/1/18. +// Copyright © 2018 Alex Karpov. All rights reserved. +// + +import UIKit +import SnapKit + +protocol CourseInfoTabInfoViewDelegate: class { + func courseInfoTabInfoViewDidTapOnJoin(_ courseInfoTabInfoView: CourseInfoTabInfoView) +} + +extension CourseInfoTabInfoView { + struct Appearance { + let stackViewSpacing: CGFloat = 0 + + let blockInsets = UIEdgeInsets(top: 40, left: 20, bottom: 0, right: 47) + let innerInsets = UIEdgeInsets(top: 20, left: 20, bottom: 0, right: 47) + + let joinButtonInsets = UIEdgeInsets(top: 40, left: 47, bottom: 40, right: 47) + let joinButtonHeight: CGFloat = 47 + let joinButtonBackgroundColor = UIColor.stepicGreen + let joinButtonFont = UIFont.systemFont(ofSize: 14) + let joinButtonTextColor = UIColor.white + let joinButtonCornerRadius: CGFloat = 7 + } +} + +final class CourseInfoTabInfoView: UIView { + weak var delegate: CourseInfoTabInfoViewDelegate? + + let appearance: Appearance + + private lazy var scrollableStackView: ScrollableStackView = { + let stackView = ScrollableStackView(frame: .zero, orientation: .vertical) + stackView.showsVerticalScrollIndicator = false + stackView.showsHorizontalScrollIndicator = false + stackView.spacing = self.appearance.stackViewSpacing + return stackView + }() + + private lazy var joinButton: UIButton = { + let button = UIButton(type: .system) + button.backgroundColor = self.appearance.joinButtonBackgroundColor + button.titleLabel?.font = self.appearance.joinButtonFont + button.tintColor = self.appearance.joinButtonTextColor + button.layer.cornerRadius = self.appearance.joinButtonCornerRadius + + button.setTitle(NSLocalizedString("JoinCourse", comment: ""), for: .normal) + button.addTarget( + self, + action: #selector(self.joinButtonClicked(sender:)), + for: .touchUpInside + ) + + return button + }() + + // MARK: Init + + init( + frame: CGRect = .zero, + appearance: Appearance = Appearance(), + delegate: CourseInfoTabInfoViewDelegate? = nil + ) { + self.appearance = appearance + self.delegate = delegate + super.init(frame: frame) + + self.setupView() + self.addSubviews() + self.makeConstraints() + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: Public API + + func showLoading() { + self.skeleton.viewBuilder = { + CourseInfoTabInfoSkeletonView() + } + self.skeleton.show() + } + + func hideLoading() { + self.skeleton.hide() + } + + func configure(viewModel: CourseInfoTabInfoViewModel) { + if !self.scrollableStackView.arrangedSubviews.isEmpty { + self.scrollableStackView.removeAllArrangedViews() + } + + self.addAuthorView(author: viewModel.author) + self.addIntroVideoView(introVideoURL: viewModel.introVideoURL) + + self.addTextBlockView(block: .about, message: viewModel.aboutText) + self.addTextBlockView(block: .requirements, message: viewModel.requirementsText) + self.addTextBlockView(block: .targetAudience, message: viewModel.targetAudienceText) + + self.addInstructorsView(instructors: viewModel.instructors) + + self.addTextBlockView(block: .timeToComplete, message: viewModel.timeToCompleteText) + self.addTextBlockView(block: .language, message: viewModel.languageText) + self.addTextBlockView(block: .certificate, message: viewModel.certificateText) + self.addTextBlockView(block: .certificateDetails, message: viewModel.certificateDetailsText) + + self.addJoinButton() + } + + // MARK: Actions + + @objc + private func joinButtonClicked(sender: UIButton) { + self.delegate?.courseInfoTabInfoViewDidTapOnJoin(self) + } + + // MARK: Private API + + private func addAuthorView(author: String) { + let authorView = CourseInfoTabInfoTextBlockView( + appearance: .init(headerViewInsets: self.appearance.innerInsets) + ) + authorView.headerView.icon = Block.author.icon + authorView.headerView.title = "\(Block.author.title) \(author)" + + self.scrollableStackView.addArrangedView(authorView) + } + + private func addTextBlockView( + block: Block, + message: String, + headerViewInsets: UIEdgeInsets = Appearance().blockInsets + ) { + let textBlockView = CourseInfoTabInfoTextBlockView( + appearance: .init(headerViewInsets: headerViewInsets) + ) + textBlockView.headerView.icon = block.icon + textBlockView.headerView.title = block.title + textBlockView.message = message + + self.scrollableStackView.addArrangedView(textBlockView) + } + + private func addIntroVideoView(introVideoURL: URL?) { + self.scrollableStackView.addArrangedView(CourseInfoTabInfoIntroVideoBlockView()) + } + + private func addInstructorsView(instructors: [CourseInfoTabInfoInstructorViewModel]) { + let instructorsView = CourseInfoTabInfoInstructorsBlockView() + instructorsView.configure(instructors: instructors) + self.scrollableStackView.addArrangedView(instructorsView) + } + + private func addJoinButton() { + let buttonContainer = UIView() + buttonContainer.translatesAutoresizingMaskIntoConstraints = false + self.joinButton.translatesAutoresizingMaskIntoConstraints = false + buttonContainer.addSubview(self.joinButton) + + self.scrollableStackView.addArrangedView(buttonContainer) + self.joinButton.snp.makeConstraints { make in + make.height.equalTo(self.appearance.joinButtonHeight) + make.leading.top.trailing.bottom + .equalToSuperview() + .inset(self.appearance.joinButtonInsets) + } + } +} + +// MARK: - CourseInfoTabInfoView: ProgrammaticallyInitializableViewProtocol - + +extension CourseInfoTabInfoView: ProgrammaticallyInitializableViewProtocol { + func setupView() { + self.backgroundColor = .white + } + + func addSubviews() { + self.addSubview(self.scrollableStackView) + } + + func makeConstraints() { + self.scrollableStackView.translatesAutoresizingMaskIntoConstraints = false + self.scrollableStackView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + } +} + +// MARK: - CourseInfoTabInfoView (Block) - + +extension CourseInfoTabInfoView { + enum Block { + case author + case introVideo + case about + case requirements + case targetAudience + case instructors + case timeToComplete + case language + case certificate + case certificateDetails + + var icon: UIImage? { + switch self { + case .author: + return UIImage(named: "course-info-instructor") + case .introVideo: + return nil + case .about: + return UIImage(named: "course-info-about") + case .requirements: + return UIImage(named: "course-info-requirements") + case .targetAudience: + return UIImage(named: "course-info-target-audience") + case .instructors: + return UIImage(named: "course-info-instructor") + case .timeToComplete: + return UIImage(named: "course-info-time-to-complete") + case .language: + return UIImage(named: "course-info-language") + case .certificate: + return UIImage(named: "course-info-certificate") + case .certificateDetails: + return UIImage(named: "course-info-certificate-details") + } + } + + var title: String { + switch self { + case .author: + return NSLocalizedString("CourseInfoTitleAuthor", comment: "") + case .introVideo: + return "" + case .about: + return NSLocalizedString("CourseInfoTitleAbout", comment: "") + case .requirements: + return NSLocalizedString("CourseInfoTitleRequirements", comment: "") + case .targetAudience: + return NSLocalizedString("CourseInfoTitleTargetAudience", comment: "") + case .instructors: + return NSLocalizedString("CourseInfoTitleInstructors", comment: "") + case .timeToComplete: + return NSLocalizedString("CourseInfoTitleTimeToComplete", comment: "") + case .language: + return NSLocalizedString("CourseInfoTitleLanguage", comment: "") + case .certificate: + return NSLocalizedString("CourseInfoTitleCertificate", comment: "") + case .certificateDetails: + return NSLocalizedString("CourseInfoTitleCertificateDetails", comment: "") + } + } + } +} diff --git a/Stepic/Modules/CourseInfoTab/ViewModel/CourseInfoTabInfoViewModel.swift b/Stepic/Modules/CourseInfoTab/ViewModel/CourseInfoTabInfoViewModel.swift new file mode 100644 index 0000000000..95b1f749b1 --- /dev/null +++ b/Stepic/Modules/CourseInfoTab/ViewModel/CourseInfoTabInfoViewModel.swift @@ -0,0 +1,31 @@ +// +// CourseInfoTabInfoViewModel.swift +// stepik-ios +// +// Created by Ivan Magda on 11/2/18. +// Copyright © 2018 Alex Karpov. All rights reserved. +// + +import UIKit + +struct CourseInfoTabInfoViewModel { + let author: String + let introVideoURL: URL? + + let aboutText: String + let requirementsText: String + let targetAudienceText: String + + let timeToCompleteText: String + let languageText: String + let certificateText: String + let certificateDetailsText: String + + let instructors: [CourseInfoTabInfoInstructorViewModel] +} + +struct CourseInfoTabInfoInstructorViewModel { + let avatarImageURL: URL? + let title: String + let description: String +} diff --git a/Stepic/Modules/Home/HomeViewController.swift b/Stepic/Modules/Home/HomeViewController.swift index abeee1433d..68296385f5 100644 --- a/Stepic/Modules/Home/HomeViewController.swift +++ b/Stepic/Modules/Home/HomeViewController.swift @@ -51,6 +51,23 @@ final class HomeViewController: BaseExploreViewController { override func viewDidLoad() { super.viewDidLoad() self.homeInteractor?.loadContent(request: .init()) + + // TODO: Remove + self.navigationItem.rightBarButtonItem = UIBarButtonItem( + title: "Course Info", + style: .plain, + target: self, + action: #selector(openCourseInfo) + ) + } + + // TODO: Remove + @objc + private func openCourseInfo() { + self.navigationController?.pushViewController( + CourseInfoTabInfoViewController(), + animated: true + ) } override func viewDidAppear(_ animated: Bool) { diff --git a/Stepic/en.lproj/Localizable.strings b/Stepic/en.lproj/Localizable.strings index b8c4b71bee..3f5c94ed5c 100644 --- a/Stepic/en.lproj/Localizable.strings +++ b/Stepic/en.lproj/Localizable.strings @@ -429,6 +429,17 @@ NotificationTabNotificationRequestAlertMessage = "We are trying to make learning CourseSubscriptionNotificationRequestAlertTitle = "Stay tuned"; CourseSubscriptionNotificationRequestAlertMessage = "We are trying to make learning process effective and comfortable. Enable notifications to follow deadlines and promptly receive course updates?"; +/* Course info */ +CourseInfoTitleAuthor = "by"; +CourseInfoTitleAbout = "About"; +CourseInfoTitleRequirements = "Requirements"; +CourseInfoTitleTargetAudience = "Target audience"; +CourseInfoTitleInstructors = "Instructors"; +CourseInfoTitleTimeToComplete = "Expected time to complete"; +CourseInfoTitleLanguage = "Language"; +CourseInfoTitleCertificate = "Certificate"; +CourseInfoTitleCertificateDetails = "Certificate details"; + /* Arts */ ArtCustomizeLearningProcess = "art_customize_learning_process_en"; diff --git a/Stepic/ru.lproj/Localizable.strings b/Stepic/ru.lproj/Localizable.strings index 73a2894963..386c84d052 100644 --- a/Stepic/ru.lproj/Localizable.strings +++ b/Stepic/ru.lproj/Localizable.strings @@ -431,6 +431,17 @@ NotificationTabNotificationRequestAlertMessage = "Мы пытаемся сдел CourseSubscriptionNotificationRequestAlertTitle = "Следи за обновлениями"; CourseSubscriptionNotificationRequestAlertMessage = "Мы пытаемся сделать процесс обучения максимально удобным и полезным. Разрешить уведомления, чтобы следить за дедлайнами и оперативно получать обновления по курсу?"; +/* Course info */ +CourseInfoTitleAuthor = "от"; +CourseInfoTitleAbout = "О курсе"; +CourseInfoTitleRequirements = "Требования"; +CourseInfoTitleTargetAudience = "Целевая аудитория"; +CourseInfoTitleInstructors = "Преподаватели"; +CourseInfoTitleTimeToComplete = "Время прохождения курса"; +CourseInfoTitleLanguage = "Язык"; +CourseInfoTitleCertificate = "Сертификат"; +CourseInfoTitleCertificateDetails = "Подробнее о сертификате"; + /* Arts */ ArtCustomizeLearningProcess = "art_customize_learning_process_ru";