From 0231c12de52a8c185812a5bb063e280d1c4041ae Mon Sep 17 00:00:00 2001 From: Ivan Magda Date: Tue, 15 Feb 2022 11:00:15 +0300 Subject: [PATCH 01/13] Model module (#1117) * Initial commit * Update .swift-version * Add CocoaPods support * Make APIEndpoint.name class variable * StepikModel add extensions * StepikModel add WishlistEntry * Update Meta * Add first Decodable API * Migrate Wishlist API * Clean up DecodedObjectsResponse --- .swift-version | 2 +- Podfile | 2 + Podfile.lock | 7 +- Stepic.xcodeproj/project.pbxproj | 20 ++++- .../WishlistEntryEntity.swift | 9 +- .../Decodable/DecodedObjectsResponse.swift | 65 +++++++++++++++ .../Model/Network/Endpoints/APIEndpoint.swift | 6 +- .../Endpoints/AchievementProgressesAPI.swift | 6 +- .../Network/Endpoints/AchievementsAPI.swift | 6 +- .../Endpoints/AdaptiveRatingsAPI.swift | 6 +- .../Endpoints/AdaptiveRatingsRestoreAPI.swift | 4 +- .../Network/Endpoints/AnnouncementsAPI.swift | 10 +-- .../Network/Endpoints/AssignmentsAPI.swift | 4 +- .../Model/Network/Endpoints/AttemptsAPI.swift | 6 +- .../Network/Endpoints/CatalogBlocksAPI.swift | 10 +-- .../Network/Endpoints/CertificatesAPI.swift | 8 +- .../Model/Network/Endpoints/CommentsAPI.swift | 12 +-- .../Endpoints/CourseBeneficiariesAPI.swift | 6 +- .../Endpoints/CourseBenefitByMonthsAPI.swift | 6 +- .../Endpoints/CourseBenefitSummariesAPI.swift | 6 +- .../Network/Endpoints/CourseBenefitsAPI.swift | 10 +-- .../Network/Endpoints/CourseListsAPI.swift | 14 ++-- .../Network/Endpoints/CoursePaymentsAPI.swift | 8 +- .../Endpoints/CoursePurchasesAPI.swift | 6 +- .../Endpoints/CourseRecommendationsAPI.swift | 6 +- .../Endpoints/CourseReviewSummariesAPI.swift | 4 +- .../Network/Endpoints/CourseReviewsAPI.swift | 28 +++---- .../Model/Network/Endpoints/CoursesAPI.swift | 4 +- .../Model/Network/Endpoints/DevicesAPI.swift | 8 +- .../Endpoints/DiscussionProxiesAPI.swift | 2 +- .../Endpoints/DiscussionThreadsAPI.swift | 6 +- .../Network/Endpoints/EmailAddressesAPI.swift | 6 +- .../Network/Endpoints/EnrollmentsAPI.swift | 6 +- .../Network/Endpoints/ExamSessionsAPI.swift | 6 +- .../Network/Endpoints/InstructionsAPI.swift | 6 +- .../Network/Endpoints/LastStepsAPI.swift | 4 +- .../Model/Network/Endpoints/LessonsAPI.swift | 2 +- .../Network/Endpoints/MagicLinksAPI.swift | 4 +- .../Model/Network/Endpoints/MetricsAPI.swift | 4 +- .../Network/Endpoints/MobileTiersAPI.swift | 6 +- .../Endpoints/NotificationStatusesAPI.swift | 6 +- .../Network/Endpoints/NotificationsAPI.swift | 10 +-- .../Endpoints/ProctorSessionsAPI.swift | 6 +- .../Model/Network/Endpoints/ProfilesAPI.swift | 4 +- .../Network/Endpoints/ProgressesAPI.swift | 4 +- .../Network/Endpoints/PromoCodesAPI.swift | 4 +- .../Model/Network/Endpoints/QueriesAPI.swift | 2 +- .../Endpoints/RecommendationsAPI.swift | 4 +- .../Network/Endpoints/ReviewSessionsAPI.swift | 10 +-- .../Model/Network/Endpoints/ReviewsAPI.swift | 8 +- .../Network/Endpoints/SearchResultsAPI.swift | 8 +- .../Model/Network/Endpoints/SectionsAPI.swift | 4 +- .../Network/Endpoints/SocialProfilesAPI.swift | 6 +- .../Network/Endpoints/StepSourcesAPI.swift | 8 +- .../Model/Network/Endpoints/StepicsAPI.swift | 4 +- .../Model/Network/Endpoints/StepsAPI.swift | 2 +- .../Network/Endpoints/StorageRecordsAPI.swift | 16 ++-- .../Network/Endpoints/StoryTemplatesAPI.swift | 10 +-- .../Network/Endpoints/SubmissionsAPI.swift | 4 +- .../Model/Network/Endpoints/UnitsAPI.swift | 8 +- .../Network/Endpoints/UserActivitiesAPI.swift | 6 +- .../Network/Endpoints/UserCodeRunsAPI.swift | 8 +- .../Network/Endpoints/UserCoursesAPI.swift | 12 +-- .../Model/Network/Endpoints/UsersAPI.swift | 4 +- .../Model/Network/Endpoints/ViewsAPI.swift | 4 +- .../Network/Endpoints/VisitedCoursesAPI.swift | 6 +- .../Model/Network/Endpoints/VotesAPI.swift | 4 +- .../Network/Endpoints/WishListsAPI.swift | 75 +++++++---------- Stepic/Legacy/Model/Network/Meta.swift | 31 +++---- .../RequestMakers/CreateRequestMaker.swift | 47 +++++++++++ .../RequestMakers/RetrieveRequestMaker.swift | 83 ++++++++++++++++++- .../WishlistEntryPlainObject.swift | 37 --------- .../Provider/CourseListNetworkService.swift | 4 +- .../Network/WishListsNetworkService.swift | 17 ++-- .../WishlistEntriesPersistenceService.swift | 9 +- .../Repository/WishlistRepository.swift | 7 +- StepikModel/.gitignore | 7 ++ .../xcshareddata/IDEWorkspaceChecks.plist | 8 ++ StepikModel/CocoaPodsModule.swift | 1 + StepikModel/Package.swift | 30 +++++++ StepikModel/README.md | 3 + StepikModel/Sources/BundleResolver.swift | 9 ++ .../Sources/Extensions/DateExtensions.swift | 40 +++++++++ .../KeyedDecodingContainerExtensions.swift | 22 +++++ .../Sources/StepikModel/WishlistEntry.swift | 36 ++++++++ StepikModel/StepikModel.podspec | 23 +++++ .../DateExtensionsTests.swift | 20 +++++ .../StepikModelTests/WishlistEntryTests.swift | 31 +++++++ 88 files changed, 718 insertions(+), 335 deletions(-) create mode 100644 Stepic/Legacy/Model/Network/Decodable/DecodedObjectsResponse.swift delete mode 100644 Stepic/Legacy/Model/PlainObjects/WishlistEntryPlainObject.swift create mode 100644 StepikModel/.gitignore create mode 100644 StepikModel/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 StepikModel/CocoaPodsModule.swift create mode 100644 StepikModel/Package.swift create mode 100644 StepikModel/README.md create mode 100644 StepikModel/Sources/BundleResolver.swift create mode 100644 StepikModel/Sources/Extensions/DateExtensions.swift create mode 100644 StepikModel/Sources/Extensions/KeyedDecodingContainerExtensions.swift create mode 100644 StepikModel/Sources/StepikModel/WishlistEntry.swift create mode 100644 StepikModel/StepikModel.podspec create mode 100644 StepikModel/Tests/StepikModelTests/DateExtensionsTests.swift create mode 100644 StepikModel/Tests/StepikModelTests/WishlistEntryTests.swift diff --git a/.swift-version b/.swift-version index 8ae03c1190..e4d41db984 100644 --- a/.swift-version +++ b/.swift-version @@ -1 +1 @@ -5.4.2 +5.5.2 diff --git a/Podfile b/Podfile index 6d63a191e7..602dbce790 100644 --- a/Podfile +++ b/Podfile @@ -13,6 +13,8 @@ project 'Stepic', 'Develop Release' => :release def shared_pods + pod 'StepikModel', path: './StepikModel' + pod 'Alamofire', '5.4.4' pod 'Atributika', '4.10.1' pod 'SwiftyJSON', '5.0.0' diff --git a/Podfile.lock b/Podfile.lock index edc936ff42..4b2a95ed8e 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -194,6 +194,7 @@ PODS: - SDWebImage/Core (= 5.12.1) - SDWebImage/Core (5.12.1) - SnapKit (5.0.1) + - StepikModel (0.0.1) - STRegex (2.1.1) - SVGKit (2.1.0): - CocoaLumberjack (~> 3.0) @@ -250,6 +251,7 @@ DEPENDENCIES: - Quick (= 4.0.0) - SDWebImage (= 5.12.1) - SnapKit (= 5.0.1) + - StepikModel (from `./StepikModel`) - STRegex (= 2.1.1) - SVGKit (from `https://github.com/SVGKit/SVGKit.git`, branch `2.x`) - SVProgressHUD (= 2.2.5) @@ -339,6 +341,8 @@ EXTERNAL SOURCES: PromiseKit: :git: https://github.com/mxcl/PromiseKit.git :tag: 6.16.2 + StepikModel: + :path: "./StepikModel" SVGKit: :branch: 2.x :git: https://github.com/SVGKit/SVGKit.git @@ -416,6 +420,7 @@ SPEC CHECKSUMS: Quick: 6473349e43b9271a8d43839d9ba1c442ed1b7ac4 SDWebImage: 4dc3e42d9ec0c1028b960a33ac6b637bb432207b SnapKit: 97b92857e3df3a0c71833cce143274bf6ef8e5eb + StepikModel: 7bf35e3dcd1687b5b7cf4f49838e1e74776a593c STRegex: d49e88d0fe58538d3175fdd989bc1243b9be2a07 SVGKit: 8a2fc74258bdb2abb54d3b65f3dd68b0277a9c4d SVProgressHUD: 1428aafac632c1f86f62aa4243ec12008d7a51d6 @@ -430,6 +435,6 @@ SPEC CHECKSUMS: VK-ios-sdk: 5bcf00a2014a7323f98db9328b603d4f96635caa YandexMobileMetrica: 9e713c16bb6aca0ba63b84c8d7b8b86d32f4ecc4 -PODFILE CHECKSUM: 42090896731b9ed2149070e8d1307c88748217fa +PODFILE CHECKSUM: 3ad6914f999990d5539bd6b9014b1f7be682e575 COCOAPODS: 1.11.2 diff --git a/Stepic.xcodeproj/project.pbxproj b/Stepic.xcodeproj/project.pbxproj index f611a763aa..e5b47f44f2 100644 --- a/Stepic.xcodeproj/project.pbxproj +++ b/Stepic.xcodeproj/project.pbxproj @@ -412,6 +412,7 @@ 2C06E09B2241045800AF4DA2 /* TableInputTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C06E09A2241045800AF4DA2 /* TableInputTextView.swift */; }; 2C06E09D22411F8A00AF4DA2 /* SettingsLargeInputTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C06E09C22411F8900AF4DA2 /* SettingsLargeInputTableViewCell.swift */; }; 2C06E0B02243CD2D00AF4DA2 /* CourseInfoTabReviewsCellSkeletonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C06E0AF2243CD2C00AF4DA2 /* CourseInfoTabReviewsCellSkeletonView.swift */; }; + 2C071C3F27BA5FAF002CBB75 /* DecodedObjectsResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C071C3E27BA5FAF002CBB75 /* DecodedObjectsResponse.swift */; }; 2C084AD82514ED3A000561F7 /* ProcessedContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C084AD72514ED3A000561F7 /* ProcessedContentTextView.swift */; }; 2C09313521CD4BDD002B605D /* VideoStoredFileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C09313421CD4BDD002B605D /* VideoStoredFileManager.swift */; }; 2C09313721D00B8D002B605D /* VideoDownloadingService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C09313621D00B8D002B605D /* VideoDownloadingService.swift */; }; @@ -890,7 +891,6 @@ 2C9BBE432490462C00FFED49 /* Debouncer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C9BBE422490462C00FFED49 /* Debouncer.swift */; }; 2C9BD78E1FC43C6B00F89CBE /* NotificationsBadgesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C9BD78D1FC43C6B00F89CBE /* NotificationsBadgesManager.swift */; }; 2C9CF0BC2754B79B001089AD /* WishListsAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C9CF0BB2754B79B001089AD /* WishListsAPI.swift */; }; - 2C9CF0BE2754B927001089AD /* WishlistEntryPlainObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C9CF0BD2754B927001089AD /* WishlistEntryPlainObject.swift */; }; 2C9CF0C22754DA9E001089AD /* WishlistEntryEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C9CF0C12754DA9E001089AD /* WishlistEntryEntity.swift */; }; 2C9CF0C42754DAAE001089AD /* WishlistEntryEntity+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C9CF0C32754DAAE001089AD /* WishlistEntryEntity+CoreDataProperties.swift */; }; 2C9D699B2617303900A0641F /* StepikAcademyCourseListWidgetViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C9D699A2617303900A0641F /* StepikAcademyCourseListWidgetViewModel.swift */; }; @@ -2405,6 +2405,7 @@ 2C06E09C22411F8900AF4DA2 /* SettingsLargeInputTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsLargeInputTableViewCell.swift; sourceTree = ""; }; 2C06E09E2241434100AF4DA2 /* Model_profile_fields_v33.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model_profile_fields_v33.xcdatamodel; sourceTree = ""; }; 2C06E0AF2243CD2C00AF4DA2 /* CourseInfoTabReviewsCellSkeletonView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CourseInfoTabReviewsCellSkeletonView.swift; sourceTree = ""; }; + 2C071C3E27BA5FAF002CBB75 /* DecodedObjectsResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecodedObjectsResponse.swift; sourceTree = ""; }; 2C084AD72514ED3A000561F7 /* ProcessedContentTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessedContentTextView.swift; sourceTree = ""; }; 2C09313321CCFE47002B605D /* Model_course_lesson_fields_v31.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model_course_lesson_fields_v31.xcdatamodel; sourceTree = ""; }; 2C09313421CD4BDD002B605D /* VideoStoredFileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoStoredFileManager.swift; sourceTree = ""; }; @@ -2924,7 +2925,6 @@ 2C9BBE422490462C00FFED49 /* Debouncer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Debouncer.swift; sourceTree = ""; }; 2C9BD78D1FC43C6B00F89CBE /* NotificationsBadgesManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsBadgesManager.swift; sourceTree = ""; }; 2C9CF0BB2754B79B001089AD /* WishListsAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WishListsAPI.swift; sourceTree = ""; }; - 2C9CF0BD2754B927001089AD /* WishlistEntryPlainObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WishlistEntryPlainObject.swift; sourceTree = ""; }; 2C9CF0BF2754D9C8001089AD /* Model_wish_lists_v91.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model_wish_lists_v91.xcdatamodel; sourceTree = ""; }; 2C9CF0C12754DA9E001089AD /* WishlistEntryEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WishlistEntryEntity.swift; sourceTree = ""; }; 2C9CF0C32754DAAE001089AD /* WishlistEntryEntity+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WishlistEntryEntity+CoreDataProperties.swift"; sourceTree = ""; }; @@ -4359,6 +4359,14 @@ path = CatalogBlocks; sourceTree = ""; }; + 2C071C3D27BA5F75002CBB75 /* Decodable */ = { + isa = PBXGroup; + children = ( + 2C071C3E27BA5FAF002CBB75 /* DecodedObjectsResponse.swift */, + ); + path = Decodable; + sourceTree = ""; + }; 2C084AC32514A1E9000561F7 /* Processing */ = { isa = PBXGroup; children = ( @@ -8085,6 +8093,7 @@ 2CEBEBF5242F8FB200DBFDF0 /* StepikRequestRetrier.swift */, 62E98DC7237B8757A9F58935 /* StepikURLSessionConfiguration.swift */, 2CC8678625E1504000762416 /* SubmissionsFilterQuery.swift */, + 2C071C3D27BA5F75002CBB75 /* Decodable */, 2CFF903B242A2A3900FD7311 /* Endpoints */, 2CFF903C242A2A6B00FD7311 /* RequestMakers */, 2CFF903D242A2A7900FD7311 /* SocialSDKProviders */, @@ -8214,7 +8223,6 @@ 2C97761C25D6C20F008778D6 /* UnitPlainObject.swift */, 2CB1C3AC240050F9001DA83E /* UserCodeRun.swift */, 2CECFEF0252F1F4C006BA883 /* VisitedCourse.swift */, - 2C9CF0BD2754B927001089AD /* WishlistEntryPlainObject.swift */, 2CCB4B1626E7724E0056C44E /* Announcements */, 2C97764225D6CBD2008778D6 /* Block */, 2CA3DAA62179DF7300F43888 /* Discussions */, @@ -10797,6 +10805,7 @@ "${BUILT_PRODUCTS_DIR}/SVGKit/SVGKit.framework", "${BUILT_PRODUCTS_DIR}/SVProgressHUD/SVProgressHUD.framework", "${BUILT_PRODUCTS_DIR}/SnapKit/SnapKit.framework", + "${BUILT_PRODUCTS_DIR}/StepikModel/StepikModel.framework", "${BUILT_PRODUCTS_DIR}/SwiftDate/SwiftDate.framework", "${BUILT_PRODUCTS_DIR}/SwiftyGif/SwiftyGif.framework", "${BUILT_PRODUCTS_DIR}/SwiftyJSON/SwiftyJSON.framework", @@ -10859,6 +10868,7 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SVGKit.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SVProgressHUD.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SnapKit.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/StepikModel.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftDate.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftyGif.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftyJSON.framework", @@ -10931,6 +10941,7 @@ "${BUILT_PRODUCTS_DIR}/SVGKit/SVGKit.framework", "${BUILT_PRODUCTS_DIR}/SVProgressHUD/SVProgressHUD.framework", "${BUILT_PRODUCTS_DIR}/SnapKit/SnapKit.framework", + "${BUILT_PRODUCTS_DIR}/StepikModel/StepikModel.framework", "${BUILT_PRODUCTS_DIR}/SwiftDate/SwiftDate.framework", "${BUILT_PRODUCTS_DIR}/SwiftyGif/SwiftyGif.framework", "${BUILT_PRODUCTS_DIR}/SwiftyJSON/SwiftyJSON.framework", @@ -10989,6 +11000,7 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SVGKit.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SVProgressHUD.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SnapKit.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/StepikModel.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftDate.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftyGif.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftyJSON.framework", @@ -11720,7 +11732,6 @@ 62E98F431204AD9A46EBBC18 /* CodeEditorSettingsViewController.swift in Sources */, 62E98975B396C752B07036D8 /* CodeEditorSettingsPresenter.swift in Sources */, 62E98C1377148A52D15AAEBF /* CodeEditorPreferencesContainer.swift in Sources */, - 2C9CF0BE2754B927001089AD /* WishlistEntryPlainObject.swift in Sources */, 2C4AD01B23E305FA0049B7B0 /* DiscussionThreadsPersistenceService.swift in Sources */, 2CADFEF424C1325F00008C65 /* AchievementProgressData.swift in Sources */, 62E983397736699787D8DD35 /* UIView+FromNib.swift in Sources */, @@ -12735,6 +12746,7 @@ 015598254C071835594BB121 /* StepQuizReviewOutputProtocol.swift in Sources */, 62E98F13BC8E26F8FBC95D7A /* ManagedObject.swift in Sources */, 6C92601BC04D155B948F85DD /* CourseSearchAssembly.swift in Sources */, + 2C071C3F27BA5FAF002CBB75 /* DecodedObjectsResponse.swift in Sources */, 3E4170D55BD05702BCCA1363 /* CourseSearchDataFlow.swift in Sources */, 0A4C9BA0D668107A241C7F21 /* CourseSearchInteractor.swift in Sources */, 07E09A9AA17EC2FDCFA5616C /* CourseSearchPresenter.swift in Sources */, diff --git a/Stepic/Legacy/Model/Entities/WishlistEntryEntity/WishlistEntryEntity.swift b/Stepic/Legacy/Model/Entities/WishlistEntryEntity/WishlistEntryEntity.swift index cc81a181b8..dee00bf8a3 100644 --- a/Stepic/Legacy/Model/Entities/WishlistEntryEntity/WishlistEntryEntity.swift +++ b/Stepic/Legacy/Model/Entities/WishlistEntryEntity/WishlistEntryEntity.swift @@ -1,5 +1,6 @@ import CoreData import Foundation +import StepikModel final class WishlistEntryEntity: NSManagedObject, ManagedObject, Identifiable { typealias IdType = Int @@ -15,8 +16,8 @@ final class WishlistEntryEntity: NSManagedObject, ManagedObject, Identifiable { // MARK: - WishlistEntryEntity (PlainObject Support) - extension WishlistEntryEntity { - var plainObject: WishlistEntryPlainObject { - WishlistEntryPlainObject( + var plainObject: WishlistEntry { + WishlistEntry( id: self.id, courseID: self.courseID, userID: self.userID, @@ -27,14 +28,14 @@ extension WishlistEntryEntity { static func insert( into context: NSManagedObjectContext, - wishlistEntry: WishlistEntryPlainObject + wishlistEntry: WishlistEntry ) -> WishlistEntryEntity { let entity: WishlistEntryEntity = context.insertObject() entity.update(wishlistEntry: wishlistEntry) return entity } - func update(wishlistEntry: WishlistEntryPlainObject) { + func update(wishlistEntry: WishlistEntry) { self.id = wishlistEntry.id self.courseID = wishlistEntry.courseID self.userID = wishlistEntry.userID diff --git a/Stepic/Legacy/Model/Network/Decodable/DecodedObjectsResponse.swift b/Stepic/Legacy/Model/Network/Decodable/DecodedObjectsResponse.swift new file mode 100644 index 0000000000..ad151f5982 --- /dev/null +++ b/Stepic/Legacy/Model/Network/Decodable/DecodedObjectsResponse.swift @@ -0,0 +1,65 @@ +import Foundation + +struct DecodedObjectsResponse: Decodable { + let meta: Meta + let decodedObjects: [T] + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.meta = try container.decodeIfPresent(Meta.self, forKey: .meta) ?? .oneAndOnlyPage + + for key in container.allKeys where key.stringValue != CodingKeys.meta.stringValue { + var decodedObjectsContainer = try container.nestedUnkeyedContainer(forKey: key) + + var isFailedDecode = false + var lastDecodingError: Error? + var tempDecodedObjects = [T]() + + while !decodedObjectsContainer.isAtEnd && !isFailedDecode { + do { + let decodedObject = try decodedObjectsContainer.decode(T.self) + tempDecodedObjects.append(decodedObject) + } catch { + isFailedDecode = true + lastDecodingError = error + break + } + } + + if !isFailedDecode { + self.decodedObjects = tempDecodedObjects + return + } else if !tempDecodedObjects.isEmpty { + if let lastDecodingError = lastDecodingError { + throw lastDecodingError + } + throw DecodingError.dataCorruptedError(in: decodedObjectsContainer, debugDescription: "") + } + } + + throw DecodingError.typeMismatch( + T.self, + .init( + codingPath: container.allKeys, + debugDescription: "Didn't find nestedUnkeyedContainer for \(String(describing: T.self))", + underlyingError: nil + ) + ) + } + + private struct CodingKeys: CodingKey { + let stringValue: String + let intValue: Int? + + init(stringValue: String) { + self.stringValue = stringValue + self.intValue = nil + } + + init?(intValue: Int) { + return nil + } + + static var meta: Self { Self(stringValue: "meta") } + } +} diff --git a/Stepic/Legacy/Model/Network/Endpoints/APIEndpoint.swift b/Stepic/Legacy/Model/Network/Endpoints/APIEndpoint.swift index c2d8eadf91..f6e35fedee 100644 --- a/Stepic/Legacy/Model/Network/Endpoints/APIEndpoint.swift +++ b/Stepic/Legacy/Model/Network/Endpoints/APIEndpoint.swift @@ -13,7 +13,7 @@ import PromiseKit import SwiftyJSON class APIEndpoint { - var name: String { "" } + class var name: String { "" } let manager: Alamofire.Session @@ -67,8 +67,8 @@ class APIEndpoint { printOutput: Bool = false ) -> Promise<([T])> { self.retrieve.request( - requestEndpoint: self.name, - paramName: self.name, + requestEndpoint: Self.name, + paramName: Self.name, ids: ids, updating: updating, withManager: self.manager diff --git a/Stepic/Legacy/Model/Network/Endpoints/AchievementProgressesAPI.swift b/Stepic/Legacy/Model/Network/Endpoints/AchievementProgressesAPI.swift index 86466219c5..adfe67419b 100644 --- a/Stepic/Legacy/Model/Network/Endpoints/AchievementProgressesAPI.swift +++ b/Stepic/Legacy/Model/Network/Endpoints/AchievementProgressesAPI.swift @@ -11,7 +11,7 @@ import Foundation import PromiseKit final class AchievementProgressesAPI: APIEndpoint { - override var name: String { "achievement-progresses" } + override class var name: String { "achievement-progresses" } func retrieve( userID: Int, @@ -34,8 +34,8 @@ final class AchievementProgressesAPI: APIEndpoint { } self.retrieve.request( - requestEndpoint: self.name, - paramName: self.name, + requestEndpoint: Self.name, + paramName: Self.name, params: params, updatingObjects: [], withManager: self.manager diff --git a/Stepic/Legacy/Model/Network/Endpoints/AchievementsAPI.swift b/Stepic/Legacy/Model/Network/Endpoints/AchievementsAPI.swift index 53eefbabca..c9f3c0c806 100644 --- a/Stepic/Legacy/Model/Network/Endpoints/AchievementsAPI.swift +++ b/Stepic/Legacy/Model/Network/Endpoints/AchievementsAPI.swift @@ -11,7 +11,7 @@ import Foundation import PromiseKit final class AchievementsAPI: APIEndpoint { - override var name: String { "achievements" } + override class var name: String { "achievements" } func retrieve(kind: String? = nil, page: Int = 1) -> Promise<([Achievement], Meta)> { Promise { seal in @@ -22,8 +22,8 @@ final class AchievementsAPI: APIEndpoint { params["page"] = page self.retrieve.request( - requestEndpoint: self.name, - paramName: self.name, + requestEndpoint: Self.name, + paramName: Self.name, params: params, updatingObjects: [], withManager: self.manager diff --git a/Stepic/Legacy/Model/Network/Endpoints/AdaptiveRatingsAPI.swift b/Stepic/Legacy/Model/Network/Endpoints/AdaptiveRatingsAPI.swift index 72857874fd..298ded7208 100644 --- a/Stepic/Legacy/Model/Network/Endpoints/AdaptiveRatingsAPI.swift +++ b/Stepic/Legacy/Model/Network/Endpoints/AdaptiveRatingsAPI.swift @@ -12,7 +12,7 @@ import PromiseKit import SwiftyJSON final class AdaptiveRatingsAPI: APIEndpoint { - override var name: String { "rating" } + override class var name: String { "rating" } typealias RatingRecord = (userId: Int, exp: Int, rank: Int, isFake: Bool) typealias Scoreboard = (allCount: Int, leaders: [RatingRecord]) @@ -29,7 +29,7 @@ final class AdaptiveRatingsAPI: APIEndpoint { return Promise { seal in self.manager.request( - "\(RemoteConfig.shared.adaptiveBackendURL)/\(name)", + "\(RemoteConfig.shared.adaptiveBackendURL)/\(Self.name)", method: .put, parameters: params, encoding: JSONEncoding.default, @@ -65,7 +65,7 @@ final class AdaptiveRatingsAPI: APIEndpoint { return Promise { seal in self.manager.request( - "\(RemoteConfig.shared.adaptiveBackendURL)/\(self.name)", + "\(RemoteConfig.shared.adaptiveBackendURL)/\(Self.name)", method: .get, parameters: params, encoding: URLEncoding.default, diff --git a/Stepic/Legacy/Model/Network/Endpoints/AdaptiveRatingsRestoreAPI.swift b/Stepic/Legacy/Model/Network/Endpoints/AdaptiveRatingsRestoreAPI.swift index 88cab6d2fa..fe439ec1d7 100644 --- a/Stepic/Legacy/Model/Network/Endpoints/AdaptiveRatingsRestoreAPI.swift +++ b/Stepic/Legacy/Model/Network/Endpoints/AdaptiveRatingsRestoreAPI.swift @@ -4,7 +4,7 @@ import PromiseKit import SwiftyJSON final class AdaptiveRatingsRestoreAPI: APIEndpoint { - override var name: String { "rating-restore" } + override class var name: String { "rating-restore" } func restore(courseID: Int) -> Promise<(exp: Int, streak: Int)> { var params: Parameters = [ @@ -17,7 +17,7 @@ final class AdaptiveRatingsRestoreAPI: APIEndpoint { return Promise { seal in self.manager.request( - "\(RemoteConfig.shared.adaptiveBackendURL)/\(self.name)", + "\(RemoteConfig.shared.adaptiveBackendURL)/\(Self.name)", method: .get, parameters: params, encoding: URLEncoding.default, diff --git a/Stepic/Legacy/Model/Network/Endpoints/AnnouncementsAPI.swift b/Stepic/Legacy/Model/Network/Endpoints/AnnouncementsAPI.swift index dc719e4c85..a001a978ba 100644 --- a/Stepic/Legacy/Model/Network/Endpoints/AnnouncementsAPI.swift +++ b/Stepic/Legacy/Model/Network/Endpoints/AnnouncementsAPI.swift @@ -3,7 +3,7 @@ import Foundation import PromiseKit final class AnnouncementsAPI: APIEndpoint { - override var name: String { "announcements" } + override class var name: String { "announcements" } func retrieve(courseID: Course.IdType, page: Int = 1) -> Promise<([AnnouncementPlainObject], Meta)> { let params: Parameters = [ @@ -12,8 +12,8 @@ final class AnnouncementsAPI: APIEndpoint { ] return self.retrieve.request( - requestEndpoint: self.name, - paramName: self.name, + requestEndpoint: Self.name, + paramName: Self.name, params: params, withManager: self.manager ) @@ -21,8 +21,8 @@ final class AnnouncementsAPI: APIEndpoint { func retrieve(ids: [Announcement.IdType]) -> Promise<([AnnouncementPlainObject], Meta)> { self.retrieve.request( - requestEndpoint: self.name, - paramName: self.name, + requestEndpoint: Self.name, + paramName: Self.name, ids: ids, updating: [], withManager: self.manager diff --git a/Stepic/Legacy/Model/Network/Endpoints/AssignmentsAPI.swift b/Stepic/Legacy/Model/Network/Endpoints/AssignmentsAPI.swift index 0891f059f6..3c6b984c61 100644 --- a/Stepic/Legacy/Model/Network/Endpoints/AssignmentsAPI.swift +++ b/Stepic/Legacy/Model/Network/Endpoints/AssignmentsAPI.swift @@ -12,7 +12,7 @@ import PromiseKit import SwiftyJSON final class AssignmentsAPI: APIEndpoint { - override var name: String { "assignments" } + override class var name: String { "assignments" } @discardableResult func retrieve( @@ -24,7 +24,7 @@ final class AssignmentsAPI: APIEndpoint { error errorHandler: @escaping ((NetworkError) -> Void) ) -> Request? { self.getObjectsByIds( - requestString: self.name, + requestString: Self.name, printOutput: false, ids: ids, deleteObjects: existing, diff --git a/Stepic/Legacy/Model/Network/Endpoints/AttemptsAPI.swift b/Stepic/Legacy/Model/Network/Endpoints/AttemptsAPI.swift index f50e5a73d5..c0e0e7bb1d 100644 --- a/Stepic/Legacy/Model/Network/Endpoints/AttemptsAPI.swift +++ b/Stepic/Legacy/Model/Network/Endpoints/AttemptsAPI.swift @@ -12,16 +12,16 @@ import PromiseKit import SwiftyJSON final class AttemptsAPI: APIEndpoint { - override var name: String { "attempts" } + override class var name: String { "attempts" } /// Get attempts by ids. func retrieve(ids: [Attempt.IdType], stepName: String) -> Promise<[Attempt]> { self.retrieve.request( - requestEndpoint: self.name, + requestEndpoint: Self.name, ids: ids, withManager: self.manager ).then { json -> Promise<[Attempt]> in - let attempts = json[self.name].arrayValue.map { Attempt(json: $0, stepBlockName: stepName) } + let attempts = json[Self.name].arrayValue.map { Attempt(json: $0, stepBlockName: stepName) } return .value(attempts) } } diff --git a/Stepic/Legacy/Model/Network/Endpoints/CatalogBlocksAPI.swift b/Stepic/Legacy/Model/Network/Endpoints/CatalogBlocksAPI.swift index b009b8573e..42f10074cd 100644 --- a/Stepic/Legacy/Model/Network/Endpoints/CatalogBlocksAPI.swift +++ b/Stepic/Legacy/Model/Network/Endpoints/CatalogBlocksAPI.swift @@ -4,7 +4,7 @@ import PromiseKit import SwiftyJSON final class CatalogBlocksAPI: APIEndpoint { - override var name: String { "catalog-blocks" } + override class var name: String { "catalog-blocks" } /// Get catalog blocks by ids. func retrieve(ids: [CatalogBlock.IdType], page: Int = 1) -> Promise<([CatalogBlock], Meta)> { @@ -14,8 +14,8 @@ final class CatalogBlocksAPI: APIEndpoint { ] return self.retrieve.request( - requestEndpoint: self.name, - paramName: self.name, + requestEndpoint: Self.name, + paramName: Self.name, params: params, withManager: self.manager ) @@ -33,8 +33,8 @@ final class CatalogBlocksAPI: APIEndpoint { ] return self.retrieve.request( - requestEndpoint: self.name, - paramName: self.name, + requestEndpoint: Self.name, + paramName: Self.name, params: params, withManager: self.manager ) diff --git a/Stepic/Legacy/Model/Network/Endpoints/CertificatesAPI.swift b/Stepic/Legacy/Model/Network/Endpoints/CertificatesAPI.swift index acdbe5ebaa..d7ae4ce836 100644 --- a/Stepic/Legacy/Model/Network/Endpoints/CertificatesAPI.swift +++ b/Stepic/Legacy/Model/Network/Endpoints/CertificatesAPI.swift @@ -12,7 +12,7 @@ import PromiseKit import SwiftyJSON final class CertificatesAPI: APIEndpoint { - override var name: String { "certificates" } + override class var name: String { "certificates" } func retrieve( userID: User.IdType, @@ -34,8 +34,8 @@ final class CertificatesAPI: APIEndpoint { } return self.retrieve.requestWithFetching( - requestEndpoint: self.name, - paramName: self.name, + requestEndpoint: Self.name, + paramName: Self.name, params: params, withManager: manager ) @@ -43,7 +43,7 @@ final class CertificatesAPI: APIEndpoint { func update(_ certificate: Certificate) -> Promise { self.update.request( - requestEndpoint: self.name, + requestEndpoint: Self.name, paramName: "certificate", updatingObject: certificate, withManager: self.manager diff --git a/Stepic/Legacy/Model/Network/Endpoints/CommentsAPI.swift b/Stepic/Legacy/Model/Network/Endpoints/CommentsAPI.swift index 616cc60189..dbf1258718 100644 --- a/Stepic/Legacy/Model/Network/Endpoints/CommentsAPI.swift +++ b/Stepic/Legacy/Model/Network/Endpoints/CommentsAPI.swift @@ -12,7 +12,7 @@ import PromiseKit import SwiftyJSON final class CommentsAPI: APIEndpoint { - override var name: String { "comments" } + override class var name: String { "comments" } /// Get comments by ids. /// @@ -22,8 +22,8 @@ final class CommentsAPI: APIEndpoint { func retrieve(ids: [Comment.IdType], blockName: String?) -> Promise<[Comment]> { Promise { seal in self.retrieve.request( - requestEndpoint: self.name, - paramName: self.name, + requestEndpoint: Self.name, + paramName: Self.name, ids: ids, updating: [Comment](), withManager: self.manager @@ -80,7 +80,7 @@ final class CommentsAPI: APIEndpoint { func create(_ comment: Comment, blockName: String?) -> Promise { Promise { seal in self.create.request( - requestEndpoint: self.name, + requestEndpoint: Self.name, paramName: "comment", creatingObject: comment, withManager: self.manager @@ -126,7 +126,7 @@ final class CommentsAPI: APIEndpoint { func update(_ comment: Comment, blockName: String?) -> Promise { Promise { seal in self.update.request( - requestEndpoint: self.name, + requestEndpoint: Self.name, paramName: "comment", updatingObject: comment, withManager: self.manager @@ -170,6 +170,6 @@ final class CommentsAPI: APIEndpoint { } func delete(commentID: Comment.IdType) -> Promise { - self.delete.request(requestEndpoint: self.name, deletingId: commentID, withManager: self.manager) + self.delete.request(requestEndpoint: Self.name, deletingId: commentID, withManager: self.manager) } } diff --git a/Stepic/Legacy/Model/Network/Endpoints/CourseBeneficiariesAPI.swift b/Stepic/Legacy/Model/Network/Endpoints/CourseBeneficiariesAPI.swift index a6bf65689d..b20df6b8e0 100644 --- a/Stepic/Legacy/Model/Network/Endpoints/CourseBeneficiariesAPI.swift +++ b/Stepic/Legacy/Model/Network/Endpoints/CourseBeneficiariesAPI.swift @@ -4,7 +4,7 @@ import PromiseKit import SwiftyJSON final class CourseBeneficiariesAPI: APIEndpoint { - override var name: String { "course-beneficiaries" } + override class var name: String { "course-beneficiaries" } private let courseBeneficiariesPersistenceService: CourseBeneficiariesPersistenceServiceProtocol @@ -29,8 +29,8 @@ final class CourseBeneficiariesAPI: APIEndpoint { return self.courseBeneficiariesPersistenceService.fetch(courseID: courseID, userID: userID).then { cachedCourseBeneficiaries -> Promise<([CourseBeneficiary], Meta)> in self.retrieve.request( - requestEndpoint: self.name, - paramName: self.name, + requestEndpoint: Self.name, + paramName: Self.name, params: params, updatingObjects: cachedCourseBeneficiaries, withManager: self.manager diff --git a/Stepic/Legacy/Model/Network/Endpoints/CourseBenefitByMonthsAPI.swift b/Stepic/Legacy/Model/Network/Endpoints/CourseBenefitByMonthsAPI.swift index 3853623401..af2ee9014e 100644 --- a/Stepic/Legacy/Model/Network/Endpoints/CourseBenefitByMonthsAPI.swift +++ b/Stepic/Legacy/Model/Network/Endpoints/CourseBenefitByMonthsAPI.swift @@ -4,7 +4,7 @@ import PromiseKit import SwiftyJSON final class CourseBenefitByMonthsAPI: APIEndpoint { - override var name: String { "course-benefit-by-months" } + override class var name: String { "course-benefit-by-months" } private let courseBenefitByMonthsPersistenceService: CourseBenefitByMonthsPersistenceServiceProtocol @@ -24,8 +24,8 @@ final class CourseBenefitByMonthsAPI: APIEndpoint { return self.courseBenefitByMonthsPersistenceService.fetch(courseID: courseID).then { cachedCourseBenefitByMonths -> Promise<([CourseBenefitByMonth], Meta)> in self.retrieve.request( - requestEndpoint: self.name, - paramName: self.name, + requestEndpoint: Self.name, + paramName: Self.name, params: params, updatingObjects: cachedCourseBenefitByMonths, withManager: self.manager diff --git a/Stepic/Legacy/Model/Network/Endpoints/CourseBenefitSummariesAPI.swift b/Stepic/Legacy/Model/Network/Endpoints/CourseBenefitSummariesAPI.swift index 3f56eb87bf..c780873014 100644 --- a/Stepic/Legacy/Model/Network/Endpoints/CourseBenefitSummariesAPI.swift +++ b/Stepic/Legacy/Model/Network/Endpoints/CourseBenefitSummariesAPI.swift @@ -4,7 +4,7 @@ import PromiseKit import SwiftyJSON final class CourseBenefitSummariesAPI: APIEndpoint { - override var name: String { "course-benefit-summaries" } + override class var name: String { "course-benefit-summaries" } private let courseBenefitSummariesPersistenceService: CourseBenefitSummariesPersistenceServiceProtocol @@ -20,8 +20,8 @@ final class CourseBenefitSummariesAPI: APIEndpoint { self.courseBenefitSummariesPersistenceService.fetch(id: id).map { $0 != nil ? [$0!] : [] } }.then { cachedCourseBenefitSummaries -> Promise<([CourseBenefitSummary], Meta)> in self.retrieve.request( - requestEndpoint: "\(self.name)/\(id)", - paramName: self.name, + requestEndpoint: "\(Self.name)/\(id)", + paramName: Self.name, params: [:], updatingObjects: cachedCourseBenefitSummaries, withManager: self.manager diff --git a/Stepic/Legacy/Model/Network/Endpoints/CourseBenefitsAPI.swift b/Stepic/Legacy/Model/Network/Endpoints/CourseBenefitsAPI.swift index ec942c88e5..797fa897df 100644 --- a/Stepic/Legacy/Model/Network/Endpoints/CourseBenefitsAPI.swift +++ b/Stepic/Legacy/Model/Network/Endpoints/CourseBenefitsAPI.swift @@ -4,7 +4,7 @@ import PromiseKit import SwiftyJSON final class CourseBenefitsAPI: APIEndpoint { - override var name: String { "course-benefits" } + override class var name: String { "course-benefits" } private let courseBenefitsPersistenceService: CourseBenefitsPersistenceServiceProtocol @@ -20,8 +20,8 @@ final class CourseBenefitsAPI: APIEndpoint { self.courseBenefitsPersistenceService.fetch(ids: ids).then { cachedCourseBenefits -> Promise<([CourseBenefit], Meta, JSON)> in self.retrieve.request( - requestEndpoint: self.name, - paramName: self.name, + requestEndpoint: Self.name, + paramName: Self.name, params: ["ids": ids], updatingObjects: cachedCourseBenefits, withManager: self.manager @@ -49,8 +49,8 @@ final class CourseBenefitsAPI: APIEndpoint { } }.then { cachedCourseBenefits -> Promise<([CourseBenefit], Meta)> in self.retrieve.request( - requestEndpoint: self.name, - paramName: self.name, + requestEndpoint: Self.name, + paramName: Self.name, params: params, updatingObjects: cachedCourseBenefits, withManager: self.manager diff --git a/Stepic/Legacy/Model/Network/Endpoints/CourseListsAPI.swift b/Stepic/Legacy/Model/Network/Endpoints/CourseListsAPI.swift index 567bc5cd48..7585c7f34e 100644 --- a/Stepic/Legacy/Model/Network/Endpoints/CourseListsAPI.swift +++ b/Stepic/Legacy/Model/Network/Endpoints/CourseListsAPI.swift @@ -12,7 +12,7 @@ import PromiseKit import SwiftyJSON final class CourseListsAPI: APIEndpoint { - override var name: String { "course-lists" } + override class var name: String { "course-lists" } func retrieve(id: CourseListModel.IdType, page: Int = 1) -> Promise<([CourseListModel], Meta)> { let params: Parameters = [ @@ -20,8 +20,8 @@ final class CourseListsAPI: APIEndpoint { ] return self.retrieve.requestWithFetching( - requestEndpoint: "\(self.name)/\(id)", - paramName: self.name, + requestEndpoint: "\(Self.name)/\(id)", + paramName: Self.name, params: params, withManager: self.manager ) @@ -37,8 +37,8 @@ final class CourseListsAPI: APIEndpoint { CourseListModel.fetchAsync(ids: ids).then { cachedCourseLists -> Promise<([CourseListModel], Meta)> in self.retrieve.request( - requestEndpoint: self.name, - paramName: self.name, + requestEndpoint: Self.name, + paramName: Self.name, params: params, updatingObjects: cachedCourseLists, withManager: self.manager @@ -59,8 +59,8 @@ final class CourseListsAPI: APIEndpoint { ] return self.retrieve.requestWithFetching( - requestEndpoint: self.name, - paramName: self.name, + requestEndpoint: Self.name, + paramName: Self.name, params: params, withManager: self.manager ) diff --git a/Stepic/Legacy/Model/Network/Endpoints/CoursePaymentsAPI.swift b/Stepic/Legacy/Model/Network/Endpoints/CoursePaymentsAPI.swift index bc4d45a8ec..a72744b6f7 100644 --- a/Stepic/Legacy/Model/Network/Endpoints/CoursePaymentsAPI.swift +++ b/Stepic/Legacy/Model/Network/Endpoints/CoursePaymentsAPI.swift @@ -4,7 +4,7 @@ import PromiseKit import SwiftyJSON final class CoursePaymentsAPI: APIEndpoint { - override var name: String { "course-payments" } + override class var name: String { "course-payments" } func retrieve(courseID: Course.IdType) -> Promise<([CoursePayment], Meta)> { let params: Parameters = [ @@ -13,8 +13,8 @@ final class CoursePaymentsAPI: APIEndpoint { ] return self.retrieve.request( - requestEndpoint: self.name, - paramName: self.name, + requestEndpoint: Self.name, + paramName: Self.name, params: params, withManager: self.manager ) @@ -22,7 +22,7 @@ final class CoursePaymentsAPI: APIEndpoint { func create(_ coursePayment: CoursePayment) -> Promise { self.create.request( - requestEndpoint: self.name, + requestEndpoint: Self.name, paramName: "course-payment", creatingObject: coursePayment, withManager: self.manager diff --git a/Stepic/Legacy/Model/Network/Endpoints/CoursePurchasesAPI.swift b/Stepic/Legacy/Model/Network/Endpoints/CoursePurchasesAPI.swift index 006a3e68ac..877408ee79 100644 --- a/Stepic/Legacy/Model/Network/Endpoints/CoursePurchasesAPI.swift +++ b/Stepic/Legacy/Model/Network/Endpoints/CoursePurchasesAPI.swift @@ -4,7 +4,7 @@ import PromiseKit import SwiftyJSON final class CoursePurchasesAPI: APIEndpoint { - override var name: String { "course-purchases" } + override class var name: String { "course-purchases" } private let coursePurchasesPersistenceService: CoursePurchasesPersistenceServiceProtocol @@ -25,8 +25,8 @@ final class CoursePurchasesAPI: APIEndpoint { self.coursePurchasesPersistenceService.fetch(courseID: courseID) }.then { cachedCoursePurchases -> Promise<([CoursePurchase], Meta, JSON)> in self.retrieve.request( - requestEndpoint: self.name, - paramName: self.name, + requestEndpoint: Self.name, + paramName: Self.name, params: params, updatingObjects: cachedCoursePurchases, withManager: self.manager diff --git a/Stepic/Legacy/Model/Network/Endpoints/CourseRecommendationsAPI.swift b/Stepic/Legacy/Model/Network/Endpoints/CourseRecommendationsAPI.swift index aa8404d92e..4c606697f3 100644 --- a/Stepic/Legacy/Model/Network/Endpoints/CourseRecommendationsAPI.swift +++ b/Stepic/Legacy/Model/Network/Endpoints/CourseRecommendationsAPI.swift @@ -4,7 +4,7 @@ import PromiseKit import SwiftyJSON final class CourseRecommendationsAPI: APIEndpoint { - override var name: String { "course-recommendations" } + override class var name: String { "course-recommendations" } func getCourseRecommendations( languageString: String, @@ -18,8 +18,8 @@ final class CourseRecommendationsAPI: APIEndpoint { ] return self.retrieve.request( - requestEndpoint: self.name, - paramName: self.name, + requestEndpoint: Self.name, + paramName: Self.name, params: params, withManager: self.manager ) diff --git a/Stepic/Legacy/Model/Network/Endpoints/CourseReviewSummariesAPI.swift b/Stepic/Legacy/Model/Network/Endpoints/CourseReviewSummariesAPI.swift index 6c1e11e27e..5e0ab32192 100644 --- a/Stepic/Legacy/Model/Network/Endpoints/CourseReviewSummariesAPI.swift +++ b/Stepic/Legacy/Model/Network/Endpoints/CourseReviewSummariesAPI.swift @@ -12,7 +12,7 @@ import PromiseKit import SwiftyJSON final class CourseReviewSummariesAPI: APIEndpoint { - override var name: String { "course-review-summaries" } + override class var name: String { "course-review-summaries" } @discardableResult func retrieve( @@ -24,7 +24,7 @@ final class CourseReviewSummariesAPI: APIEndpoint { error errorHandler: @escaping ((NetworkError) -> Void) ) -> Request? { self.getObjectsByIds( - requestString: self.name, + requestString: Self.name, printOutput: false, ids: ids, deleteObjects: existing, diff --git a/Stepic/Legacy/Model/Network/Endpoints/CourseReviewsAPI.swift b/Stepic/Legacy/Model/Network/Endpoints/CourseReviewsAPI.swift index 8aae598fe2..6f27920128 100644 --- a/Stepic/Legacy/Model/Network/Endpoints/CourseReviewsAPI.swift +++ b/Stepic/Legacy/Model/Network/Endpoints/CourseReviewsAPI.swift @@ -12,7 +12,7 @@ import PromiseKit import SwiftyJSON final class CourseReviewsAPI: APIEndpoint { - override var name: String { "course-reviews" } + override class var name: String { "course-reviews" } /// Get course reviews by course id. func retrieve(courseID: Course.IdType, page: Int = 1) -> Promise<([CourseReview], Meta)> { @@ -25,8 +25,8 @@ final class CourseReviewsAPI: APIEndpoint { CourseReview.fetch(courseID: courseID).then { cachedReviews -> Promise<([CourseReview], Meta, JSON)> in self.retrieve.request( - requestEndpoint: self.name, - paramName: self.name, + requestEndpoint: Self.name, + paramName: Self.name, params: parameters, updatingObjects: cachedReviews, withManager: self.manager @@ -50,8 +50,8 @@ final class CourseReviewsAPI: APIEndpoint { CourseReview.fetch(courseID: courseID, userID: userID).then { cachedReviews -> Promise<([CourseReview], Meta, JSON)> in self.retrieve.request( - requestEndpoint: self.name, - paramName: self.name, + requestEndpoint: Self.name, + paramName: Self.name, params: parameters, updatingObjects: cachedReviews, withManager: self.manager @@ -75,8 +75,8 @@ final class CourseReviewsAPI: APIEndpoint { CourseReview.fetch(userID: userID).then { cachedReviews -> Promise<([CourseReview], Meta, JSON)> in self.retrieve.request( - requestEndpoint: self.name, - paramName: self.name, + requestEndpoint: Self.name, + paramName: Self.name, params: parameters, updatingObjects: cachedReviews, withManager: self.manager @@ -93,8 +93,8 @@ final class CourseReviewsAPI: APIEndpoint { func retrieveAll(userID: User.IdType) -> Promise<[CourseReview]> { CourseReview.fetch(userID: userID).then { self.retrieve.requestWithCollectAllPages( - requestEndpoint: self.name, - paramName: self.name, + requestEndpoint: Self.name, + paramName: Self.name, params: ["user": userID], updatingObjects: $0, withManager: self.manager @@ -111,8 +111,8 @@ final class CourseReviewsAPI: APIEndpoint { let courseReview = CourseReview(courseID: courseID, userID: userID, score: score, text: text) return Promise { seal in self.create.request( - requestEndpoint: self.name, - paramName: self.name, + requestEndpoint: Self.name, + paramName: Self.name, creatingObject: courseReview, withManager: self.manager ).done { courseReview, _ in @@ -125,8 +125,8 @@ final class CourseReviewsAPI: APIEndpoint { func update(_ courseReview: CourseReview) -> Promise { self.update.request( - requestEndpoint: self.name, - paramName: self.name, + requestEndpoint: Self.name, + paramName: Self.name, updatingObject: courseReview, withManager: self.manager ) @@ -134,7 +134,7 @@ final class CourseReviewsAPI: APIEndpoint { func delete(id: CourseReview.IdType) -> Promise { self.delete.request( - requestEndpoint: self.name, + requestEndpoint: Self.name, deletingId: id, withManager: self.manager ).then { diff --git a/Stepic/Legacy/Model/Network/Endpoints/CoursesAPI.swift b/Stepic/Legacy/Model/Network/Endpoints/CoursesAPI.swift index e749301c09..8d31eeec0a 100644 --- a/Stepic/Legacy/Model/Network/Endpoints/CoursesAPI.swift +++ b/Stepic/Legacy/Model/Network/Endpoints/CoursesAPI.swift @@ -14,7 +14,7 @@ import SwiftyJSON final class CoursesAPI: APIEndpoint { private let coursesPersistenceService: CoursesPersistenceServiceProtocol - override var name: String { "courses" } + override class var name: String { "courses" } init( coursesPersistenceService: CoursesPersistenceServiceProtocol = CoursesPersistenceService(), @@ -142,7 +142,7 @@ final class CoursesAPI: APIEndpoint { error errorHandler: @escaping (NetworkError) -> Void ) -> Request? { self.getObjectsByIds( - requestString: self.name, + requestString: Self.name, printOutput: false, ids: ids, deleteObjects: existing, diff --git a/Stepic/Legacy/Model/Network/Endpoints/DevicesAPI.swift b/Stepic/Legacy/Model/Network/Endpoints/DevicesAPI.swift index 530d45e0ac..0adc59f9be 100644 --- a/Stepic/Legacy/Model/Network/Endpoints/DevicesAPI.swift +++ b/Stepic/Legacy/Model/Network/Endpoints/DevicesAPI.swift @@ -13,7 +13,7 @@ import UIKit //TODO: Refactor this after DeviceError refactoring final class DevicesAPI: APIEndpoint { - override var name: String { "devices" } + override class var name: String { "devices" } func retrieve(registrationId: String) -> Promise { Promise { seal in @@ -32,7 +32,7 @@ final class DevicesAPI: APIEndpoint { func retrieve(deviceId: Int, headers: HTTPHeaders = AuthInfo.shared.initialHTTPHeaders) -> Promise { Promise { seal in self.manager.request( - "\(StepikApplicationsInfo.apiURL)/\(self.name)/\(deviceId)", + "\(StepikApplicationsInfo.apiURL)/\(Self.name)/\(deviceId)", parameters: [:], headers: headers ).responseSwiftyJSON { response in @@ -70,7 +70,7 @@ final class DevicesAPI: APIEndpoint { ] self.manager.request( - "\(StepikApplicationsInfo.apiURL)/\(self.name)/\(deviceId)", + "\(StepikApplicationsInfo.apiURL)/\(Self.name)/\(deviceId)", method: .put, parameters: params, encoding: JSONEncoding.default, @@ -169,7 +169,7 @@ final class DevicesAPI: APIEndpoint { ) -> Promise<(Meta, [Device])> { Promise { seal in self.manager.request( - "\(StepikApplicationsInfo.apiURL)/\(self.name)", + "\(StepikApplicationsInfo.apiURL)/\(Self.name)", parameters: params, headers: headers ).responseSwiftyJSON { response in diff --git a/Stepic/Legacy/Model/Network/Endpoints/DiscussionProxiesAPI.swift b/Stepic/Legacy/Model/Network/Endpoints/DiscussionProxiesAPI.swift index 40a49c2532..6db824e3e1 100644 --- a/Stepic/Legacy/Model/Network/Endpoints/DiscussionProxiesAPI.swift +++ b/Stepic/Legacy/Model/Network/Endpoints/DiscussionProxiesAPI.swift @@ -12,7 +12,7 @@ import PromiseKit import SwiftyJSON final class DiscussionProxiesAPI: APIEndpoint { - override var name: String { "discussion-proxies" } + override class var name: String { "discussion-proxies" } func retrieve(id: String) -> Promise { self.retrieve.request( diff --git a/Stepic/Legacy/Model/Network/Endpoints/DiscussionThreadsAPI.swift b/Stepic/Legacy/Model/Network/Endpoints/DiscussionThreadsAPI.swift index 5939ec91a6..8a8d5db969 100644 --- a/Stepic/Legacy/Model/Network/Endpoints/DiscussionThreadsAPI.swift +++ b/Stepic/Legacy/Model/Network/Endpoints/DiscussionThreadsAPI.swift @@ -4,7 +4,7 @@ import PromiseKit import SwiftyJSON final class DiscussionThreadsAPI: APIEndpoint { - override var name: String { "discussion-threads" } + override class var name: String { "discussion-threads" } /// Get discussion threads by ids. func retrieve(ids: [DiscussionThread.IdType], page: Int = 1) -> Promise<([DiscussionThread], Meta)> { @@ -18,8 +18,8 @@ final class DiscussionThreadsAPI: APIEndpoint { DiscussionThread.fetchAsync(ids: ids) }.then { cachedDiscussionThreads -> Promise<([DiscussionThread], Meta, JSON)> in self.retrieve.request( - requestEndpoint: self.name, - paramName: self.name, + requestEndpoint: Self.name, + paramName: Self.name, params: params, updatingObjects: cachedDiscussionThreads, withManager: self.manager diff --git a/Stepic/Legacy/Model/Network/Endpoints/EmailAddressesAPI.swift b/Stepic/Legacy/Model/Network/Endpoints/EmailAddressesAPI.swift index 2bfc4d552d..cdce7dc571 100644 --- a/Stepic/Legacy/Model/Network/Endpoints/EmailAddressesAPI.swift +++ b/Stepic/Legacy/Model/Network/Endpoints/EmailAddressesAPI.swift @@ -12,7 +12,7 @@ import PromiseKit import SwiftyJSON final class EmailAddressesAPI: APIEndpoint { - override var name: String { "email-addresses" } + override class var name: String { "email-addresses" } /// Get email addresses by ids. func retrieve(ids: [EmailAddress.IdType], page: Int = 1) -> Promise<([EmailAddress], Meta)> { @@ -25,8 +25,8 @@ final class EmailAddressesAPI: APIEndpoint { EmailAddress.fetchAsync(ids: ids).then { cachedEmailAddresses -> Promise<([EmailAddress], Meta, JSON)> in self.retrieve.request( - requestEndpoint: self.name, - paramName: self.name, + requestEndpoint: Self.name, + paramName: Self.name, params: parameters, updatingObjects: cachedEmailAddresses, withManager: self.manager diff --git a/Stepic/Legacy/Model/Network/Endpoints/EnrollmentsAPI.swift b/Stepic/Legacy/Model/Network/Endpoints/EnrollmentsAPI.swift index 8946e3ab86..129ff73737 100644 --- a/Stepic/Legacy/Model/Network/Endpoints/EnrollmentsAPI.swift +++ b/Stepic/Legacy/Model/Network/Endpoints/EnrollmentsAPI.swift @@ -12,10 +12,10 @@ import PromiseKit import SwiftyJSON final class EnrollmentsAPI: APIEndpoint { - override var name: String { "enrollments" } + override class var name: String { "enrollments" } func delete(courseId: Int) -> Promise { - self.delete.request(requestEndpoint: self.name, deletingId: courseId, withManager: manager) + self.delete.request(requestEndpoint: Self.name, deletingId: courseId, withManager: manager) } } @@ -48,7 +48,7 @@ extension EnrollmentsAPI { if !delete { return self.manager.request( - "\(StepikApplicationsInfo.apiURL)/\(self.name)", + "\(StepikApplicationsInfo.apiURL)/\(Self.name)", method: .post, parameters: params, encoding: JSONEncoding.default, diff --git a/Stepic/Legacy/Model/Network/Endpoints/ExamSessionsAPI.swift b/Stepic/Legacy/Model/Network/Endpoints/ExamSessionsAPI.swift index fb98852fbd..080bff084f 100644 --- a/Stepic/Legacy/Model/Network/Endpoints/ExamSessionsAPI.swift +++ b/Stepic/Legacy/Model/Network/Endpoints/ExamSessionsAPI.swift @@ -4,13 +4,13 @@ import PromiseKit import SwiftyJSON final class ExamSessionsAPI: APIEndpoint { - override var name: String { "exam-sessions" } + override class var name: String { "exam-sessions" } func get(ids: [ExamSession.IdType]) -> Promise<[ExamSession]> { ExamSession.fetchAsync(ids: ids).then { cachedExamSessions in self.retrieve.request( - requestEndpoint: self.name, - paramName: self.name, + requestEndpoint: Self.name, + paramName: Self.name, ids: ids, updating: cachedExamSessions, withManager: self.manager diff --git a/Stepic/Legacy/Model/Network/Endpoints/InstructionsAPI.swift b/Stepic/Legacy/Model/Network/Endpoints/InstructionsAPI.swift index 2b9d9582fb..9c3c7076a7 100644 --- a/Stepic/Legacy/Model/Network/Endpoints/InstructionsAPI.swift +++ b/Stepic/Legacy/Model/Network/Endpoints/InstructionsAPI.swift @@ -4,17 +4,17 @@ import PromiseKit import SwiftyJSON final class InstructionsAPI: APIEndpoint { - override var name: String { "instructions" } + override class var name: String { "instructions" } func getInstruction(id: Int) -> Promise { self.retrieve - .request(requestEndpoint: "\(self.name)/\(id)", withManager: self.manager) + .request(requestEndpoint: "\(Self.name)/\(id)", withManager: self.manager) .map(InstructionsResponse.init) } func getInstructions(ids: [Int]) -> Promise { self.retrieve - .request(requestEndpoint: self.name, ids: ids, withManager: self.manager) + .request(requestEndpoint: Self.name, ids: ids, withManager: self.manager) .map(InstructionsResponse.init) } } diff --git a/Stepic/Legacy/Model/Network/Endpoints/LastStepsAPI.swift b/Stepic/Legacy/Model/Network/Endpoints/LastStepsAPI.swift index 2b3a87e2b9..3ee59bf277 100644 --- a/Stepic/Legacy/Model/Network/Endpoints/LastStepsAPI.swift +++ b/Stepic/Legacy/Model/Network/Endpoints/LastStepsAPI.swift @@ -11,7 +11,7 @@ import Foundation import SwiftyJSON final class LastStepsAPI: APIEndpoint { - override var name: String { "last-steps" } + override class var name: String { "last-steps" } @discardableResult func retrieve( @@ -22,7 +22,7 @@ final class LastStepsAPI: APIEndpoint { error errorHandler: @escaping ((NetworkError) -> Void) ) -> Request? { self.getObjectsByIds( - requestString: self.name, + requestString: Self.name, printOutput: false, ids: ids, deleteObjects: updatingLastSteps, diff --git a/Stepic/Legacy/Model/Network/Endpoints/LessonsAPI.swift b/Stepic/Legacy/Model/Network/Endpoints/LessonsAPI.swift index 89fd3fe88f..1270847af1 100644 --- a/Stepic/Legacy/Model/Network/Endpoints/LessonsAPI.swift +++ b/Stepic/Legacy/Model/Network/Endpoints/LessonsAPI.swift @@ -12,7 +12,7 @@ import PromiseKit import SwiftyJSON final class LessonsAPI: APIEndpoint { - override var name: String { "lessons" } + override class var name: String { "lessons" } func retrieve( ids: [Int], diff --git a/Stepic/Legacy/Model/Network/Endpoints/MagicLinksAPI.swift b/Stepic/Legacy/Model/Network/Endpoints/MagicLinksAPI.swift index e43226f7e3..10a9ec85c4 100644 --- a/Stepic/Legacy/Model/Network/Endpoints/MagicLinksAPI.swift +++ b/Stepic/Legacy/Model/Network/Endpoints/MagicLinksAPI.swift @@ -4,7 +4,7 @@ import PromiseKit import SwiftyJSON final class MagicLinksAPI: APIEndpoint { - override var name: String { "magic-links" } + override class var name: String { "magic-links" } func create(magicLink: MagicLink) -> Promise { guard let nextURLPath = magicLink.nextURLPath else { @@ -16,7 +16,7 @@ final class MagicLinksAPI: APIEndpoint { } return self.create.request( - requestEndpoint: self.name, + requestEndpoint: Self.name, paramName: "magic-link", creatingObject: magicLink, withManager: self.manager diff --git a/Stepic/Legacy/Model/Network/Endpoints/MetricsAPI.swift b/Stepic/Legacy/Model/Network/Endpoints/MetricsAPI.swift index c96e427b5f..44f5ee3b38 100644 --- a/Stepic/Legacy/Model/Network/Endpoints/MetricsAPI.swift +++ b/Stepic/Legacy/Model/Network/Endpoints/MetricsAPI.swift @@ -4,11 +4,11 @@ import PromiseKit import SwiftyJSON final class MetricsAPI: APIEndpoint { - override var name: String { "metrics" } + override class var name: String { "metrics" } func createBatchMetrics(_ metrics: [JSONDictionary]) -> Promise { self.create.request( - requestEndpoint: "\(self.name)/batch", + requestEndpoint: "\(Self.name)/batch", bodyJSONObject: metrics, withManager: self.manager ).map { _ in () } diff --git a/Stepic/Legacy/Model/Network/Endpoints/MobileTiersAPI.swift b/Stepic/Legacy/Model/Network/Endpoints/MobileTiersAPI.swift index ef32dc71a5..07a28b14bd 100644 --- a/Stepic/Legacy/Model/Network/Endpoints/MobileTiersAPI.swift +++ b/Stepic/Legacy/Model/Network/Endpoints/MobileTiersAPI.swift @@ -4,13 +4,11 @@ import PromiseKit import SwiftyJSON final class MobileTiersAPI: APIEndpoint { - override var name: String { "mobile-tiers" } - - private var calculateRequestEndpoint: String { "\(self.name)/calculate" } + override class var name: String { "mobile-tiers" } func calculate(request: MobileTierCalculateRequest) -> Promise { self.create.request( - requestEndpoint: self.calculateRequestEndpoint, + requestEndpoint: "\(Self.name)/calculate", bodyJSONObject: request.bodyJSON, withManager: self.manager ).map(MobileTierCalculateResponse.init) diff --git a/Stepic/Legacy/Model/Network/Endpoints/NotificationStatusesAPI.swift b/Stepic/Legacy/Model/Network/Endpoints/NotificationStatusesAPI.swift index 8db5696ef5..09ff57eaa1 100644 --- a/Stepic/Legacy/Model/Network/Endpoints/NotificationStatusesAPI.swift +++ b/Stepic/Legacy/Model/Network/Endpoints/NotificationStatusesAPI.swift @@ -11,13 +11,13 @@ import Foundation import PromiseKit final class NotificationStatusesAPI: APIEndpoint { - override var name: String { "notification-statuses" } + override class var name: String { "notification-statuses" } func retrieve() -> Promise { Promise { seal in self.retrieve.request( - requestEndpoint: self.name, - paramName: self.name, + requestEndpoint: Self.name, + paramName: Self.name, params: Parameters(), updatingObjects: [NotificationsStatus](), withManager: self.manager diff --git a/Stepic/Legacy/Model/Network/Endpoints/NotificationsAPI.swift b/Stepic/Legacy/Model/Network/Endpoints/NotificationsAPI.swift index 4c7fe14875..0e0ac1b549 100644 --- a/Stepic/Legacy/Model/Network/Endpoints/NotificationsAPI.swift +++ b/Stepic/Legacy/Model/Network/Endpoints/NotificationsAPI.swift @@ -14,7 +14,7 @@ import PromiseKit import SwiftyJSON final class NotificationsAPI: APIEndpoint { - override var name: String { "notifications" } + override class var name: String { "notifications" } func retrieve(page: Int = 1, notificationType: NotificationType? = nil) -> Promise<([Notification], Meta)> { var parameters = [ @@ -26,8 +26,8 @@ final class NotificationsAPI: APIEndpoint { } return self.retrieve.requestWithFetching( - requestEndpoint: self.name, - paramName: self.name, + requestEndpoint: Self.name, + paramName: Self.name, params: parameters, withManager: self.manager ) @@ -35,7 +35,7 @@ final class NotificationsAPI: APIEndpoint { func update(_ notification: Notification) -> Promise { self.update.request( - requestEndpoint: self.name, + requestEndpoint: Self.name, paramName: "notification", updatingObject: notification, withManager: self.manager @@ -46,7 +46,7 @@ final class NotificationsAPI: APIEndpoint { Promise { seal in checkToken().done { self.manager.request( - "\(StepikApplicationsInfo.apiURL)/\(self.name)/mark-as-read", + "\(StepikApplicationsInfo.apiURL)/\(Self.name)/mark-as-read", method: .post, parameters: nil, encoding: JSONEncoding.default, diff --git a/Stepic/Legacy/Model/Network/Endpoints/ProctorSessionsAPI.swift b/Stepic/Legacy/Model/Network/Endpoints/ProctorSessionsAPI.swift index 7898f5bb91..f0f9d09a91 100644 --- a/Stepic/Legacy/Model/Network/Endpoints/ProctorSessionsAPI.swift +++ b/Stepic/Legacy/Model/Network/Endpoints/ProctorSessionsAPI.swift @@ -4,13 +4,13 @@ import PromiseKit import SwiftyJSON final class ProctorSessionsAPI: APIEndpoint { - override var name: String { "proctor-sessions" } + override class var name: String { "proctor-sessions" } func get(ids: [ProctorSession.IdType]) -> Promise<[ProctorSession]> { ProctorSession.fetchAsync(ids: ids).then { cachedProctorSessions in self.retrieve.request( - requestEndpoint: self.name, - paramName: self.name, + requestEndpoint: Self.name, + paramName: Self.name, ids: ids, updating: cachedProctorSessions, withManager: self.manager diff --git a/Stepic/Legacy/Model/Network/Endpoints/ProfilesAPI.swift b/Stepic/Legacy/Model/Network/Endpoints/ProfilesAPI.swift index 94bddd8e0d..b58a7af3dd 100644 --- a/Stepic/Legacy/Model/Network/Endpoints/ProfilesAPI.swift +++ b/Stepic/Legacy/Model/Network/Endpoints/ProfilesAPI.swift @@ -12,7 +12,7 @@ import PromiseKit import SwiftyJSON final class ProfilesAPI: APIEndpoint { - override var name: String { "profiles" } + override class var name: String { "profiles" } func retrieve( ids: [Int], @@ -32,7 +32,7 @@ final class ProfilesAPI: APIEndpoint { func update(_ profile: Profile) -> Promise { self.update.request( - requestEndpoint: self.name, + requestEndpoint: Self.name, paramName: "profile", updatingObject: profile, withManager: self.manager diff --git a/Stepic/Legacy/Model/Network/Endpoints/ProgressesAPI.swift b/Stepic/Legacy/Model/Network/Endpoints/ProgressesAPI.swift index d08e07d424..83c2948e68 100644 --- a/Stepic/Legacy/Model/Network/Endpoints/ProgressesAPI.swift +++ b/Stepic/Legacy/Model/Network/Endpoints/ProgressesAPI.swift @@ -12,7 +12,7 @@ import PromiseKit import SwiftyJSON final class ProgressesAPI: APIEndpoint { - override var name: String { "progresses" } + override class var name: String { "progresses" } @discardableResult func retrieve( @@ -29,7 +29,7 @@ final class ProgressesAPI: APIEndpoint { } return getObjectsByIds( - requestString: self.name, + requestString: Self.name, printOutput: false, ids: ids, deleteObjects: existing, diff --git a/Stepic/Legacy/Model/Network/Endpoints/PromoCodesAPI.swift b/Stepic/Legacy/Model/Network/Endpoints/PromoCodesAPI.swift index 37f8d1dbcb..906897abf3 100644 --- a/Stepic/Legacy/Model/Network/Endpoints/PromoCodesAPI.swift +++ b/Stepic/Legacy/Model/Network/Endpoints/PromoCodesAPI.swift @@ -6,11 +6,11 @@ import SwiftyJSON final class PromoCodesAPI: APIEndpoint { typealias CheckPromoCodeResponse = (price: Float, currencyCode: String) - override var name: String { "promo-codes" } + override class var name: String { "promo-codes" } func check(courseID: Course.IdType, name: String) -> Promise { Promise { seal in - let urlPath = "\(StepikApplicationsInfo.apiURL)/\(self.name)/check" + let urlPath = "\(StepikApplicationsInfo.apiURL)/\(Self.name)/check" let params: Parameters = [ JSONKey.course.rawValue: courseID, diff --git a/Stepic/Legacy/Model/Network/Endpoints/QueriesAPI.swift b/Stepic/Legacy/Model/Network/Endpoints/QueriesAPI.swift index dbde24ea8a..e4683dcbc6 100644 --- a/Stepic/Legacy/Model/Network/Endpoints/QueriesAPI.swift +++ b/Stepic/Legacy/Model/Network/Endpoints/QueriesAPI.swift @@ -13,7 +13,7 @@ import SwiftyJSON //TODO: Refactor this by adding class Query: JSONSerializable final class QueriesAPI: APIEndpoint { - override var name: String { "queries" } + override class var name: String { "queries" } func retrieve(query: String) -> Promise<[String]> { let params: Parameters = ["query": query] diff --git a/Stepic/Legacy/Model/Network/Endpoints/RecommendationsAPI.swift b/Stepic/Legacy/Model/Network/Endpoints/RecommendationsAPI.swift index 45fcef9d30..49ddafa5ab 100644 --- a/Stepic/Legacy/Model/Network/Endpoints/RecommendationsAPI.swift +++ b/Stepic/Legacy/Model/Network/Endpoints/RecommendationsAPI.swift @@ -13,7 +13,7 @@ import SwiftyJSON //TODO: Refactor this class into two separate API classes final class RecommendationsAPI: APIEndpoint { - override var name: String { "recommendations" } + override class var name: String { "recommendations" } var reactionName: String { "recommendation-reactions" } @@ -24,7 +24,7 @@ final class RecommendationsAPI: APIEndpoint { ) -> Promise<[Int]> { Promise { seal in self.manager.request( - "\(StepikApplicationsInfo.apiURL)/\(self.name)", + "\(StepikApplicationsInfo.apiURL)/\(Self.name)", parameters: [ "course": courseId, "count": count diff --git a/Stepic/Legacy/Model/Network/Endpoints/ReviewSessionsAPI.swift b/Stepic/Legacy/Model/Network/Endpoints/ReviewSessionsAPI.swift index fe502050d5..0034cce0a2 100644 --- a/Stepic/Legacy/Model/Network/Endpoints/ReviewSessionsAPI.swift +++ b/Stepic/Legacy/Model/Network/Endpoints/ReviewSessionsAPI.swift @@ -4,7 +4,7 @@ import PromiseKit import SwiftyJSON final class ReviewSessionsAPI: APIEndpoint { - override var name: String { "review-sessions" } + override class var name: String { "review-sessions" } func createReviewSession(submissionID: Submission.IdType, blockName: String?) -> Promise { let body = [ @@ -14,7 +14,7 @@ final class ReviewSessionsAPI: APIEndpoint { ] return self.create - .request(requestEndpoint: self.name, bodyJSONObject: body, withManager: self.manager) + .request(requestEndpoint: Self.name, bodyJSONObject: body, withManager: self.manager) .map { ReviewSessionResponse(json: $0, blockName: blockName ?? "") } } @@ -26,7 +26,7 @@ final class ReviewSessionsAPI: APIEndpoint { ] return self.create - .request(requestEndpoint: self.name, bodyJSONObject: body, withManager: self.manager) + .request(requestEndpoint: Self.name, bodyJSONObject: body, withManager: self.manager) .map { ReviewSessionResponse(json: $0, blockName: blockName ?? "") } } @@ -37,7 +37,7 @@ final class ReviewSessionsAPI: APIEndpoint { /// - Returns: A promise with an `ReviewSessionResponse`. func getReviewSessions(ids: [Int], blockName: String?) -> Promise { self.retrieve - .request(requestEndpoint: self.name, ids: ids, withManager: self.manager) + .request(requestEndpoint: Self.name, ids: ids, withManager: self.manager) .map { ReviewSessionResponse(json: $0, blockName: blockName ?? "") } } @@ -52,7 +52,7 @@ final class ReviewSessionsAPI: APIEndpoint { ] return self.retrieve - .request(requestEndpoint: self.name, params: params, withManager: self.manager) + .request(requestEndpoint: Self.name, params: params, withManager: self.manager) .map { ReviewSessionResponse(json: $0, blockName: blockName ?? "") } } diff --git a/Stepic/Legacy/Model/Network/Endpoints/ReviewsAPI.swift b/Stepic/Legacy/Model/Network/Endpoints/ReviewsAPI.swift index 41019c1a4e..657efd8950 100644 --- a/Stepic/Legacy/Model/Network/Endpoints/ReviewsAPI.swift +++ b/Stepic/Legacy/Model/Network/Endpoints/ReviewsAPI.swift @@ -4,7 +4,7 @@ import PromiseKit import SwiftyJSON final class ReviewsAPI: APIEndpoint { - override var name: String { "reviews" } + override class var name: String { "reviews" } func createReview(sessionID: Int, blockName: String?) -> Promise { let body = [ @@ -15,13 +15,13 @@ final class ReviewsAPI: APIEndpoint { ] return self.create - .request(requestEndpoint: self.name, bodyJSONObject: body, withManager: self.manager) + .request(requestEndpoint: Self.name, bodyJSONObject: body, withManager: self.manager) .map { ReviewsResponse(json: $0, blockName: blockName ?? "") } } func getReviews(ids: [Int], blockName: String?) -> Promise { self.retrieve - .request(requestEndpoint: self.name, ids: ids, withManager: self.manager) + .request(requestEndpoint: Self.name, ids: ids, withManager: self.manager) .map { ReviewsResponse(json: $0, blockName: blockName ?? "") } } @@ -36,7 +36,7 @@ final class ReviewsAPI: APIEndpoint { ] return self.retrieve - .request(requestEndpoint: self.name, params: params, withManager: self.manager) + .request(requestEndpoint: Self.name, params: params, withManager: self.manager) .map { ReviewSessionResponse(json: $0, blockName: blockName ?? "") } } diff --git a/Stepic/Legacy/Model/Network/Endpoints/SearchResultsAPI.swift b/Stepic/Legacy/Model/Network/Endpoints/SearchResultsAPI.swift index b146b3fbbb..7edd9772e4 100644 --- a/Stepic/Legacy/Model/Network/Endpoints/SearchResultsAPI.swift +++ b/Stepic/Legacy/Model/Network/Endpoints/SearchResultsAPI.swift @@ -4,7 +4,7 @@ import PromiseKit import SwiftyJSON final class SearchResultsAPI: APIEndpoint { - override var name: String { "search-results" } + override class var name: String { "search-results" } func searchCourses( query: String, @@ -81,12 +81,12 @@ final class SearchResultsAPI: APIEndpoint { } return self.retrieve.request( - requestEndpoint: self.name, + requestEndpoint: Self.name, params: params, withManager: self.manager ).map { json -> ([SearchResultPlainObject], Meta) in - let searchResults = json[self.name].arrayValue.map(SearchResultPlainObject.init(json:)) - let meta = Meta(json: json[Meta.JSONKey.meta.rawValue]) + let searchResults = json[Self.name].arrayValue.map(SearchResultPlainObject.init(json:)) + let meta = Meta(json: json["meta"]) return (searchResults, meta) } } diff --git a/Stepic/Legacy/Model/Network/Endpoints/SectionsAPI.swift b/Stepic/Legacy/Model/Network/Endpoints/SectionsAPI.swift index 7b0e567f1d..9f6c16164e 100644 --- a/Stepic/Legacy/Model/Network/Endpoints/SectionsAPI.swift +++ b/Stepic/Legacy/Model/Network/Endpoints/SectionsAPI.swift @@ -12,7 +12,7 @@ import PromiseKit import SwiftyJSON final class SectionsAPI: APIEndpoint { - override var name: String { "sections" } + override class var name: String { "sections" } func retrieve(ids: [Int], existing: [Section]) -> Promise<[Section]> { self.getObjectsByIds(ids: ids, updating: existing, printOutput: false) @@ -38,7 +38,7 @@ final class SectionsAPI: APIEndpoint { error errorHandler: @escaping ((NetworkError) -> Void) ) -> Request? { self.getObjectsByIds( - requestString: self.name, + requestString: Self.name, printOutput: false, ids: ids, deleteObjects: existing, diff --git a/Stepic/Legacy/Model/Network/Endpoints/SocialProfilesAPI.swift b/Stepic/Legacy/Model/Network/Endpoints/SocialProfilesAPI.swift index 6052549127..2fe39c8ceb 100644 --- a/Stepic/Legacy/Model/Network/Endpoints/SocialProfilesAPI.swift +++ b/Stepic/Legacy/Model/Network/Endpoints/SocialProfilesAPI.swift @@ -4,7 +4,7 @@ import PromiseKit import SwiftyJSON final class SocialProfilesAPI: APIEndpoint { - override var name: String { "social-profiles" } + override class var name: String { "social-profiles" } /// Get social profiles by ids. func retrieve(ids: [Int], page: Int = 1) -> Promise<([SocialProfile], Meta)> { @@ -15,8 +15,8 @@ final class SocialProfilesAPI: APIEndpoint { ] self.retrieve.requestWithFetching( - requestEndpoint: self.name, - paramName: self.name, + requestEndpoint: Self.name, + paramName: Self.name, params: parameters, withManager: self.manager ).done { socialProfiles, meta, _ in diff --git a/Stepic/Legacy/Model/Network/Endpoints/StepSourcesAPI.swift b/Stepic/Legacy/Model/Network/Endpoints/StepSourcesAPI.swift index 4301c02421..2db5f46ffd 100644 --- a/Stepic/Legacy/Model/Network/Endpoints/StepSourcesAPI.swift +++ b/Stepic/Legacy/Model/Network/Endpoints/StepSourcesAPI.swift @@ -4,7 +4,7 @@ import PromiseKit import SwiftyJSON final class StepSourcesAPI: APIEndpoint { - override var name: String { "step-sources" } + override class var name: String { "step-sources" } /// Get step sources by ids. func retrieve(ids: [StepSource.IdType], page: Int = 1) -> Promise<([StepSource], Meta)> { @@ -14,8 +14,8 @@ final class StepSourcesAPI: APIEndpoint { ] return self.retrieve.request( - requestEndpoint: self.name, - paramName: self.name, + requestEndpoint: Self.name, + paramName: Self.name, params: parameters, withManager: self.manager ) @@ -23,7 +23,7 @@ final class StepSourcesAPI: APIEndpoint { func update(_ stepSource: StepSource) -> Promise { self.update.request( - requestEndpoint: self.name, + requestEndpoint: Self.name, paramName: "stepSource", updatingObject: stepSource, withManager: self.manager diff --git a/Stepic/Legacy/Model/Network/Endpoints/StepicsAPI.swift b/Stepic/Legacy/Model/Network/Endpoints/StepicsAPI.swift index 5c3d2292f8..f5a575cdf4 100644 --- a/Stepic/Legacy/Model/Network/Endpoints/StepicsAPI.swift +++ b/Stepic/Legacy/Model/Network/Endpoints/StepicsAPI.swift @@ -12,12 +12,12 @@ import PromiseKit import SwiftyJSON final class StepicsAPI: APIEndpoint { - override var name: String { "stepics" } + override class var name: String { "stepics" } func retrieveCurrentUser() -> Promise { Promise { seal in self.manager.request( - "\(StepikApplicationsInfo.apiURL)/\(name)/1", + "\(StepikApplicationsInfo.apiURL)/\(Self.name)/1", parameters: nil, encoding: URLEncoding.default, headers: AuthInfo.shared.initialHTTPHeaders diff --git a/Stepic/Legacy/Model/Network/Endpoints/StepsAPI.swift b/Stepic/Legacy/Model/Network/Endpoints/StepsAPI.swift index ea3428ebf3..84b01802de 100644 --- a/Stepic/Legacy/Model/Network/Endpoints/StepsAPI.swift +++ b/Stepic/Legacy/Model/Network/Endpoints/StepsAPI.swift @@ -14,7 +14,7 @@ import SwiftyJSON final class StepsAPI: APIEndpoint { private let stepsPersistenceService: StepsPersistenceServiceProtocol - override var name: String { "steps" } + override class var name: String { "steps" } init(stepsPersistenceService: StepsPersistenceServiceProtocol = StepsPersistenceService()) { self.stepsPersistenceService = stepsPersistenceService diff --git a/Stepic/Legacy/Model/Network/Endpoints/StorageRecordsAPI.swift b/Stepic/Legacy/Model/Network/Endpoints/StorageRecordsAPI.swift index d6b0b7fdae..50da0a6861 100644 --- a/Stepic/Legacy/Model/Network/Endpoints/StorageRecordsAPI.swift +++ b/Stepic/Legacy/Model/Network/Endpoints/StorageRecordsAPI.swift @@ -14,7 +14,7 @@ import SwiftyJSON final class StorageRecordsAPI: APIEndpoint { private static let createUpdateParamName = "storage-record" - override var name: String { "storage-records" } + override class var name: String { "storage-records" } func retrieve( userID: User.IdType, @@ -31,8 +31,8 @@ final class StorageRecordsAPI: APIEndpoint { } return self.retrieve.request( - requestEndpoint: self.name, - paramName: self.name, + requestEndpoint: Self.name, + paramName: Self.name, params: params, withManager: self.manager ) @@ -53,20 +53,20 @@ final class StorageRecordsAPI: APIEndpoint { } return self.retrieve.request( - requestEndpoint: self.name, - paramName: self.name, + requestEndpoint: Self.name, + paramName: Self.name, params: params, withManager: self.manager ) } func delete(id: Int) -> Promise { - self.delete.request(requestEndpoint: self.name, deletingId: id, withManager: self.manager) + self.delete.request(requestEndpoint: Self.name, deletingId: id, withManager: self.manager) } func create(record: StorageRecord) -> Promise { self.create.request( - requestEndpoint: self.name, + requestEndpoint: Self.name, paramName: Self.createUpdateParamName, creatingObject: record, withManager: self.manager @@ -75,7 +75,7 @@ final class StorageRecordsAPI: APIEndpoint { func update(record: StorageRecord) -> Promise { self.update.request( - requestEndpoint: self.name, + requestEndpoint: Self.name, paramName: Self.createUpdateParamName, updatingObject: record, withManager: self.manager diff --git a/Stepic/Legacy/Model/Network/Endpoints/StoryTemplatesAPI.swift b/Stepic/Legacy/Model/Network/Endpoints/StoryTemplatesAPI.swift index c6620c36e2..30f3a9e905 100644 --- a/Stepic/Legacy/Model/Network/Endpoints/StoryTemplatesAPI.swift +++ b/Stepic/Legacy/Model/Network/Endpoints/StoryTemplatesAPI.swift @@ -3,7 +3,7 @@ import Foundation import PromiseKit final class StoryTemplatesAPI: APIEndpoint { - override var name: String { "story-templates" } + override class var name: String { "story-templates" } func retrieve( isPublished: Bool?, @@ -22,8 +22,8 @@ final class StoryTemplatesAPI: APIEndpoint { } self.retrieve.requestWithCollectAllPages( - requestEndpoint: self.name, - paramName: self.name, + requestEndpoint: Self.name, + paramName: Self.name, params: params, withManager: self.manager ).done { stories in @@ -36,8 +36,8 @@ final class StoryTemplatesAPI: APIEndpoint { func retrieve(ids: [Int]) -> Promise<[Story]> { self.retrieve.request( - requestEndpoint: self.name, - paramName: self.name, + requestEndpoint: Self.name, + paramName: Self.name, ids: ids, updating: [], withManager: self.manager diff --git a/Stepic/Legacy/Model/Network/Endpoints/SubmissionsAPI.swift b/Stepic/Legacy/Model/Network/Endpoints/SubmissionsAPI.swift index 9c982c78c9..c44b7774fa 100644 --- a/Stepic/Legacy/Model/Network/Endpoints/SubmissionsAPI.swift +++ b/Stepic/Legacy/Model/Network/Endpoints/SubmissionsAPI.swift @@ -12,7 +12,7 @@ import PromiseKit import SwiftyJSON final class SubmissionsAPI: APIEndpoint { - override var name: String { "submissions" } + override class var name: String { "submissions" } @discardableResult private func retrieve( @@ -78,7 +78,7 @@ final class SubmissionsAPI: APIEndpoint { } self.manager.request( - "\(StepikApplicationsInfo.apiURL)/\(self.name)", + "\(StepikApplicationsInfo.apiURL)/\(Self.name)", method: .get, parameters: parameters, encoding: URLEncoding.default, diff --git a/Stepic/Legacy/Model/Network/Endpoints/UnitsAPI.swift b/Stepic/Legacy/Model/Network/Endpoints/UnitsAPI.swift index 596fcc5f81..961b4d164e 100644 --- a/Stepic/Legacy/Model/Network/Endpoints/UnitsAPI.swift +++ b/Stepic/Legacy/Model/Network/Endpoints/UnitsAPI.swift @@ -14,7 +14,7 @@ import SwiftyJSON final class UnitsAPI: APIEndpoint { private let unitsPersistenceService: UnitsPersistenceServiceProtocol - override var name: String { "units" } + override class var name: String { "units" } init( unitsPersistenceService: UnitsPersistenceServiceProtocol = UnitsPersistenceService(), @@ -30,8 +30,8 @@ final class UnitsAPI: APIEndpoint { return Promise { seal in self.retrieve.request( - requestEndpoint: self.name, - paramName: self.name, + requestEndpoint: Self.name, + paramName: Self.name, params: params, updatingObjects: [Unit](), withManager: self.manager @@ -69,7 +69,7 @@ final class UnitsAPI: APIEndpoint { error errorHandler: @escaping (NetworkError) -> Void ) -> Request? { self.getObjectsByIds( - requestString: self.name, + requestString: Self.name, printOutput: false, ids: ids, deleteObjects: existing, diff --git a/Stepic/Legacy/Model/Network/Endpoints/UserActivitiesAPI.swift b/Stepic/Legacy/Model/Network/Endpoints/UserActivitiesAPI.swift index 5e2cc2ee91..8ef6886c77 100644 --- a/Stepic/Legacy/Model/Network/Endpoints/UserActivitiesAPI.swift +++ b/Stepic/Legacy/Model/Network/Endpoints/UserActivitiesAPI.swift @@ -12,12 +12,12 @@ import PromiseKit import SwiftyJSON final class UserActivitiesAPI: APIEndpoint { - override var name: String { "user-activities" } + override class var name: String { "user-activities" } func retrieve(user userId: Int) -> Promise { self.retrieve.request( - requestEndpoint: self.name, - paramName: self.name, + requestEndpoint: Self.name, + paramName: Self.name, id: userId, withManager: self.manager ) diff --git a/Stepic/Legacy/Model/Network/Endpoints/UserCodeRunsAPI.swift b/Stepic/Legacy/Model/Network/Endpoints/UserCodeRunsAPI.swift index c08bbaccab..b38d9df2f1 100644 --- a/Stepic/Legacy/Model/Network/Endpoints/UserCodeRunsAPI.swift +++ b/Stepic/Legacy/Model/Network/Endpoints/UserCodeRunsAPI.swift @@ -4,10 +4,10 @@ import PromiseKit import SwiftyJSON final class UserCodeRunsAPI: APIEndpoint { - override var name: String { "user-code-runs" } + override class var name: String { "user-code-runs" } func retrieve(id: UserCodeRun.IdType) -> Promise { - self.retrieve.request(requestEndpoint: self.name, paramName: self.name, id: id, withManager: self.manager) + self.retrieve.request(requestEndpoint: Self.name, paramName: Self.name, id: id, withManager: self.manager) } func create( @@ -26,8 +26,8 @@ final class UserCodeRunsAPI: APIEndpoint { ) return self.create.request( - requestEndpoint: self.name, - paramName: self.name, + requestEndpoint: Self.name, + paramName: Self.name, creatingObject: userCodeRun, withManager: self.manager ) diff --git a/Stepic/Legacy/Model/Network/Endpoints/UserCoursesAPI.swift b/Stepic/Legacy/Model/Network/Endpoints/UserCoursesAPI.swift index 83dfb4d16b..6dae79314f 100644 --- a/Stepic/Legacy/Model/Network/Endpoints/UserCoursesAPI.swift +++ b/Stepic/Legacy/Model/Network/Endpoints/UserCoursesAPI.swift @@ -12,7 +12,7 @@ import PromiseKit import SwiftyJSON final class UserCoursesAPI: APIEndpoint { - override var name: String { "user-courses" } + override class var name: String { "user-courses" } private let userCoursesPersistenceService: UserCoursesPersistenceServiceProtocol @@ -51,8 +51,8 @@ final class UserCoursesAPI: APIEndpoint { self.userCoursesPersistenceService.fetchAll() }.then { cachedUserCourses -> Promise<([UserCourse], Meta, JSON)> in self.retrieve.request( - requestEndpoint: self.name, - paramName: self.name, + requestEndpoint: Self.name, + paramName: Self.name, params: params, updatingObjects: cachedUserCourses, withManager: self.manager @@ -74,8 +74,8 @@ final class UserCoursesAPI: APIEndpoint { self.userCoursesPersistenceService.fetch(courseID: courseID) }.then { cachedUserCourses -> Promise<([UserCourse], Meta, JSON)> in self.retrieve.request( - requestEndpoint: self.name, - paramName: self.name, + requestEndpoint: Self.name, + paramName: Self.name, params: params, updatingObjects: cachedUserCourses, withManager: self.manager @@ -90,7 +90,7 @@ final class UserCoursesAPI: APIEndpoint { func update(_ userCourse: UserCourse) -> Promise { self.update.request( - requestEndpoint: self.name, + requestEndpoint: Self.name, paramName: "userCourse", updatingObject: userCourse, withManager: self.manager diff --git a/Stepic/Legacy/Model/Network/Endpoints/UsersAPI.swift b/Stepic/Legacy/Model/Network/Endpoints/UsersAPI.swift index 17d960599f..cafb519475 100644 --- a/Stepic/Legacy/Model/Network/Endpoints/UsersAPI.swift +++ b/Stepic/Legacy/Model/Network/Endpoints/UsersAPI.swift @@ -12,7 +12,7 @@ import PromiseKit import SwiftyJSON final class UsersAPI: APIEndpoint { - override var name: String { "users" } + override class var name: String { "users" } func retrieve( ids: [Int], @@ -48,7 +48,7 @@ extension UsersAPI { error errorHandler: @escaping ((NetworkError) -> Void) ) -> Request? { self.getObjectsByIds( - requestString: self.name, + requestString: Self.name, printOutput: false, ids: ids, deleteObjects: existing, diff --git a/Stepic/Legacy/Model/Network/Endpoints/ViewsAPI.swift b/Stepic/Legacy/Model/Network/Endpoints/ViewsAPI.swift index f38711749a..0cde116c0c 100644 --- a/Stepic/Legacy/Model/Network/Endpoints/ViewsAPI.swift +++ b/Stepic/Legacy/Model/Network/Endpoints/ViewsAPI.swift @@ -12,11 +12,11 @@ import PromiseKit import SwiftyJSON final class ViewsAPI: APIEndpoint { - override var name: String { "views" } + override class var name: String { "views" } func create(view: StepikModelView) -> Promise { self.create.request( - requestEndpoint: self.name, + requestEndpoint: Self.name, paramName: "view", creatingObject: view, withManager: self.manager diff --git a/Stepic/Legacy/Model/Network/Endpoints/VisitedCoursesAPI.swift b/Stepic/Legacy/Model/Network/Endpoints/VisitedCoursesAPI.swift index 5790ef1d6f..9ef501b628 100644 --- a/Stepic/Legacy/Model/Network/Endpoints/VisitedCoursesAPI.swift +++ b/Stepic/Legacy/Model/Network/Endpoints/VisitedCoursesAPI.swift @@ -4,7 +4,7 @@ import PromiseKit import SwiftyJSON final class VisitedCoursesAPI: APIEndpoint { - override var name: String { "visited-courses" } + override class var name: String { "visited-courses" } func retrieve(page: Int = 1) -> Promise<([VisitedCourse], Meta)> { let params: Parameters = [ @@ -12,8 +12,8 @@ final class VisitedCoursesAPI: APIEndpoint { ] return self.retrieve.request( - requestEndpoint: self.name, - paramName: self.name, + requestEndpoint: Self.name, + paramName: Self.name, params: params, withManager: self.manager ) diff --git a/Stepic/Legacy/Model/Network/Endpoints/VotesAPI.swift b/Stepic/Legacy/Model/Network/Endpoints/VotesAPI.swift index 7fdc2928c8..7776bc4c5c 100644 --- a/Stepic/Legacy/Model/Network/Endpoints/VotesAPI.swift +++ b/Stepic/Legacy/Model/Network/Endpoints/VotesAPI.swift @@ -12,11 +12,11 @@ import PromiseKit import SwiftyJSON final class VotesAPI: APIEndpoint { - override var name: String { "votes" } + override class var name: String { "votes" } func update(_ vote: Vote) -> Promise { self.update.request( - requestEndpoint: self.name, + requestEndpoint: Self.name, paramName: "vote", updatingObject: vote, withManager: self.manager diff --git a/Stepic/Legacy/Model/Network/Endpoints/WishListsAPI.swift b/Stepic/Legacy/Model/Network/Endpoints/WishListsAPI.swift index 8130da7934..4133d3cd42 100644 --- a/Stepic/Legacy/Model/Network/Endpoints/WishListsAPI.swift +++ b/Stepic/Legacy/Model/Network/Endpoints/WishListsAPI.swift @@ -1,62 +1,45 @@ import Alamofire import Foundation import PromiseKit +import StepikModel final class WishListsAPI: APIEndpoint { - override var name: String { "wish-lists" } + override class var name: String { "wish-lists" } - func retrieveWishlistEntry(courseID: Course.IdType) -> Promise { - self.retrieve.request( - requestEndpoint: self.name, - paramName: self.name, - params: ["course": courseID], - withManager: self.manager - ).then { wishlistEntries, _ -> Promise in - return .value(wishlistEntries.first) - } + func retrieveWishlistEntry(courseID: Course.IdType) -> Promise { + self.retrieve + .requestDecodableObjects( + requestEndpoint: Self.name, + params: [JSONKey.course.rawValue: courseID], + withManager: self.manager + ) + .map { $0.decodedObjects.first } } - func retrieveWishlist(page: Int = 1) -> Promise<([WishlistEntryPlainObject], Meta)> { - self.retrieve.request( - requestEndpoint: self.name, - paramName: self.name, - params: ["page": page], - withManager: self.manager - ) + func retrieveAllWishlistPages() -> Promise<[WishlistEntry]> { + self.retrieve.requestDecodableObjectsWithCollectAllPages(requestEndpoint: Self.name, withManager: self.manager) } - func retrieveAllWishlistPages() -> Promise<[WishlistEntryPlainObject]> { - self.retrieve.requestWithCollectAllPages( - requestEndpoint: self.name, - paramName: self.name, - params: [:], - withManager: self.manager - ) - } + func createWishlistEntry(courseID: Course.IdType) -> Promise { + let body = [ + JSONKey.wishList.rawValue: [ + JSONKey.course.rawValue: courseID, + JSONKey.platform.rawValue: PlatformType.mobile.stringValue + ] + ] - func createWishlistEntry(courseID: Course.IdType) -> Promise { - let wishlistEntryToAdd = WishlistEntryPlainObject( - id: -1, - courseID: courseID, - userID: -1, - createDate: nil, - platform: PlatformType.mobile.stringValue - ) + return self.create + .requestDecodableObjects(requestEndpoint: Self.name, bodyJSONObject: body, withManager: self.manager) + .compactMap { $0.decodedObjects.first } + } - return self.create.request( - requestEndpoint: self.name, - paramName: "wish-list", - creatingObject: wishlistEntryToAdd, - withManager: self.manager - ).compactMap { _, json -> WishlistEntryPlainObject? in - if let createdObjectJSON = json[self.name].arrayValue.first { - return WishlistEntryPlainObject(json: createdObjectJSON) - } - return nil - } + func deleteWishlistEntry(wishlistEntryID: Int) -> Promise { + self.delete.request(requestEndpoint: Self.name, deletingId: wishlistEntryID, withManager: self.manager) } - func deleteWishlistEntry(wishlistEntryID: WishlistEntryPlainObject.IdType) -> Promise { - self.delete.request(requestEndpoint: self.name, deletingId: wishlistEntryID, withManager: self.manager) + private enum JSONKey: String { + case course + case platform + case wishList = "wish-list" } } diff --git a/Stepic/Legacy/Model/Network/Meta.swift b/Stepic/Legacy/Model/Network/Meta.swift index ab8f0c597c..aaa5048f36 100644 --- a/Stepic/Legacy/Model/Network/Meta.swift +++ b/Stepic/Legacy/Model/Network/Meta.swift @@ -1,31 +1,26 @@ import Foundation import SwiftyJSON -struct Meta { - let hasNext: Bool - let hasPrev: Bool +struct Meta: Codable { let page: Int + let hasNext: Bool + let hasPrevious: Bool static var oneAndOnlyPage: Meta { - Meta(hasNext: false, hasPrev: false, page: 1) + Meta(page: 1, hasNext: false, hasPrevious: false) } - init(hasNext: Bool, hasPrev: Bool, page: Int) { - self.hasNext = hasNext - self.hasPrev = hasPrev - self.page = page + enum CodingKeys: String, CodingKey { + case page + case hasNext = "has_next" + case hasPrevious = "has_previous" } +} +extension Meta { init(json: JSON) { - self.hasNext = json[JSONKey.hasNext.rawValue].boolValue - self.hasPrev = json[JSONKey.hasPrevious.rawValue].boolValue - self.page = json[JSONKey.page.rawValue].intValue - } - - enum JSONKey: String { - case meta - case hasNext = "has_next" - case hasPrevious = "has_previous" - case page + self.page = json[CodingKeys.page.rawValue].intValue + self.hasNext = json[CodingKeys.hasNext.rawValue].boolValue + self.hasPrevious = json[CodingKeys.hasPrevious.rawValue].boolValue } } diff --git a/Stepic/Legacy/Model/Network/RequestMakers/CreateRequestMaker.swift b/Stepic/Legacy/Model/Network/RequestMakers/CreateRequestMaker.swift index dfce7da9d9..54d1abef91 100644 --- a/Stepic/Legacy/Model/Network/RequestMakers/CreateRequestMaker.swift +++ b/Stepic/Legacy/Model/Network/RequestMakers/CreateRequestMaker.swift @@ -123,3 +123,50 @@ final class CreateRequestMaker { case badRequest } } + +// MARK: - Response Decodable - + +extension CreateRequestMaker { + func requestDecodable( + requestEndpoint: String, + bodyJSONObject body: Any, + withManager manager: Alamofire.Session + ) -> Promise { + guard let url = URL(string: "\(StepikApplicationsInfo.apiURL)/\(requestEndpoint)") else { + return Promise(error: Error.badRequest) + } + + var request = URLRequest(url: url) + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.httpMethod = "POST" + + do { + request.httpBody = try JSONSerialization.data(withJSONObject: body, options: []) + } catch { + return Promise(error: Error.badRequest) + } + + return Promise { seal in + checkToken().done { + manager.request(request).validate().responseDecodable { (response: AFDataResponse) in + switch response.result { + case .failure(let error): + seal.reject(NetworkError(error: error)) + case .success(let decodable): + seal.fulfill(decodable) + } + } + }.catch { error in + seal.reject(error) + } + } + } + + func requestDecodableObjects( + requestEndpoint: String, + bodyJSONObject body: Any, + withManager manager: Alamofire.Session + ) -> Promise> { + self.requestDecodable(requestEndpoint: requestEndpoint, bodyJSONObject: body, withManager: manager) + } +} diff --git a/Stepic/Legacy/Model/Network/RequestMakers/RetrieveRequestMaker.swift b/Stepic/Legacy/Model/Network/RequestMakers/RetrieveRequestMaker.swift index da6bd6838b..be489969ea 100644 --- a/Stepic/Legacy/Model/Network/RequestMakers/RetrieveRequestMaker.swift +++ b/Stepic/Legacy/Model/Network/RequestMakers/RetrieveRequestMaker.swift @@ -12,6 +12,8 @@ import PromiseKit import SwiftyJSON final class RetrieveRequestMaker { + // MARK: SwiftyJSON + func request( requestEndpoint: String, params: Parameters? = nil, @@ -134,7 +136,7 @@ final class RetrieveRequestMaker { func requestWithCollectAllPages( requestEndpoint: String, paramName: String, - params: Parameters, + params: Parameters = [:], updatingObjects: [T] = [], withManager manager: Alamofire.Session ) -> Promise<[T]> { @@ -144,7 +146,7 @@ final class RetrieveRequestMaker { Promise { seal in firstly { () -> Promise<([T], Meta)> in var currentPageParams = params - currentPageParams["page"] = page + currentPageParams[Meta.CodingKeys.page.rawValue] = page return self.request( requestEndpoint: requestEndpoint, @@ -353,3 +355,80 @@ final class RetrieveRequestMaker { } } } + +// MARK: - Response Decodable - + +extension RetrieveRequestMaker { + func requestDecodable( + requestEndpoint: String, + params: Parameters? = nil, + withManager manager: Alamofire.Session + ) -> Promise { + Promise { seal in + checkToken().done { + manager.request( + "\(StepikApplicationsInfo.apiURL)/\(requestEndpoint)", + parameters: params, + encoding: URLEncoding.default + ).validate().responseDecodable { (response: AFDataResponse) in + switch response.result { + case .failure(let error): + seal.reject(NetworkError(error: error)) + case .success(let decodable): + seal.fulfill(decodable) + } + } + }.catch { error in + seal.reject(error) + } + } + } + + func requestDecodableObjects( + requestEndpoint: String, + params: Parameters? = nil, + withManager manager: Alamofire.Session + ) -> Promise> { + self.requestDecodable(requestEndpoint: requestEndpoint, params: params, withManager: manager) + } + + func requestDecodableObjectsWithCollectAllPages( + requestEndpoint: String, + params: Parameters = [:], + withManager manager: Alamofire.Session + ) -> Promise<[T]> { + var allDecodedObjects = [T]() + + func load(page: Int) -> Promise { + Promise { seal in + firstly { () -> Promise> in + var currentPageParams = params + currentPageParams[Meta.CodingKeys.page.rawValue] = page + + return self.requestDecodableObjects( + requestEndpoint: requestEndpoint, + params: currentPageParams, + withManager: manager + ) + }.done { response in + allDecodedObjects.append(contentsOf: response.decodedObjects) + seal.fulfill(response.meta.hasNext) + }.catch { error in + seal.reject(error) + } + } + } + + func collect(page: Int) -> Promise<[T]> { + load(page: page).then { hasNext -> Promise<[T]> in + if hasNext { + return collect(page: page + 1) + } else { + return .value(allDecodedObjects) + } + } + } + + return collect(page: 1) + } +} diff --git a/Stepic/Legacy/Model/PlainObjects/WishlistEntryPlainObject.swift b/Stepic/Legacy/Model/PlainObjects/WishlistEntryPlainObject.swift deleted file mode 100644 index 5183f5525d..0000000000 --- a/Stepic/Legacy/Model/PlainObjects/WishlistEntryPlainObject.swift +++ /dev/null @@ -1,37 +0,0 @@ -import Foundation -import SwiftyJSON - -struct WishlistEntryPlainObject: JSONSerializable { - let id: Int - let courseID: Int - let userID: Int - let createDate: Date? - let platform: String -} - -extension WishlistEntryPlainObject { - var json: JSON { - [ - JSONKey.course.rawValue: self.courseID, - JSONKey.platform.rawValue: self.platform - ] - } - - init(json: JSON) { - self.id = json[JSONKey.id.rawValue].intValue - self.courseID = json[JSONKey.course.rawValue].intValue - self.userID = json[JSONKey.user.rawValue].intValue - self.createDate = Parser.dateFromTimedateJSON(json[JSONKey.createDate.rawValue]) - self.platform = json[JSONKey.platform.rawValue].stringValue - } - - func update(json: JSON) {} - - enum JSONKey: String { - case id - case course - case user - case createDate = "create_date" - case platform - } -} diff --git a/Stepic/Sources/Modules/CourseList/Provider/CourseListNetworkService.swift b/Stepic/Sources/Modules/CourseList/Provider/CourseListNetworkService.swift index 43047efb6b..8c01a5317d 100644 --- a/Stepic/Sources/Modules/CourseList/Provider/CourseListNetworkService.swift +++ b/Stepic/Sources/Modules/CourseList/Provider/CourseListNetworkService.swift @@ -33,9 +33,9 @@ class BaseCourseListNetworkService { let pageIndex = max(0, page - 1) let hasNext = slices.indices.contains(pageIndex + 1) - let hasPrev = slices.indices.contains(pageIndex - 1) + let hasPrevious = slices.indices.contains(pageIndex - 1) - return (slices[pageIndex], Meta(hasNext: hasNext, hasPrev: hasPrev, page: page)) + return (slices[pageIndex], Meta(page: page, hasNext: hasNext, hasPrevious: hasPrevious)) } enum Error: Swift.Error { diff --git a/Stepic/Sources/Services/Models/Network/WishListsNetworkService.swift b/Stepic/Sources/Services/Models/Network/WishListsNetworkService.swift index 115499165a..49fed6de5e 100644 --- a/Stepic/Sources/Services/Models/Network/WishListsNetworkService.swift +++ b/Stepic/Sources/Services/Models/Network/WishListsNetworkService.swift @@ -1,13 +1,14 @@ import Foundation import PromiseKit +import StepikModel protocol WishListsNetworkServiceProtocol: AnyObject { - func fetchWishlistEntry(courseID: Course.IdType) -> Promise - func fetchWishlistEntries() -> Promise<[WishlistEntryPlainObject]> + func fetchWishlistEntry(courseID: Course.IdType) -> Promise + func fetchWishlistEntries() -> Promise<[WishlistEntry]> - func createWishlistEntry(courseID: Course.IdType) -> Promise + func createWishlistEntry(courseID: Course.IdType) -> Promise - func deleteWishlistEntry(wishlistEntryID: WishlistEntryPlainObject.IdType) -> Promise + func deleteWishlistEntry(wishlistEntryID: Int) -> Promise } final class WishListsNetworkService: WishListsNetworkServiceProtocol { @@ -17,19 +18,19 @@ final class WishListsNetworkService: WishListsNetworkServiceProtocol { self.wishListsAPI = wishListsAPI } - func fetchWishlistEntry(courseID: Course.IdType) -> Promise { + func fetchWishlistEntry(courseID: Course.IdType) -> Promise { self.wishListsAPI.retrieveWishlistEntry(courseID: courseID) } - func fetchWishlistEntries() -> Promise<[WishlistEntryPlainObject]> { + func fetchWishlistEntries() -> Promise<[WishlistEntry]> { self.wishListsAPI.retrieveAllWishlistPages() } - func createWishlistEntry(courseID: Course.IdType) -> Promise { + func createWishlistEntry(courseID: Course.IdType) -> Promise { self.wishListsAPI.createWishlistEntry(courseID: courseID) } - func deleteWishlistEntry(wishlistEntryID: WishlistEntryPlainObject.IdType) -> Promise { + func deleteWishlistEntry(wishlistEntryID: Int) -> Promise { self.wishListsAPI.deleteWishlistEntry(wishlistEntryID: wishlistEntryID) } } diff --git a/Stepic/Sources/Services/Models/Persistence/WishlistEntriesPersistenceService.swift b/Stepic/Sources/Services/Models/Persistence/WishlistEntriesPersistenceService.swift index 100b3d19db..12075f2ad8 100644 --- a/Stepic/Sources/Services/Models/Persistence/WishlistEntriesPersistenceService.swift +++ b/Stepic/Sources/Services/Models/Persistence/WishlistEntriesPersistenceService.swift @@ -1,12 +1,13 @@ import Foundation import PromiseKit +import StepikModel protocol WishlistEntriesPersistenceServiceProtocol: AnyObject { func fetchAll() -> Guarantee<[WishlistEntryEntity]> func fetch(courseID: Course.IdType) -> Guarantee - func save(wishlistEntries: [WishlistEntryPlainObject]) -> Guarantee - func saveNewWishlistEntries(_ wishlistEntries: [WishlistEntryPlainObject]) -> Guarantee + func save(wishlistEntries: [WishlistEntry]) -> Guarantee + func saveNewWishlistEntries(_ wishlistEntries: [WishlistEntry]) -> Guarantee func deleteAll() -> Promise func deleteWishlistEntry(courseID: Course.IdType) -> Guarantee @@ -18,7 +19,7 @@ final class WishlistEntriesPersistenceService: BasePersistenceService Guarantee { + func save(wishlistEntries: [WishlistEntry]) -> Guarantee { if wishlistEntries.isEmpty { return .value(()) } @@ -47,7 +48,7 @@ final class WishlistEntriesPersistenceService: BasePersistenceService Guarantee { + func saveNewWishlistEntries(_ wishlistEntries: [WishlistEntry]) -> Guarantee { Guarantee { seal in firstly { () -> Guarantee in Guarantee(self.deleteAll(), fallback: nil) diff --git a/Stepic/Sources/Services/Models/Repository/WishlistRepository.swift b/Stepic/Sources/Services/Models/Repository/WishlistRepository.swift index 1f9a2e0f63..46aaf3b75e 100644 --- a/Stepic/Sources/Services/Models/Repository/WishlistRepository.swift +++ b/Stepic/Sources/Services/Models/Repository/WishlistRepository.swift @@ -1,8 +1,9 @@ import Foundation import PromiseKit +import StepikModel protocol WishlistRepositoryProtocol: AnyObject { - func fetchWishlistEntries(sourceType: DataSourceType) -> Promise<[WishlistEntryPlainObject]> + func fetchWishlistEntries(sourceType: DataSourceType) -> Promise<[WishlistEntry]> func addCourseToWishlist(courseID: Course.IdType) -> Promise func deleteCourseFromWishlist(courseID: Course.IdType, sourceType: DataSourceType) -> Promise @@ -30,7 +31,7 @@ final class WishlistRepository: WishlistRepositoryProtocol { self.dataBackUpdateService = dataBackUpdateService } - func fetchWishlistEntries(sourceType: DataSourceType) -> Promise<[WishlistEntryPlainObject]> { + func fetchWishlistEntries(sourceType: DataSourceType) -> Promise<[WishlistEntry]> { switch sourceType { case .cache: return self.wishlistEntriesPersistenceService @@ -65,7 +66,7 @@ final class WishlistRepository: WishlistRepositoryProtocol { if sourceType == .remote { return self.wishlistEntriesPersistenceService .fetch(courseID: courseID) - .then { cachedWishlistEntry -> Promise in + .then { cachedWishlistEntry -> Promise in if let cachedWishlistEntry = cachedWishlistEntry { return .value(cachedWishlistEntry.plainObject) } diff --git a/StepikModel/.gitignore b/StepikModel/.gitignore new file mode 100644 index 0000000000..bb460e7be9 --- /dev/null +++ b/StepikModel/.gitignore @@ -0,0 +1,7 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ +DerivedData/ +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata diff --git a/StepikModel/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/StepikModel/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000000..18d981003d --- /dev/null +++ b/StepikModel/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/StepikModel/CocoaPodsModule.swift b/StepikModel/CocoaPodsModule.swift new file mode 100644 index 0000000000..00330e7450 --- /dev/null +++ b/StepikModel/CocoaPodsModule.swift @@ -0,0 +1 @@ +// This is a dummy file to get Pods to generate the appropriate folder structure \ No newline at end of file diff --git a/StepikModel/Package.swift b/StepikModel/Package.swift new file mode 100644 index 0000000000..77384ec59b --- /dev/null +++ b/StepikModel/Package.swift @@ -0,0 +1,30 @@ +// swift-tools-version:5.5 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "StepikModel", + platforms: [ + .macOS(.v10_10), + .iOS(.v9), + .tvOS(.v9), + .watchOS(.v2) + ], + products: [ + .library( + name: "StepikModel", + targets: ["StepikModel"] + ) + ], + targets: [ + .target( + name: "StepikModel", + path: "Sources" + ), + .testTarget( + name: "StepikModelTests", + dependencies: ["StepikModel"] + ) + ] +) diff --git a/StepikModel/README.md b/StepikModel/README.md new file mode 100644 index 0000000000..52c22bb027 --- /dev/null +++ b/StepikModel/README.md @@ -0,0 +1,3 @@ +# StepikModel + +A description of this package. diff --git a/StepikModel/Sources/BundleResolver.swift b/StepikModel/Sources/BundleResolver.swift new file mode 100644 index 0000000000..cd9623924c --- /dev/null +++ b/StepikModel/Sources/BundleResolver.swift @@ -0,0 +1,9 @@ +import Foundation + +private class BundleResolver {} + +extension Bundle { + static var module: Bundle { + Bundle(for: BundleResolver.self) + } +} diff --git a/StepikModel/Sources/Extensions/DateExtensions.swift b/StepikModel/Sources/Extensions/DateExtensions.swift new file mode 100644 index 0000000000..e2e71c6da2 --- /dev/null +++ b/StepikModel/Sources/Extensions/DateExtensions.swift @@ -0,0 +1,40 @@ +import Foundation + +extension DateFormatter { + private static let stepikISO8601Short: DateFormatter = { + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'" + formatter.timeZone = TimeZone(abbreviation: "UTC") ?? TimeZone(secondsFromGMT: 0) + formatter.locale = Locale(identifier: "en_US_POSIX") + return formatter + }() + + private static let stepikISO8601Medium: DateFormatter = { + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" + formatter.timeZone = TimeZone(abbreviation: "UTC") ?? TimeZone(secondsFromGMT: 0) + formatter.locale = Locale(identifier: "en_US_POSIX") + return formatter + }() + + private static let stepikISO8601Full: DateFormatter = { + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd'T'hh:mm:ss.SSS a'Z'" + formatter.timeZone = TimeZone(abbreviation: "UTC") ?? TimeZone(secondsFromGMT: 0) + formatter.locale = Locale(identifier: "en_US_POSIX") + return formatter + }() + + static var allStepikISO8601: [DateFormatter] { + [self.stepikISO8601Short, self.stepikISO8601Medium, self.stepikISO8601Full] + } + + static func parsedStepikISO8601Date(from dateString: String) -> Date? { + for dateFormatter in self.allStepikISO8601 { + if let date = dateFormatter.date(from: dateString) { + return date + } + } + return nil + } +} diff --git a/StepikModel/Sources/Extensions/KeyedDecodingContainerExtensions.swift b/StepikModel/Sources/Extensions/KeyedDecodingContainerExtensions.swift new file mode 100644 index 0000000000..531d009b1b --- /dev/null +++ b/StepikModel/Sources/Extensions/KeyedDecodingContainerExtensions.swift @@ -0,0 +1,22 @@ +import Foundation + +extension KeyedDecodingContainer { + func decode(forKey key: Key) throws -> T { + try self.decode(T.self, forKey: key) + } + + func decode( + forKey key: Key, + default defaultExpression: @autoclosure () -> T + ) throws -> T { + try self.decodeIfPresent(T.self, forKey: key) ?? defaultExpression() + } + + func decodeStepikDate(key: K) throws -> Date? { + guard let dateString = try self.decodeIfPresent(String.self, forKey: key) else { + return nil + } + + return DateFormatter.parsedStepikISO8601Date(from: dateString) + } +} diff --git a/StepikModel/Sources/StepikModel/WishlistEntry.swift b/StepikModel/Sources/StepikModel/WishlistEntry.swift new file mode 100644 index 0000000000..9722c61048 --- /dev/null +++ b/StepikModel/Sources/StepikModel/WishlistEntry.swift @@ -0,0 +1,36 @@ +import Foundation + +public struct WishlistEntry { + public let id: Int + public let courseID: Int + public let userID: Int + public let createDate: Date? + public let platform: String + + public init(id: Int, courseID: Int, userID: Int, createDate: Date?, platform: String) { + self.id = id + self.courseID = courseID + self.userID = userID + self.createDate = createDate + self.platform = platform + } +} + +extension WishlistEntry: Decodable { + enum CodingKeys: String, CodingKey { + case id + case course + case user + case createDate = "create_date" + case platform + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.id = try container.decode(forKey: .id) + self.courseID = try container.decode(forKey: .course) + self.userID = try container.decode(forKey: .user) + self.createDate = try container.decodeStepikDate(key: .createDate) + self.platform = try container.decode(forKey: .platform) + } +} diff --git a/StepikModel/StepikModel.podspec b/StepikModel/StepikModel.podspec new file mode 100644 index 0000000000..64ba8b3fd8 --- /dev/null +++ b/StepikModel/StepikModel.podspec @@ -0,0 +1,23 @@ +Pod::Spec.new do |spec| + +spec.name = "StepikModel" +spec.version = "0.0.1" +spec.summary = "Stepik model" +spec.description = "A framework for Stepik model." +spec.homepage = "https://github.com/StepicOrg/stepik-ios" +spec.license = "MIT" +spec.author = { "Ivan Magda" => "ivan.magda@stepik.org" } + +spec.ios.deployment_target = "9.0" +spec.osx.deployment_target = "10.10" +spec.watchos.deployment_target = "2.0" +spec.tvos.deployment_target = "9.0" + +spec.source = { :git => "https://github.com/StepicOrg/stepik-ios.git" } + +spec.source_files = [ + "CocoaPodsModule.swift", + "Sources/**/*.swift" +] +spec.exclude_files = "Package.swift" +end diff --git a/StepikModel/Tests/StepikModelTests/DateExtensionsTests.swift b/StepikModel/Tests/StepikModelTests/DateExtensionsTests.swift new file mode 100644 index 0000000000..082ccc274d --- /dev/null +++ b/StepikModel/Tests/StepikModelTests/DateExtensionsTests.swift @@ -0,0 +1,20 @@ +@testable +import StepikModel + +import XCTest + +final class DateExtensionsTests: XCTestCase { + func testParsesDateStrings() throws { + // Given + let dateStrings = [ + "2021-05-04T10:21:43.571Z", + "2021-06-07T16:13:25.147Z" + ] + + // When + let dates = dateStrings.compactMap(DateFormatter.parsedStepikISO8601Date(from:)) + + // Then + XCTAssert(!dates.isEmpty, "Expected not empty array of dates") + } +} diff --git a/StepikModel/Tests/StepikModelTests/WishlistEntryTests.swift b/StepikModel/Tests/StepikModelTests/WishlistEntryTests.swift new file mode 100644 index 0000000000..1594c38311 --- /dev/null +++ b/StepikModel/Tests/StepikModelTests/WishlistEntryTests.swift @@ -0,0 +1,31 @@ +@testable +import StepikModel + +import XCTest + +final class WishlistEntryTests: XCTestCase { + func testDeserializeJSON() throws { + // Given + let jsonString = """ + { + "id": 626257, + "user": 21612976, + "course": 5482, + "create_date": "2022-02-03T07:59:56.095Z", + "platform": "web" + } + """ + let jsonData = jsonString.data(using: .utf8)! + + // When + let decoder = JSONDecoder() + let wishlistEntry = try decoder.decode(WishlistEntry.self, from: jsonData) + + // Then + XCTAssertEqual(wishlistEntry.id, 626257) + XCTAssertEqual(wishlistEntry.userID, 21612976) + XCTAssertEqual(wishlistEntry.courseID, 5482) + XCTAssertNotNil(wishlistEntry.createDate) + XCTAssertEqual(wishlistEntry.platform, "web") + } +} From 70a7525b9ec86b4187266ede6af7cee52358ecbf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 15 Feb 2022 11:02:18 +0300 Subject: [PATCH 02/13] bundler: bump fastlane from 2.204.2 to 2.204.3 (#1118) * bundler: bump fastlane from 2.204.2 to 2.204.3 Bumps [fastlane](https://github.com/fastlane/fastlane) from 2.204.2 to 2.204.3. - [Release notes](https://github.com/fastlane/fastlane/releases) - [Commits](https://github.com/fastlane/fastlane/compare/fastlane/2.204.2...fastlane/2.204.3) --- updated-dependencies: - dependency-name: fastlane dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * Run bundle update Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Ivan Magda --- Gemfile | 2 +- Gemfile.lock | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Gemfile b/Gemfile index 4aa1aea1e0..03f916378d 100644 --- a/Gemfile +++ b/Gemfile @@ -1,7 +1,7 @@ source "https://rubygems.org" ruby "2.6.5" -gem "fastlane", "2.204.2" +gem "fastlane", "2.204.3" gem "cocoapods", "1.11.2" gem "generamba", "1.5.0" diff --git a/Gemfile.lock b/Gemfile.lock index 7b221a4af2..d781f066a1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -3,7 +3,7 @@ GEM specs: CFPropertyList (3.0.5) rexml - activesupport (6.1.4.4) + activesupport (6.1.4.6) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -17,8 +17,8 @@ GEM artifactory (3.0.15) atomos (0.1.3) aws-eventstream (1.2.0) - aws-partitions (1.552.0) - aws-sdk-core (3.126.0) + aws-partitions (1.554.0) + aws-sdk-core (3.126.1) aws-eventstream (~> 1, >= 1.0.2) aws-partitions (~> 1, >= 1.525.0) aws-sigv4 (~> 1.1) @@ -116,7 +116,7 @@ GEM faraday_middleware (1.2.0) faraday (~> 1.0) fastimage (2.2.6) - fastlane (2.204.2) + fastlane (2.204.3) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) @@ -191,7 +191,7 @@ GEM google-cloud-env (1.5.0) faraday (>= 0.17.3, < 2.0) google-cloud-errors (1.2.0) - google-cloud-storage (1.36.0) + google-cloud-storage (1.36.1) addressable (~> 2.8) digest-crc (~> 0.4) google-apis-iamcredentials_v1 (~> 0.1) @@ -199,7 +199,7 @@ GEM google-cloud-core (~> 1.6) googleauth (>= 0.16.2, < 2.a) mini_mime (~> 1.0) - googleauth (1.1.0) + googleauth (1.1.1) faraday (>= 0.17.3, < 2.0) jwt (>= 1.4, < 3.0) memoist (~> 0.16) @@ -210,9 +210,9 @@ GEM http-cookie (1.0.4) domain_name (~> 0.5) httpclient (2.8.3) - i18n (1.9.1) + i18n (1.10.0) concurrent-ruby (~> 1.0) - jmespath (1.5.0) + jmespath (1.6.0) json (2.6.1) jwt (2.3.0) liquid (4.0.0) @@ -287,7 +287,7 @@ PLATFORMS DEPENDENCIES cocoapods (= 1.11.2) - fastlane (= 2.204.2) + fastlane (= 2.204.3) fastlane-plugin-firebase_app_distribution generamba (= 1.5.0) From 24bde729da2959e930555e4586d241cafaee977a Mon Sep 17 00:00:00 2001 From: Ivan Magda Date: Tue, 15 Feb 2022 13:09:03 +0300 Subject: [PATCH 03/13] New home release v1 (#1119) * Add gradient background image * Add new placeholders * Update dark mode gradient * Update enrolled course list * Update popular courses course list * Update ContinueCourseView main insets * Revert changes in ExploreStoriesContainerView --- Stepic.xcodeproj/project.pbxproj | 23 +++- .../Contents.json | 25 ++++ .../new_courses_gradient.pdf | Bin 0 -> 6211 bytes .../new_courses_gradient_dark.pdf | Bin 0 -> 4067 bytes .../Contents.json | 15 ++ ...new_courses_placeholder_gradient_large.pdf | Bin 0 -> 6211 bytes .../Contents.json | 15 ++ ...new_courses_placeholder_gradient_small.pdf | Bin 0 -> 6203 bytes .../ContinueCourseSkeletonView.swift | 7 +- .../Views/CourseListColorMode.swift | 23 ++-- .../CourseList/Views/CourseListView.swift | 6 +- .../View/CourseListContainerViewFactory.swift | 13 ++ .../ExploreBlockContainerView.swift | 58 +++++++- .../ExploreBlockPlaceholderView.swift | 0 .../ExplorePlaceholderActionButton.swift | 117 ++++++++++++++++ .../ExplorePlaceholderView.swift | 119 ++++++++++++++++ .../NewExploreBlockPlaceholderView.swift | 130 ++++++++++++++++++ .../Modules/Home/HomeViewController.swift | 42 ++++-- .../ContinueLastStepView.swift | 2 +- Stepic/en.lproj/Localizable.strings | 7 + Stepic/ru.lproj/Localizable.strings | 7 + 21 files changed, 575 insertions(+), 34 deletions(-) create mode 100644 Stepic/Images.xcassets/Course_list_placeholder/new_courses_gradient.imageset/Contents.json create mode 100644 Stepic/Images.xcassets/Course_list_placeholder/new_courses_gradient.imageset/new_courses_gradient.pdf create mode 100644 Stepic/Images.xcassets/Course_list_placeholder/new_courses_gradient.imageset/new_courses_gradient_dark.pdf create mode 100644 Stepic/Images.xcassets/Course_list_placeholder/new_courses_placeholder_gradient_large.imageset/Contents.json create mode 100644 Stepic/Images.xcassets/Course_list_placeholder/new_courses_placeholder_gradient_large.imageset/new_courses_placeholder_gradient_large.pdf create mode 100644 Stepic/Images.xcassets/Course_list_placeholder/new_courses_placeholder_gradient_small.imageset/Contents.json create mode 100644 Stepic/Images.xcassets/Course_list_placeholder/new_courses_placeholder_gradient_small.imageset/new_courses_placeholder_gradient_small.pdf rename Stepic/Sources/Modules/Explore/View/{ => ExploreBlockPlaceholderView}/ExploreBlockPlaceholderView.swift (100%) create mode 100644 Stepic/Sources/Modules/Explore/View/ExploreBlockPlaceholderView/ExplorePlaceholderActionButton.swift create mode 100644 Stepic/Sources/Modules/Explore/View/ExploreBlockPlaceholderView/ExplorePlaceholderView.swift create mode 100644 Stepic/Sources/Modules/Explore/View/ExploreBlockPlaceholderView/NewExploreBlockPlaceholderView.swift diff --git a/Stepic.xcodeproj/project.pbxproj b/Stepic.xcodeproj/project.pbxproj index e5b47f44f2..2aaff15f76 100644 --- a/Stepic.xcodeproj/project.pbxproj +++ b/Stepic.xcodeproj/project.pbxproj @@ -705,6 +705,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 */; }; @@ -2737,6 +2740,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 = ""; }; @@ -5006,6 +5012,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 = ( @@ -9803,11 +9820,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 = ""; @@ -11266,6 +11283,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 */, @@ -11380,6 +11398,8 @@ 2C98423C26DE4CA80098E36B /* SearchResult+CoreDataProperties.swift in Sources */, 2C12E4722565668000DC52CB /* UIViewController+PresentPanModal.swift in Sources */, 2CBEAACE271EFA9A00B52D2C /* CourseInfoPurchaseModalDisclaimerView.swift in Sources */, + 2C12E4722565668000DC52CB /* UIViewController+PresentPanModal.swift in Sources */, + 2C6917C625EE47D700BAE0F5 /* NewExploreBlockPlaceholderView.swift in Sources */, 2CA867C12588FFF40006576E /* GridSimpleCourseListCollectionHeaderView.swift in Sources */, 2C0FE8C425F81A4900626289 /* InstructionPlainObject.swift in Sources */, 08CBA3011F57459800302154 /* MenuUIManager.swift in Sources */, @@ -11401,6 +11421,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 */, 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 0000000000000000000000000000000000000000..896ec0ae7aa91e14220f19f88cfc4155743b8247 GIT binary patch literal 6211 zcmeHLOK;ma5We$Q@DiXs#1g5O1rz}`Pb{`Uz4jgo6g?<1DfdoJ2dXuMh zR^-2ZSqtiG;$^W)vYdXP?#NX?%G$w4y=0o?3-RQNvoliFrASufEA0*ukLd%QZ`K`U zQPZzfk=MoA{#0%5I+h4UOKE1zfS)pythTrSeAaGdQnI@@k!wB2T|; zeZPPOm*S5s6*pyKe+K31Gd6d%G@El>lq*e}imKR@si>3$lccUinKxsm{@o^ZnLUE# zPSLohEC@z1594t}jp6PEKBF27-L0M&IkSgkp5+V0|Dq_$d1WF8Zg+!S`;5w|Y@5i*mM3QZV@w*f`)ASc0#fO8=l; z!C_nFsuqBIcQ_hgilrAb zVouYFY$LRLB-`JRak?TnHaM@yutZCfjvJTV8ACMV+$DJ+t?DUA?MxW0(wy9roDBFr z>AV`>$GMNbFF>jhIl>Muh#_U%za?_uwYF7>6x&)5UGR%?^1Eg`A`gZj99WBGEJERA zEJ8LIi(Y#DzF3EX;{at;lwGr(kx`nwL5Jssh>i%U20F(7K6JRFD2iDaH3@?|8oN;( zv1Vqg#4@0rN1#e^U@eyD05`y0UovV^70q=gJVHOch;>zr>Qu)6qaVb2XB1tnmobYV%;SxM=_Ly>S#?Ug^MN+^j#10KxX$JwSASteb1=O5~p3=j;W3Mt~0zefy#@?MAgM2 zAHdOe=vdddGYS!=vbpW59vp*F)XN!E$7mV{M7k`%SVQ;gT7iC71+lJlmfU~J^P+~< zmhLuTBV!?&DCFFVd6pdCod6$dB*RLl+C4!_D2HYjFjb0ZsT0{$tf7~uAFHV_yRQEw zQ@uEKk!c&c;M9%WuBC3?<|J8&3Ml4TU2Q0@->kbx>SS3g^j@`VP>{z`+!L3wh^V?o z{h<&{y?_E;|0JyBkLE4ObAf4(#GweWNGuowE^Jchg0U0^(C6-2eap literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..4d9ee3797ac968620adcdd0cc42438e07a9a5c31 GIT binary patch literal 4067 zcmc&%O>f&c5WVwP@DiXs#QL@*pa`&W5@XQ@b?rSYu-F45Gmg5p=#{7c&yRPXc_wv{=z5!4 zh|Ws^;qMgGyxFb_vZ=}y*eh94?ODl-?Ca6`3^rWxFL}o2CF?IkS!o;foy4McZbZ4$ zvL&xXU1q#e9xRxYyeyirr+#jkEb~XG>vP|YCTWrco=<%@nRsdF#xaWRvDp%N(@B)L zSiyliodgh!IHj5_x{EzAa#jy)ofjMB!c2&AUD?!f%pXQF_5}N2=lW4X1;7citaynxC)I(>XJRYL)t+S#^(WF}gc;ako&|~&q2G#es<^Ao!IK(} zPQe#lS(lnt9fQ-w1-N9qaxO0grnpHWGYGw~edj{YZ-;%#vVf(t6|tvTMGUDJo9AY9 zyk?fN9Wi6;*l%67_gWZVe4b)&GJPl!&lqyCB2f>-ARf7ptf!qHKRe!Sp0@FAc}tp zp1c9Z0OaB4n9|15U7H>@{TSA=sOmK-5b5sI5aA&tn(#u`py#v7{^w)z!p(budP@s+)CEm108;!tF}sA zW7+m*V6;Yc?Q@fr3%21ENNii(KZve*oiWL_Vxx7H)}SMgCBG*w^#al~=_+5RQ4~ZJ zx+W)@WcQf<-=W3 z3i$UGr_=64S+ab~OM=k-eg(b9n@a5XTlywnzilGnEQNqDAj>&MN=L5iwPmtdt7F NAode-c6R;o<|p4~Zn*#e literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..9fb81b865cdd827fbc94ff7c3a896d2e63bc8bb9 GIT binary patch literal 6211 zcmeHLOK;ma5We$Q@DiXs#1bh{vVkJN=845NsMp>@fuaXRW*l{G$(7_qx<&u{4k>Cx z+KL~|2KzvLNXQY%8P0s1*U9+u`jib~LKyXFarcQ(`sNMw-pYJlKTr~jb`_H6H4cvhh)t`uU2fn5t+}7cra4xAxb5U zo>(|k-!(Nrs?wfTk@x=H-6xsWMh!-zp{3EAvVbdg8}z(cu2tUXeG2E5NnWjzQs(KG zt?w7G;8OmPrShgs+|Qs~bH@6vR%Y|Ai*jjb6Im7OGL@BNwkF{1_x-L0M&dDDkvmgRHJ|Dq_$S!E*!3MiAj3WMS*Zcra{-3LBljvJ^`% zro^7671>5;_ei$CA>(vOaBOj2l75L6HXS!Ex-izd{k~X-g5v;X)RbMbosm(Qyg^40M2Lt*aB2=jQWV0y_yOchZ z$Omw=9Xi%E?yN$D>1=Mhst3nl74>Qc)iIjJ1CcHZFjn9Fx>jJ=RY9yPofY?=^1P^_ zwPm_Z*vOd6CJK4Ca+W2>cPGGyTFJ21sc}!x63U_31x&RfTIyIe6>G}c7;Y=ft?Pfu zbT3X{WYUH%ICZVI8>ySOIZo!X0*ZZBR~xG9H|s8vI$0ERvsdrx738s$_r#|xCc3WC ze`q8`ASlqyPr*w4Xx@?nh_IU^4n>HwiisiM!X-sM7|S7X#`Q0Z%>6a=^*{v>r$BG+U-|wvbQmxVRu?RmgZnzEXRMd`uqbe6zNc zMMb|-O{mEdNLoc~ivhGAP&FvAL_1*}Ut#SQ*+>mieYgWT_>X#8oAWtR8dy?>4TA^bss~ zj>bJ7^n1gQg@cgV(Z1zjpBfz47d)s`KAdTuNNbOBHEt8DglZ|G+zbAx`elI|3D{6!mE{Gv*+`lDi;InpBi4@mb5Ls|VJNSLGJy8e44-TBoDh{D= zDh?qVj6*NI|4^Jm!Et~xYRbOZ-l(WdUZbPm4-p*^Qd{U4{QJ*H}HrM9z&!mczlOLhS1`d}3S(AC^O^O%nMQRAY{0e#lT9FV(xM`K^@aNl#PGQ_E^*)grr>n6KPVyL>fN>UjX z_yCTkGskViu2qC^oyqN1^xzm;1-+UXZ2|hW0^+u8R@{Hevb=(} zmgzKM6JsIkDCFJBc^aSInE)T!N`$pejeCN2PzH@IV5$|-P$#OHSW(`@@V3I-n*Ntm z_u=$IrcLOATQ};sk-GjiC-Fj-KxyA)>q7PXdfzCn;$^-tXZ5L0K^_ZvPZ;G9(KU_! zL&G5G2NdY$r(mUiG;2tG228g|0tyg^#Di#u1QB{`m*8TT$baA^~yAkg6h1m$D$r3_p*d@%5_ASMoJ| omCj$+R`sTN4klF=(7P<*UwhiPsI)ymZv%36cJ=Q1FV~DB9smFU literal 0 HcmV?d00001 diff --git a/Stepic/Legacy/Views/Skeleton/Views/CourseList/ContinueCourseSkeletonView.swift b/Stepic/Legacy/Views/Skeleton/Views/CourseList/ContinueCourseSkeletonView.swift index a055464cdb..2b4666ae32 100644 --- a/Stepic/Legacy/Views/Skeleton/Views/CourseList/ContinueCourseSkeletonView.swift +++ b/Stepic/Legacy/Views/Skeleton/Views/CourseList/ContinueCourseSkeletonView.swift @@ -11,7 +11,7 @@ import UIKit extension ContinueCourseSkeletonView { struct Appearance { - let mainInsets = UIEdgeInsets(top: 20, left: 20, bottom: 0, right: 20) + let mainInsets = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20) let cornerRadius: CGFloat = 8.0 } } @@ -50,10 +50,7 @@ extension ContinueCourseSkeletonView: ProgrammaticallyInitializableViewProtocol 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) + make.edges.equalToSuperview().inset(self.appearance.mainInsets) } } } diff --git a/Stepic/Sources/Modules/CourseList/Views/CourseListColorMode.swift b/Stepic/Sources/Modules/CourseList/Views/CourseListColorMode.swift index 91189596c8..112a8b453d 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, @@ -102,7 +107,7 @@ extension CourseListColorMode { ) switch self { - case .light, .grouped: + case .light, .grouped, .clearLight: return appearance case .dark: appearance.progressTextLabelAppearance.textColor = .dynamic( @@ -124,7 +129,7 @@ extension CourseListColorMode { ) switch self { - case .light, .grouped: + case .light, .grouped, .clearLight: appearance.textColor = .stepikSystemPrimaryText case .dark: appearance.textColor = .white @@ -140,7 +145,7 @@ extension CourseListColorMode { ) switch self { - case .light, .grouped: + case .light, .grouped, .clearLight: appearance.textColor = .stepikSystemSecondaryText case .dark: appearance.textColor = UIColor.dynamic( @@ -154,7 +159,7 @@ extension CourseListColorMode { var courseWidgetBadgeTintColor: UIColor { switch self { - case .light, .grouped: + case .light, .grouped, .clearLight: return .stepikSystemSecondaryText case .dark: return .dynamic( @@ -166,7 +171,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, *) { @@ -179,7 +184,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 5420e3584f..60ee4f24e5 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/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..375b3d1e58 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,12 +25,27 @@ 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 return view }() + private var backgroundImageViewBottomToSuperviewConstraint: Constraint? + private var backgroundImageViewBottomToContentViewConstraint: Constraint? + var onShowAllButtonClick: (() -> Void)? { didSet { self.headerView.onShowAllButtonClick = self.onShowAllButtonClick @@ -74,28 +96,54 @@ final class ExploreBlockContainerView: UIView { super.traitCollectionDidChange(previousTraitCollection) self.performBlockIfAppearanceChanged(from: previousTraitCollection) { - self.updateViewColor() + self.updateBackgroundImageViewBottomConstraint() } } - private func updateViewColor() { - self.backgroundColor = self.appearance.backgroundColor + private func updateBackgroundImageViewBottomConstraint() { + if self.isDarkInterfaceStyle { + self.backgroundImageViewBottomToSuperviewConstraint?.deactivate() + self.backgroundImageViewBottomToContentViewConstraint?.activate() + } else { + self.backgroundImageViewBottomToSuperviewConstraint?.activate() + self.backgroundImageViewBottomToContentViewConstraint?.deactivate() + } } } 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() + + self.backgroundImageViewBottomToSuperviewConstraint = make + .bottom.equalToSuperview().priority(.low).constraint + self.backgroundImageViewBottomToContentViewConstraint = make + .bottom.equalTo(self.contentView.snp.bottom).constraint + self.updateBackgroundImageViewBottomConstraint() + } + } + 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..7edb1f0e71 --- /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 "" + } + } + + 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/Home/HomeViewController.swift b/Stepic/Sources/Modules/Home/HomeViewController.swift index 9c48350163..f30cfc68c9 100644 --- a/Stepic/Sources/Modules/Home/HomeViewController.swift +++ b/Stepic/Sources/Modules/Home/HomeViewController.swift @@ -185,6 +185,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 +204,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 +222,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 +250,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 - self?.displayAuthorization(viewModel: .init()) + placeholderView.onActionButtonClick = { [weak self] in + self?.displayCatalog() } 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() + } default: break } @@ -262,6 +277,7 @@ final class HomeViewController: BaseExploreViewController { let containerView = CourseListContainerViewFactory(colorMode: .light) .makeHorizontalContainerView( for: view, + containerDescription: state.containerDescription, headerDescription: state.headerDescription, contentViewInsets: contentViewInsets ) @@ -402,7 +418,7 @@ final class HomeViewController: BaseExploreViewController { var headerDescription: CourseListContainerViewFactory.HorizontalHeaderDescription { CourseListContainerViewFactory.HorizontalHeaderDescription( - title: NSLocalizedString("Popular", comment: ""), + title: NSLocalizedString("Recommended", comment: ""), summary: nil, shouldShowAllButton: self == .normal ) @@ -424,7 +440,7 @@ final class HomeViewController: BaseExploreViewController { let courseListType = PopularCourseListType(language: contentLanguage) let popularAssembly = HorizontalCourseListAssembly( type: courseListType, - colorMode: .dark, + colorMode: .light, courseViewSource: .query(courseListType: courseListType), output: self.interactor as? CourseListOutputProtocol ) @@ -463,7 +479,7 @@ final class HomeViewController: BaseExploreViewController { (view, viewController) = (placeholderView, nil) } - let containerView = CourseListContainerViewFactory(colorMode: .dark) + let containerView = CourseListContainerViewFactory(colorMode: .light) .makeHorizontalContainerView( for: view, headerDescription: state.headerDescription @@ -547,6 +563,10 @@ extension HomeViewController: HomeViewControllerProtocol { strongSelf.refreshStateForPopularCourses(state: .normal) } } + + func displayCatalog() { + DeepLinkRouter.routeToCatalog() + } } extension HomeViewController: BaseExploreViewDelegate { diff --git a/Stepic/Sources/Views/ContinueLastStepView/ContinueLastStepView.swift b/Stepic/Sources/Views/ContinueLastStepView/ContinueLastStepView.swift index 26b6990e76..a3ab6e4a8b 100644 --- a/Stepic/Sources/Views/ContinueLastStepView/ContinueLastStepView.swift +++ b/Stepic/Sources/Views/ContinueLastStepView/ContinueLastStepView.swift @@ -5,7 +5,7 @@ import UIKit extension ContinueLastStepView { struct Appearance { - let mainInsets = UIEdgeInsets(top: 20, left: 20, bottom: 0, right: 20) + let mainInsets = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20) let contentInsets = UIEdgeInsets(top: 35, left: 19, bottom: 17, right: 19) let cornerRadius: CGFloat = 8.0 diff --git a/Stepic/en.lproj/Localizable.strings b/Stepic/en.lproj/Localizable.strings index 67fda6f438..d168b2ba03 100644 --- a/Stepic/en.lproj/Localizable.strings +++ b/Stepic/en.lproj/Localizable.strings @@ -311,8 +311,15 @@ ShowAllTestText = "See All"; ShowAllTestTextDetailIndicator = "See All ›"; Enrolled = "Enrolled"; Popular = "Popular"; +Recommended = "Recommended"; VisitedCourses = "Visited courses"; Home = "Home"; +RecommendedCategory = "Recommended courses"; +NewHomePlaceholderAnonymousButtonTitle = "Find your first course"; +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"; diff --git a/Stepic/ru.lproj/Localizable.strings b/Stepic/ru.lproj/Localizable.strings index d30c86f3b1..9b939cadcc 100644 --- a/Stepic/ru.lproj/Localizable.strings +++ b/Stepic/ru.lproj/Localizable.strings @@ -312,8 +312,15 @@ ShowAllTestText = "Смотреть все"; ShowAllTestTextDetailIndicator = "Смотреть все ›"; Enrolled = "Мои курсы"; Popular = "Популярные"; +Recommended = "Рекомендуемые"; VisitedCourses = "Посещённые курсы"; Home = "Обучение"; +RecommendedCategory = "Подборка"; +NewHomePlaceholderAnonymousButtonTitle = "Найдите свой первый курс"; +NewHomePlaceholderErrorTitle = "Соединение потеряно"; +NewHomePlaceholderErrorButtonTitle = "Повторить"; +NewHomePlaceholderEmptyEnrolledTitle = "Здесь появятся ваши Курсы"; +NewHomePlaceholderEmptyEnrolledButtonTitle = "Найти курсы в Каталоге"; HomePlaceholderAnonymous = "Войдите и начните учиться прямо сейчас"; HomePlaceholderEmptyEnrolled = "Запишитесь на курсы и они будут показаны здесь"; HomePlaceholderEmptyPopular = "Открытые курсы сейчас в отпуске. Пожалуйста, зайдите позже"; From 1a37ec6a31c9e737fe923aeb067e5a780b026223 Mon Sep 17 00:00:00 2001 From: Ivan Magda Date: Tue, 15 Feb 2022 23:17:05 +0300 Subject: [PATCH 04/13] Authors promo banners (#1120) * Add model * Add view * Display in home * Add analytics * Open URL * Display in explore --- Stepic.xcodeproj/project.pbxproj | 12 + .../PromoBannersIllustrations/Contents.json | 6 + .../Contents.json | 15 ++ ...romo-banner-illustration-bicycle-green.pdf | Bin 0 -> 28555 bytes .../Contents.json | 15 ++ ...omo-banner-illustration-bicycle-violet.pdf | Bin 0 -> 28559 bytes .../Contents.json | 15 ++ .../promo-banner-illustration-work.pdf | Bin 0 -> 23023 bytes .../Analytics/Events/AnalyticsEvents.swift | 32 +++ .../Model/RemoteConfig/RemoteConfig.swift | 30 +-- Stepic/Sources/Model/PromoBanner.swift | 26 ++ .../BaseExplore/BaseExploreInteractor.swift | 3 + .../BaseExploreViewController.swift | 1 + .../Modules/Explore/ExploreDataFlow.swift | 3 +- .../Modules/Explore/ExploreInteractor.swift | 9 +- .../Modules/Explore/ExplorePresenter.swift | 4 +- .../Explore/ExploreViewController.swift | 70 +++++- .../Sources/Modules/Home/HomeDataFlow.swift | 2 + .../Sources/Modules/Home/HomeInteractor.swift | 7 +- .../Sources/Modules/Home/HomePresenter.swift | 3 +- .../Modules/Home/HomeViewController.swift | 70 ++++++ .../Services/PromoBannersService.swift | 41 ++++ Stepic/Sources/Views/PromoBannerView.swift | 228 ++++++++++++++++++ Stepic/Theme/ColorPalette.swift | 2 + 24 files changed, 564 insertions(+), 30 deletions(-) create mode 100644 Stepic/Images.xcassets/PromoBannersIllustrations/Contents.json create mode 100644 Stepic/Images.xcassets/PromoBannersIllustrations/promo-banner-illustration-bicycle-green.imageset/Contents.json create mode 100644 Stepic/Images.xcassets/PromoBannersIllustrations/promo-banner-illustration-bicycle-green.imageset/promo-banner-illustration-bicycle-green.pdf create mode 100644 Stepic/Images.xcassets/PromoBannersIllustrations/promo-banner-illustration-bicycle-violet.imageset/Contents.json create mode 100644 Stepic/Images.xcassets/PromoBannersIllustrations/promo-banner-illustration-bicycle-violet.imageset/promo-banner-illustration-bicycle-violet.pdf create mode 100644 Stepic/Images.xcassets/PromoBannersIllustrations/promo-banner-illustration-work.imageset/Contents.json create mode 100644 Stepic/Images.xcassets/PromoBannersIllustrations/promo-banner-illustration-work.imageset/promo-banner-illustration-work.pdf create mode 100644 Stepic/Sources/Model/PromoBanner.swift create mode 100644 Stepic/Sources/Services/PromoBannersService.swift create mode 100644 Stepic/Sources/Views/PromoBannerView.swift diff --git a/Stepic.xcodeproj/project.pbxproj b/Stepic.xcodeproj/project.pbxproj index 2aaff15f76..d66ede56c4 100644 --- a/Stepic.xcodeproj/project.pbxproj +++ b/Stepic.xcodeproj/project.pbxproj @@ -558,6 +558,9 @@ 2C313E5526AFE636004ECBD2 /* StepQuizReviewStatusCircleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C313E5426AFE636004ECBD2 /* StepQuizReviewStatusCircleView.swift */; }; 2C32E71320FF6C1D008BB909 /* Auth.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 2CC351841F6827BE004255B6 /* Auth.storyboard */; }; 2C381BEF25505EC90084AD90 /* CourseListFilterBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C381BEE25505EC90084AD90 /* CourseListFilterBarButtonItem.swift */; }; + 2C38D49E27BBCAC6002865B7 /* PromoBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C38D49D27BBCAC6002865B7 /* PromoBanner.swift */; }; + 2C38D4A027BBD074002865B7 /* PromoBannersService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C38D49F27BBD074002865B7 /* PromoBannersService.swift */; }; + 2C38D4A227BBD69E002865B7 /* PromoBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C38D4A127BBD69E002865B7 /* PromoBannerView.swift */; }; 2C3A035624AE3DCB007D28F7 /* NewProfileStreakNotificationsSwitchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C3A035524AE3DCB007D28F7 /* NewProfileStreakNotificationsSwitchView.swift */; }; 2C3A035924AE3E1C007D28F7 /* NewProfileStreakNotificationsTimeSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C3A035824AE3E1C007D28F7 /* NewProfileStreakNotificationsTimeSelectionView.swift */; }; 2C3A052025B763370007FBCA /* WidgetURL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C3A051F25B763370007FBCA /* WidgetURL.swift */; }; @@ -2575,6 +2578,9 @@ 2C35C4D41F4DA471002F3BF4 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = en; path = LeaderboardNames/en.lproj/nouns_m.plist; sourceTree = ""; }; 2C36E4772501F28E00D63C41 /* Model_user_remove_level_v65.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model_user_remove_level_v65.xcdatamodel; sourceTree = ""; }; 2C381BEE25505EC90084AD90 /* CourseListFilterBarButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseListFilterBarButtonItem.swift; sourceTree = ""; }; + 2C38D49D27BBCAC6002865B7 /* PromoBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PromoBanner.swift; sourceTree = ""; }; + 2C38D49F27BBD074002865B7 /* PromoBannersService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PromoBannersService.swift; sourceTree = ""; }; + 2C38D4A127BBD69E002865B7 /* PromoBannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PromoBannerView.swift; sourceTree = ""; }; 2C3A035524AE3DCB007D28F7 /* NewProfileStreakNotificationsSwitchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewProfileStreakNotificationsSwitchView.swift; sourceTree = ""; }; 2C3A035824AE3E1C007D28F7 /* NewProfileStreakNotificationsTimeSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewProfileStreakNotificationsTimeSelectionView.swift; sourceTree = ""; }; 2C3A051F25B763370007FBCA /* WidgetURL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetURL.swift; sourceTree = ""; }; @@ -5397,6 +5403,7 @@ 2CD80A7D269C47670047AE3C /* PaginationState.swift */, 2CD1D9EB272AA8D80031C78E /* PaymentStore.swift */, 2CEDC660260898F700B0B018 /* PlatformType.swift */, + 2C38D49D27BBCAC6002865B7 /* PromoBanner.swift */, 2C5D340725C98A6800372C61 /* PromoCode.swift */, 2C20778322BBA54800D44DC0 /* QuizStatus.swift */, 2C619A7E24D0004B007D3529 /* SocialProfileProvider.swift */, @@ -9061,6 +9068,7 @@ 62E98CA36176671DDF744499 /* PaddingLabel.swift */, 2C7F641B23B0F5B7006C7648 /* PlayNextCircleControlView.swift */, 62E984651165CED551545B5C /* ProgressCircleImage.swift */, + 2C38D4A127BBD69E002865B7 /* PromoBannerView.swift */, 2CB2740322C515050078CA2F /* QuizElementView.swift */, 62E988152D37F6F6884F2977 /* ScrollableStackView.swift */, 62E98315DDA05307C326E27C /* SeparatorView.swift */, @@ -9647,6 +9655,7 @@ 62E98E01F05F1205F284595F /* NetworkReachabilityService.swift */, 2CDAD309229EC81A00AA9EF5 /* PersistenceQueuesService.swift */, 2C911B9925C037EB0076DC31 /* PersonalOffersService.swift */, + 2C38D49F27BBD074002865B7 /* PromoBannersService.swift */, 2CDBCCCE23EB777E005D2370 /* SubmissionURLProvider.swift */, 62E98FBA6AB1C2BD6EA95634 /* UnitNavigationService.swift */, 62E98EBA0AF48AD90775FF7E /* UserAccountService.swift */, @@ -11531,6 +11540,7 @@ 2CB62BDB2019ECB800B5E336 /* OnboardingCardStepViewController.swift in Sources */, 08E7CA1020DAF6E0004F8563 /* AnalyticsUserProperties.swift in Sources */, 2C7F41672694938700BD5C48 /* CourseInfoTabReviewsSummaryDistributionCountItemView.swift in Sources */, + 2C38D4A227BBD69E002865B7 /* PromoBannerView.swift in Sources */, 2C1AC31E255B476A00E6ECA9 /* CatalogBlocksRepository.swift in Sources */, 2C2972052147F5FD001502BD /* CourseTag.swift in Sources */, 08EF9A081C91D0F800433E4A /* StepikVideoPlayerViewController.swift in Sources */, @@ -12310,6 +12320,7 @@ 62E9804624D413C3F8D71AC7 /* CodeDetailsContentView.swift in Sources */, 2C87A7A82446646600933CA4 /* UserActivityEntity.swift in Sources */, 62E98E54D4E57B4371493032 /* CodeDetailsLimitItemView.swift in Sources */, + 2C38D4A027BBD074002865B7 /* PromoBannersService.swift in Sources */, 62E98C379F06E77CC850DE57 /* CodeDetailsSampleItemView.swift in Sources */, 62E988B311FB626588A8615D /* CodeDetailsView.swift in Sources */, 62E9819CB6D1AAFEECBF7732 /* CodeEditorView.swift in Sources */, @@ -12757,6 +12768,7 @@ 733EA9B5B70C811D7A144DF3 /* CourseRevenueTabMonthlyInputProtocol.swift in Sources */, EF3252195A0C7A3F15319D12 /* CourseRevenueTabMonthlyOutputProtocol.swift in Sources */, F8189D6BC4B0A57D076B052A /* StepQuizReviewAssembly.swift in Sources */, + 2C38D49E27BBCAC6002865B7 /* PromoBanner.swift in Sources */, EE315359D4CBD905081C5F10 /* StepQuizReviewDataFlow.swift in Sources */, 2C6277C3270C905200FDAFD9 /* SiriShortcutsStorageManager.swift in Sources */, B7A7B95C8D6C1A2A54487E5B /* StepQuizReviewInteractor.swift in Sources */, diff --git a/Stepic/Images.xcassets/PromoBannersIllustrations/Contents.json b/Stepic/Images.xcassets/PromoBannersIllustrations/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Stepic/Images.xcassets/PromoBannersIllustrations/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Stepic/Images.xcassets/PromoBannersIllustrations/promo-banner-illustration-bicycle-green.imageset/Contents.json b/Stepic/Images.xcassets/PromoBannersIllustrations/promo-banner-illustration-bicycle-green.imageset/Contents.json new file mode 100644 index 0000000000..98de10c6a2 --- /dev/null +++ b/Stepic/Images.xcassets/PromoBannersIllustrations/promo-banner-illustration-bicycle-green.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "promo-banner-illustration-bicycle-green.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Stepic/Images.xcassets/PromoBannersIllustrations/promo-banner-illustration-bicycle-green.imageset/promo-banner-illustration-bicycle-green.pdf b/Stepic/Images.xcassets/PromoBannersIllustrations/promo-banner-illustration-bicycle-green.imageset/promo-banner-illustration-bicycle-green.pdf new file mode 100644 index 0000000000000000000000000000000000000000..3572447a52d68c6e7225b829fa94afae9ccef957 GIT binary patch literal 28555 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 literal 0 HcmV?d00001 diff --git a/Stepic/Images.xcassets/PromoBannersIllustrations/promo-banner-illustration-bicycle-violet.imageset/Contents.json b/Stepic/Images.xcassets/PromoBannersIllustrations/promo-banner-illustration-bicycle-violet.imageset/Contents.json new file mode 100644 index 0000000000..03fda34088 --- /dev/null +++ b/Stepic/Images.xcassets/PromoBannersIllustrations/promo-banner-illustration-bicycle-violet.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "promo-banner-illustration-bicycle-violet.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Stepic/Images.xcassets/PromoBannersIllustrations/promo-banner-illustration-bicycle-violet.imageset/promo-banner-illustration-bicycle-violet.pdf b/Stepic/Images.xcassets/PromoBannersIllustrations/promo-banner-illustration-bicycle-violet.imageset/promo-banner-illustration-bicycle-violet.pdf new file mode 100644 index 0000000000000000000000000000000000000000..de4167d4461ccef05c808be2bc9aed5cb3310abf GIT binary patch literal 28559 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 X?jO$Y9)b)VZ>21|Pd@qT4`2T`9+LA% literal 0 HcmV?d00001 diff --git a/Stepic/Images.xcassets/PromoBannersIllustrations/promo-banner-illustration-work.imageset/Contents.json b/Stepic/Images.xcassets/PromoBannersIllustrations/promo-banner-illustration-work.imageset/Contents.json new file mode 100644 index 0000000000..39caa69c61 --- /dev/null +++ b/Stepic/Images.xcassets/PromoBannersIllustrations/promo-banner-illustration-work.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "promo-banner-illustration-work.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Stepic/Images.xcassets/PromoBannersIllustrations/promo-banner-illustration-work.imageset/promo-banner-illustration-work.pdf b/Stepic/Images.xcassets/PromoBannersIllustrations/promo-banner-illustration-work.imageset/promo-banner-illustration-work.pdf new file mode 100644 index 0000000000000000000000000000000000000000..4139de045285b2f606b51ee85a949669524595bd GIT binary patch literal 23023 zcmeI4-;W(P702K2UokJ0S|sj{XY84=R3Sl25duU>k$9+h2-~FuC0WR(2>9#yd~MIM zXYNgMvqF%<-G}JrJ7b@pK0ZFS=c6w_|Kf-4I3EtK>%03u{Q1yzpMKi?=tqaUm*42W zhoQx9etGxu+t+{UM!+X}>*4jQyQeP>H=p1A?b+?!lb`*h`|PXsf42{ZzaDOnzg-)V|TMvGO7f@N9nrrIhSC{jms9 zA?r{)?`%Rn3cb`W`*bFg;-vK?R)!Rc zR(b_dJLAgUGS2m=MBJ@>?;n8{V)}b47MJNGAz)$bnr2&YoTeOY`f*tX4RH;fR<(vm zR8x0(OTHGOb53K|TE^9wi}SOS+6rP!r1-PvERT*t!pNrh1DDIxGFJw;JDLE7m_?Cu z4{qa6VUPFR5{4+pG8y)gHqskwyZBQGH}Qw*8nuq{Pw|NeSruDXl5#*c6bhF3EzG&3 zMP6*>lJW`uEoAba6JHVDL@PwZa52BJM~7yfB0~!`H1?*IOBhzzgOlteCWCQdNF%;+ z(S{!w%HxxC>-3jBb%Q_7W8cq17sjKX;v6oEK+`;pANQBLry@h{0D5X?n8*Z$FzZRIOp16$uOMn?T-jU3x%Fg- zI!N?tXgA95(rtL0Zc&hQ%V^EC<#C4jVGh^`kMPUVD+NlUw$ZI2lGWrdZ^_>lGTcRp zZNZoyS8FaaNvcR2J(IuTpnF4of3ZK^V{60pYiE z3KT}5uwgynHnrK}q>jtEuFeH6}D#j_H*MxuXPB=XZ^)O(FYIg4kG&E>}JlI&U%O0}r(NlEC^NaQsAvH~U#cj2hg zB}=DkDEhaBB1d865&FHxqH8RoDA9!CW&^-#!2e^hXbeZ^{6cxgQ)h~l{aE;>CH~vD z$*iW^l=%#^y0X=RBP)ag^tjbCpzk7{ZY~Tim*H%h63U0qLR`v?-wA3aM_kZuV-soK1aFt-MvAsST>5 z+GeVF15vHZcGnAL%;iU^rzA?MT9d0oFSSdc$Z)D4Z@snq^pnc4Mo$cbsGV_DFp!mV z>&Xyxkm%JK^`)wv9-~@1w^C4{eOe!4A6ZPS8}%yGw%W}_*`i(3KzU15YTKx{mSvlk zsg?~(FH70Rt!dy4Yq{!MNElf;gKg9s3H?ZOydI^Ua6?)Wd+>Bs276*<-KaMLdr4*M zJ!`u%bs;Q<3tfgZC0))xmaBf+c=88}+;hzb2QIpG7dJupx%`*ha&~ z%4sH)iEK9N5#o%=L}YJU`GjGGKl75jQQyjMNTXq6t*ul;J{et^-TsF?f)L9v`k(wU z29$GMkg*Z2eY{N&;pP zPScEQ2J~OdfGA2YOK@@Q7F?NAa&Ee45oSdnt5uLotta08#|uFh#r60Yy{hNyNkyj1 z;#J!P*!84hIwGlf`Qe{bw5hp{meX`7EJqFrPAo}vOp%rAxNgo5s(r8y@f-?G2NIXr z2M*Zkw#G0V`w%z=+|9>+U@@$7iUS=m;gd%h_*@`I>U9ihRT6GzmdCMdM% zFddg+kfk!}X~oeqL>hu0eS;dW!5?`v_d3P7s*zYpVnto%R&++47Yp5(P>#COW94Z@ z2T=H5IK;;(_ZFR~p3HQBRZ0&-!w2UTbWBnfnpos=EO3_ZA+YDvKF&?$xK`y!26dYOZp3+DoBaLCOri)e3l7dfsX5 zEu84h99A%LMmk?%ngZ(X0tEAW#!ZFNRRFht5pGZV+hB#TRvPui0?JTaKOwi9f~wc) z1u+Q-KO<&05NP2&Y)-^w{JE%`D+W!H6s4%Cfiv7vA8&h$dM^kvss&0-4J@pPL(>K8 z0v(A^Ilqc%U`hmvh!m&vrmWwzK8KYa_nIg?Oidr;1r!Xe+1jZ|xg}G@AnE%BBzcE>fKMIrkTbsDsVi3b@G6OGg9SHPLbG zv>l0nQJ_ajRh~DZrGv(BwO8qm|Mk-_%yU<9ruM+a5m4fo@jx7FQDXwM_HZ6GCR%z~ zu}lVuTB>mG3AFa;kEkol;8JE86liE87B3Vdo&{@r__Fpby0w;uD-k>8!pT9)5C&_!tI-x+ znOPJoR;wCVdOS0PldWtBwmek>o<|!|t$l;N_Yj^Ub15?)vosqaWL}KMG?!Y)+W%0( zVBvwpY~dd6s3FX0sle4lW`T+_omP~5I=`z)wscDddj_;#r8{~X)GwFqxK&`(Dw^Uv z*IZMcNYdlT`Bi$rBsSXCPKO`DN;(0R$MVk&-FpWcC{A{q|<%!fvf9tyy zWx=AWx@s?ZpkqU-;mEM6j+UdO_R}#bKS_3E&(nI?_+z4;t|_f@LovQ)=fpA=Dq&)-rwTB0{=+k-D(VNNqC~51fiqT?I>J95SyWA2 zjV5V#3#a)>+6{wbzp(W5y4Y1==UB#2JfKzSlS!fh3;C4bHZTjRnLER*V%J_-T%gv4 zV{^kwA8$73_D~pQv1eeFr}kH|D!JB^*ibAnMnh^JFR99-hM^wcL2%l6LDcoI> zvJZr4W<<7?C$8;kr9%ZaV`!YXFjB*0wBn{znPI_AC_%BhFkPWW5aROkue3^RjoVQ2L?6hNFVYLsTqhu27QGqBAAg^tGjmrU=hi96AcgOwjm_ z%7A?Mgo6*-lpvFUYAzv?%gubjgZY60NgCWy-lwbxsZ}1dbWU?^fjyb#O%?XmZX=n- zpwcEwW=aN%99O%^ADH1%sa9e2!?T&3LPz4X%MG#GW2mnn7-6X~Wi${Cyp7 z6ge{LwRKfF+Mvu3e%41kHYgh?3n7GT)Or}$q+>*}@x~?)Cz??~Cycrao(@YB`*QR{ zMub3oypGn;3`7@_L~sE&v#d>12;lTE!#_H#A* z1C;?LW=9-g(qSD)M>8s{n6|Sfwx!q*wD)A0xWqwU+DdK7pVLIanHHj>VZ?*N;Xsvy zc%QQP0_A5ZI6^@mpi3*{^i;vtSXp~fJs6%R<^yKl(zQVCti_RfN(I?4i5hE~tETh~ z5)d{{Sh#AH&1rOfaRh8&Pn1zec1|74k;-5wCW9*Lsg_#vtQ6agEi+4f$k#Xm{Ss-T zW=M7cFDW3J_S7R6HUw5Vp6;wCyW_O?=`hp&Az8t>93uUHRqmEf(#J2as>daZ-$syf{VfSH2k}Z;k*~xhK~_u$<_SF;LC#^;wpKV2|8J6{3dd&^ zC%#U`P$5I~WJGxLyEDZ9tSF}jFr*4HDSYuMM@|mltgJEhH67)EZ%y{KH$BIwFw9^k zlo9d?*|IM<)geQ3$i{0G$QzUa+B0U@gh(LkHq1rB5;@wizA4aDMtmd@;O`Xkbb0N36vWcmIDHC%wc54 zB`dwe`$_>(lL}u>En6fMsTC6|SXEBo<%SniVhb^Tx$=R?E!ea3pK3X4Z@5AxVHHZ* zB139=!iu5^Um(K39B=6A%Tz3=kdh2?2fw0Vqu#^_#KcT|gBh0_w20I6XsQ5PX&c7T zmWmqtdEK-MU~pn|?ZQx;x^`h5H#OR-_ZDp0udzwrrd-p1ywi{Qlc*)4#vE`{SYS z=0lcS|8^XEVc!6_{At%ke;r>RE`OAo)2|g82CwgY>aT$!ir7LEYr-8g>(jsGt=F%f zK6`%m>cAJ7Z+`wc>iGEK{>9xV-N(rMhEt>1k0?4NgVpT5le^wsMm Q4(VjyA3pl%r@#K<{9 literal 0 HcmV?d00001 diff --git a/Stepic/Legacy/Analytics/Events/AnalyticsEvents.swift b/Stepic/Legacy/Analytics/Events/AnalyticsEvents.swift index d6291cffbb..ed9ec6125c 100644 --- a/Stepic/Legacy/Analytics/Events/AnalyticsEvents.swift +++ b/Stepic/Legacy/Analytics/Events/AnalyticsEvents.swift @@ -108,6 +108,38 @@ extension AnalyticsEvent { static let profileOpenSettingsTapped = AnalyticsEvent(name: "main_choice_settings") static let profilePinsMapInteracted = AnalyticsEvent(name: "pins_map_interaction") + // MARK: - Promo Banners - + + static func promoBannerSeen(_ promoBanner: PromoBanner) -> AnalyticsEvent { + AnalyticsEvent( + name: "Promo banner seen", + parameters: [ + "type": promoBanner.type, + "lang": promoBanner.lang, + "title": promoBanner.title, + "description": promoBanner.description, + "url": promoBanner.url, + "screen": promoBanner.screen, + "position": promoBanner.position + ] + ) + } + + static func promoBannerClicked(_ promoBanner: PromoBanner) -> AnalyticsEvent { + AnalyticsEvent( + name: "Promo banner clicked", + parameters: [ + "type": promoBanner.type, + "lang": promoBanner.lang, + "title": promoBanner.title, + "description": promoBanner.description, + "url": promoBanner.url, + "screen": promoBanner.screen, + "position": promoBanner.position + ] + ) + } + // MARK: - Step - static let generateNewAttemptTapped = AnalyticsEvent(name: "clicked_generate_new_attempt") diff --git a/Stepic/Legacy/Model/RemoteConfig/RemoteConfig.swift b/Stepic/Legacy/Model/RemoteConfig/RemoteConfig.swift index 01884ffc2b..f3d15eb799 100644 --- a/Stepic/Legacy/Model/RemoteConfig/RemoteConfig.swift +++ b/Stepic/Legacy/Model/RemoteConfig/RemoteConfig.swift @@ -126,6 +126,10 @@ The price includes commission from App Store and VAT. By paying for access to th self.getNSStringValueFromDelegateOrRemoteConfigForKey(.purchaseFlowPromoCodeEnabled)?.boolValue ?? false } + var promoBannersStringValue: String? { + self.getStringValueFromDelegateOrRemoteConfigForKey(.promoBanners) + } + init(delegate: RemoteConfigDelegate? = nil) { self.delegate = delegate @@ -241,31 +245,9 @@ The price includes commission from App Store and VAT. By paying for access to th case purchaseFlowDisclaimerRussian = "purchase_flow_ios_disclaimer_ru" case purchaseFlowDisclaimerEnglish = "purchase_flow_ios_disclaimer_en" case purchaseFlowPromoCodeEnabled = "purchase_flow_ios_promocode_enabled" + case promoBanners = "promo_banners_ios" - var valueDataType: ValueDataType { - switch self { - case .showStreaksNotificationTrigger: - return .string - case .adaptiveBackendUrl: - return .string - case .supportedInAdaptiveModeCourses: - return .string - case .arQuickLookAvailable: - return .string - case .searchResultsQueryParams: - return .string - case .isCoursePricesEnabled: - return .string - case .isCourseRevenueAvailable: - return .string - case .purchaseFlow: - return .string - case .purchaseFlowDisclaimerRussian, .purchaseFlowDisclaimerEnglish: - return .string - case .purchaseFlowPromoCodeEnabled: - return .string - } - } + var valueDataType: ValueDataType { .string } fileprivate var analyticsUserPropertyKey: String { "\(RemoteConfig.analyticsUserPropertyKeyPrefix)\(self.rawValue)" diff --git a/Stepic/Sources/Model/PromoBanner.swift b/Stepic/Sources/Model/PromoBanner.swift new file mode 100644 index 0000000000..ad87111ee7 --- /dev/null +++ b/Stepic/Sources/Model/PromoBanner.swift @@ -0,0 +1,26 @@ +import Foundation + +struct PromoBanner: Decodable { + let type: String + let lang: String + let title: String + let description: String + let url: String + let screen: String + var position: Int + + var colorType: ColorType? { ColorType(rawValue: self.type) } + + var screenType: ScreenType? { ScreenType(rawValue: self.screen) } + + enum ColorType: String { + case blue + case green + case violet + } + + enum ScreenType: String { + case home + case catalog + } +} diff --git a/Stepic/Sources/Modules/BaseExplore/BaseExploreInteractor.swift b/Stepic/Sources/Modules/BaseExplore/BaseExploreInteractor.swift index 61738aa154..7873a917b5 100644 --- a/Stepic/Sources/Modules/BaseExplore/BaseExploreInteractor.swift +++ b/Stepic/Sources/Modules/BaseExplore/BaseExploreInteractor.swift @@ -9,15 +9,18 @@ protocol BaseExploreInteractorProtocol { class BaseExploreInteractor: BaseExploreInteractorProtocol, CourseListOutputProtocol { let presenter: BaseExplorePresenterProtocol let contentLanguageService: ContentLanguageServiceProtocol + let promoBannersService: PromoBannersServiceProtocol let networkReachabilityService: NetworkReachabilityServiceProtocol init( presenter: BaseExplorePresenterProtocol, contentLanguageService: ContentLanguageServiceProtocol, + promoBannersService: PromoBannersServiceProtocol, networkReachabilityService: NetworkReachabilityServiceProtocol ) { self.presenter = presenter self.contentLanguageService = contentLanguageService + self.promoBannersService = promoBannersService self.networkReachabilityService = networkReachabilityService } diff --git a/Stepic/Sources/Modules/BaseExplore/BaseExploreViewController.swift b/Stepic/Sources/Modules/BaseExplore/BaseExploreViewController.swift index 310e70ecaf..688d9351ab 100644 --- a/Stepic/Sources/Modules/BaseExplore/BaseExploreViewController.swift +++ b/Stepic/Sources/Modules/BaseExplore/BaseExploreViewController.swift @@ -48,6 +48,7 @@ class BaseExploreViewController: UIViewController { func registerSubmodule(_ submodule: Submodule) { self.submodules.append(submodule) + self.submodules.sort { $0.type.position < $1.type.position } if let viewController = submodule.viewController { self.addChild(viewController) diff --git a/Stepic/Sources/Modules/Explore/ExploreDataFlow.swift b/Stepic/Sources/Modules/Explore/ExploreDataFlow.swift index 58d722fb05..96a23f8f90 100644 --- a/Stepic/Sources/Modules/Explore/ExploreDataFlow.swift +++ b/Stepic/Sources/Modules/Explore/ExploreDataFlow.swift @@ -20,6 +20,7 @@ enum Explore { struct Response { let contentLanguage: ContentLanguage + let promoBanners: [PromoBanner] } struct ViewModel { @@ -134,6 +135,6 @@ enum Explore { enum ViewControllerState { case loading - case normal(contentLanguage: ContentLanguage) + case normal(contentLanguage: ContentLanguage, promoBanners: [PromoBanner]) } } diff --git a/Stepic/Sources/Modules/Explore/ExploreInteractor.swift b/Stepic/Sources/Modules/Explore/ExploreInteractor.swift index 4fa1ad0bce..d026eb5b52 100644 --- a/Stepic/Sources/Modules/Explore/ExploreInteractor.swift +++ b/Stepic/Sources/Modules/Explore/ExploreInteractor.swift @@ -33,13 +33,20 @@ final class ExploreInteractor: BaseExploreInteractor, ExploreInteractorProtocol super.init( presenter: presenter, contentLanguageService: contentLanguageService, + promoBannersService: PromoBannersService(remoteConfig: .shared), networkReachabilityService: networkReachabilityService ) } func doContentLoad(request: Explore.ContentLoad.Request) { self.explorePresenter?.presentContent( - response: .init(contentLanguage: self.contentLanguageService.globalContentLanguage) + response: .init( + contentLanguage: self.contentLanguageService.globalContentLanguage, + promoBanners: self.promoBannersService.getPromoBanners( + language: self.contentLanguageService.globalContentLanguage, + screen: .catalog + ) + ) ) self.syncPersonalOffers() } diff --git a/Stepic/Sources/Modules/Explore/ExplorePresenter.swift b/Stepic/Sources/Modules/Explore/ExplorePresenter.swift index 3f12c1f162..b464995a7c 100644 --- a/Stepic/Sources/Modules/Explore/ExplorePresenter.swift +++ b/Stepic/Sources/Modules/Explore/ExplorePresenter.swift @@ -18,7 +18,9 @@ final class ExplorePresenter: BaseExplorePresenter, ExplorePresenterProtocol { func presentContent(response: Explore.ContentLoad.Response) { self.exploreViewController?.displayContent( - viewModel: .init(state: .normal(contentLanguage: response.contentLanguage)) + viewModel: .init( + state: .normal(contentLanguage: response.contentLanguage, promoBanners: response.promoBanners) + ) ) } diff --git a/Stepic/Sources/Modules/Explore/ExploreViewController.swift b/Stepic/Sources/Modules/Explore/ExploreViewController.swift index 6380e3b8bc..297c4ea682 100644 --- a/Stepic/Sources/Modules/Explore/ExploreViewController.swift +++ b/Stepic/Sources/Modules/Explore/ExploreViewController.swift @@ -122,7 +122,7 @@ final class ExploreViewController: BaseExploreViewController { private func updateState(newState: Explore.ViewControllerState) { switch newState { - case .normal(let language): + case .normal(let language, let promoBanners): self.exploreView?.endRefreshing() DispatchQueue.main.asyncAfter(deadline: .now() + Animation.modulesRefreshDelay) { [weak self] in guard let strongSelf = self else { @@ -133,6 +133,7 @@ final class ExploreViewController: BaseExploreViewController { strongSelf.initLanguageDependentSubmodules(contentLanguage: language) strongSelf.refreshStateForVisitedCourses(state: .shown) + strongSelf.refreshStateForPromoBanners(promoBanners) } case .loading: break @@ -318,6 +319,73 @@ final class ExploreViewController: BaseExploreViewController { private func ipadCancelSearchButtonClicked() { self.searchBarCancelButtonClicked(self.searchBar) } + + // MARK: Promo Banners + + private func refreshStateForPromoBanners(_ promoBanners: [PromoBanner]) { + promoBanners.forEach(self.registerPromoBanner(_:)) + } + + private func registerPromoBanner(_ promoBanner: PromoBanner) { + var updatedPromoBanner = promoBanner + updatedPromoBanner.position += 2 + + if let module = self.getSubmodule(type: updatedPromoBanner) { + self.removeSubmodule(module) + } + + guard let colorType = updatedPromoBanner.colorType else { + return + } + + let view = PromoBannerView() + view.title = updatedPromoBanner.title + view.subtitle = updatedPromoBanner.description + view.style = .init(colorType: colorType) + view.onClick = { [weak self] in + guard let strongSelf = self else { + return + } + + strongSelf.analytics.send(.promoBannerClicked(promoBanner)) + + WebControllerManager.shared.presentWebControllerWithURLString( + promoBanner.url, + inController: strongSelf, + withKey: .externalLink, + allowsSafari: true, + backButtonStyle: .done + ) + } + + var headerViewInsets = ExploreBlockContainerView.Appearance().headerViewInsets + if promoBanner.position > 0 { + headerViewInsets.top = 0 + } + + var contentViewInsets = CourseListContainerViewFactory.Appearance.horizontalContentInsets + contentViewInsets.left = headerViewInsets.left + contentViewInsets.right = headerViewInsets.right + + let containerView = CourseListContainerViewFactory(colorMode: .light) + .makeHorizontalContainerView( + for: view, + headerDescription: .init(title: nil, summary: nil, shouldShowAllButton: false), + headerViewInsets: headerViewInsets, + contentViewInsets: contentViewInsets + ) + + self.registerSubmodule( + .init( + viewController: nil, + view: containerView, + isLanguageDependent: true, + type: updatedPromoBanner + ) + ) + + self.analytics.send(.promoBannerSeen(promoBanner)) + } } extension Explore.Submodule: SubmoduleType { diff --git a/Stepic/Sources/Modules/Home/HomeDataFlow.swift b/Stepic/Sources/Modules/Home/HomeDataFlow.swift index c27998a357..15f3f6fe06 100644 --- a/Stepic/Sources/Modules/Home/HomeDataFlow.swift +++ b/Stepic/Sources/Modules/Home/HomeDataFlow.swift @@ -23,11 +23,13 @@ enum Home { struct Response { let isAuthorized: Bool let contentLanguage: ContentLanguage + let promoBanners: [PromoBanner] } struct ViewModel { let isAuthorized: Bool let contentLanguage: ContentLanguage + let promoBanners: [PromoBanner] } } diff --git a/Stepic/Sources/Modules/Home/HomeInteractor.swift b/Stepic/Sources/Modules/Home/HomeInteractor.swift index 143f488d4c..d770c7c2e6 100644 --- a/Stepic/Sources/Modules/Home/HomeInteractor.swift +++ b/Stepic/Sources/Modules/Home/HomeInteractor.swift @@ -24,6 +24,7 @@ final class HomeInteractor: BaseExploreInteractor, HomeInteractorProtocol { super.init( presenter: presenter, contentLanguageService: contentLanguageService, + promoBannersService: PromoBannersService(remoteConfig: .shared), networkReachabilityService: networkReachabilityService ) } @@ -52,7 +53,11 @@ final class HomeInteractor: BaseExploreInteractor, HomeInteractorProtocol { self.homePresenter?.presentContent( response: .init( isAuthorized: self.userAccountService.isAuthorized, - contentLanguage: self.contentLanguageService.globalContentLanguage + contentLanguage: self.contentLanguageService.globalContentLanguage, + promoBanners: self.promoBannersService.getPromoBanners( + language: self.contentLanguageService.globalContentLanguage, + screen: .home + ) ) ) } diff --git a/Stepic/Sources/Modules/Home/HomePresenter.swift b/Stepic/Sources/Modules/Home/HomePresenter.swift index cb12f9b5b3..a53dbcf3b3 100644 --- a/Stepic/Sources/Modules/Home/HomePresenter.swift +++ b/Stepic/Sources/Modules/Home/HomePresenter.swift @@ -38,7 +38,8 @@ final class HomePresenter: BaseExplorePresenter, HomePresenterProtocol { self.homeViewController?.displayContent( viewModel: .init( isAuthorized: response.isAuthorized, - contentLanguage: response.contentLanguage + contentLanguage: response.contentLanguage, + promoBanners: response.promoBanners ) ) } diff --git a/Stepic/Sources/Modules/Home/HomeViewController.swift b/Stepic/Sources/Modules/Home/HomeViewController.swift index f30cfc68c9..61f9bded6e 100644 --- a/Stepic/Sources/Modules/Home/HomeViewController.swift +++ b/Stepic/Sources/Modules/Home/HomeViewController.swift @@ -1,6 +1,7 @@ import PromiseKit import UIKit +// swiftlint:disable file_length protocol HomeViewControllerProtocol: BaseExploreViewControllerProtocol { func displayStreakInfo(viewModel: Home.StreakLoad.ViewModel) func displayContent(viewModel: Home.ContentLoad.ViewModel) @@ -499,6 +500,70 @@ final class HomeViewController: BaseExploreViewController { ) ) } + + // MARK: Promo Banners + + private func refreshStateForPromoBanners(_ promoBanners: [PromoBanner]) { + promoBanners.forEach(self.registerPromoBanner(_:)) + } + + private func registerPromoBanner(_ promoBanner: PromoBanner) { + if let module = self.getSubmodule(type: promoBanner) { + self.removeSubmodule(module) + } + + guard let colorType = promoBanner.colorType else { + return + } + + let view = PromoBannerView() + view.title = promoBanner.title + view.subtitle = promoBanner.description + view.style = .init(colorType: colorType) + view.onClick = { [weak self] in + guard let strongSelf = self else { + return + } + + strongSelf.analytics.send(.promoBannerClicked(promoBanner)) + + WebControllerManager.shared.presentWebControllerWithURLString( + promoBanner.url, + inController: strongSelf, + withKey: .externalLink, + allowsSafari: true, + backButtonStyle: .done + ) + } + + var headerViewInsets = ExploreBlockContainerView.Appearance().headerViewInsets + if promoBanner.position > 0 { + headerViewInsets.top = 0 + } + + var contentViewInsets = CourseListContainerViewFactory.Appearance.horizontalContentInsets + contentViewInsets.left = headerViewInsets.left + contentViewInsets.right = headerViewInsets.right + + let containerView = CourseListContainerViewFactory(colorMode: .light) + .makeHorizontalContainerView( + for: view, + headerDescription: .init(title: nil, summary: nil, shouldShowAllButton: false), + headerViewInsets: headerViewInsets, + contentViewInsets: contentViewInsets + ) + + self.registerSubmodule( + .init( + viewController: nil, + view: containerView, + isLanguageDependent: true, + type: promoBanner + ) + ) + + self.analytics.send(.promoBannerSeen(promoBanner)) + } } // MARK: - HomeViewController: HomeViewControllerProtocol - @@ -561,6 +626,7 @@ extension HomeViewController: HomeViewControllerProtocol { strongSelf.refreshReviewsAndWishlist(state: shouldDisplayReviewsAndWishlist ? .shown : .hidden) strongSelf.refreshStateForVisitedCourses(state: .shown) strongSelf.refreshStateForPopularCourses(state: .normal) + strongSelf.refreshStateForPromoBanners(viewModel.promoBanners) } } @@ -586,3 +652,7 @@ extension Home.Submodule: SubmoduleType { return position } } + +extension PromoBanner: SubmoduleType { + var uniqueIdentifier: UniqueIdentifierType { "promoBanner\(self.position)" } +} diff --git a/Stepic/Sources/Services/PromoBannersService.swift b/Stepic/Sources/Services/PromoBannersService.swift new file mode 100644 index 0000000000..f35cee0012 --- /dev/null +++ b/Stepic/Sources/Services/PromoBannersService.swift @@ -0,0 +1,41 @@ +import Foundation + +protocol PromoBannersServiceProtocol: AnyObject { + func getPromoBanners() -> [PromoBanner] +} + +extension PromoBannersServiceProtocol { + func getPromoBanners(language: ContentLanguage, screen: PromoBanner.ScreenType) -> [PromoBanner] { + self.getPromoBanners().filter { $0.lang == language.languageString && $0.screenType == screen } + } +} + +final class PromoBannersService: PromoBannersServiceProtocol { + private let remoteConfig: RemoteConfig + + init(remoteConfig: RemoteConfig) { + self.remoteConfig = remoteConfig + } + + func getPromoBanners() -> [PromoBanner] { + guard let promoBannersStringValue = self.remoteConfig.promoBannersStringValue, + let data = promoBannersStringValue.data(using: .utf8), + !data.isEmpty else { + return [] + } + + do { + let decoder = JSONDecoder() + + let promoBanners = try decoder.decode([PromoBanner].self, from: data) + let supportedPromoBanners = promoBanners.filter { + $0.colorType != nil && $0.screenType != nil && $0.position >= 0 + } + + return supportedPromoBanners + } catch { + print("PromoBannersService :: failed decode with error = \(error)") + return [] + } + } +} diff --git a/Stepic/Sources/Views/PromoBannerView.swift b/Stepic/Sources/Views/PromoBannerView.swift new file mode 100644 index 0000000000..f1e4c06c54 --- /dev/null +++ b/Stepic/Sources/Views/PromoBannerView.swift @@ -0,0 +1,228 @@ +import SnapKit +import UIKit + +extension PromoBannerView { + struct Appearance { + let cornerRadius: CGFloat = 13 + + let shadowColor = UIColor.black + let shadowOffset = CGSize(width: 0, height: 1) + let shadowRadius: CGFloat = 4.0 + let shadowOpacity: Float = 0.05 + + let titleLabelFont = UIFont.systemFont(ofSize: 18, weight: .bold) + let titleLabelInsets = LayoutInsets(top: 16, left: 16) + + let subtitleLabelFont = Typography.caption2Font + let subtitleLabelInsets = LayoutInsets(top: 8, left: 16, bottom: 16) + } +} + +final class PromoBannerView: UIControl { + let appearance: Appearance + + private lazy var titleLabel: UILabel = { + let label = UILabel() + label.font = self.appearance.titleLabelFont + label.numberOfLines = 0 + return label + }() + + private lazy var subtitleLabel: UILabel = { + let label = UILabel() + label.font = self.appearance.subtitleLabelFont + label.numberOfLines = 0 + return label + }() + + private lazy var illustrationImageView: UIImageView = { + let imageView = UIImageView() + imageView.contentMode = .scaleAspectFit + return imageView + }() + + var title: String? { + didSet { + self.titleLabel.text = self.title + } + } + + var subtitle: String? { + didSet { + self.subtitleLabel.text = self.subtitle + } + } + + var style = Style.green { + didSet { + self.updateStyle() + } + } + + var onClick: (() -> Void)? + + override var isHighlighted: Bool { + didSet { + self.animateBounce() + } + } + + override var intrinsicContentSize: CGSize { + let titleHeightWithInsets = self.appearance.titleLabelInsets.top + self.titleLabel.intrinsicContentSize.height + let subtitleHeightWithInsets = self.appearance.subtitleLabelInsets.top + + self.subtitleLabel.intrinsicContentSize.height + + self.appearance.subtitleLabelInsets.bottom + + let height = titleHeightWithInsets + subtitleHeightWithInsets + + return CGSize(width: UIView.noIntrinsicMetric, height: height) + } + + init( + frame: CGRect = .zero, + appearance: Appearance = Appearance() + ) { + self.appearance = appearance + super.init(frame: frame) + + self.setupView() + self.addSubviews() + self.makeConstraints() + + self.updateStyle() + } + + @available(*, unavailable) + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + + self.layer.cornerRadius = self.appearance.cornerRadius + self.layer.masksToBounds = true + + self.layer.shadowColor = self.appearance.shadowColor.cgColor + self.layer.shadowOffset = self.appearance.shadowOffset + self.layer.shadowRadius = self.appearance.shadowRadius + self.layer.shadowOpacity = self.appearance.shadowOpacity + self.layer.masksToBounds = false + self.layer.shadowPath = UIBezierPath( + roundedRect: self.bounds, + cornerRadius: self.layer.cornerRadius + ).cgPath + } + + // MARK: Private API + + private func updateStyle() { + self.backgroundColor = self.style.backgroundColor + self.illustrationImageView.image = self.style.illustrationImage + + self.titleLabel.textColor = self.style.titleLabelTextColor + self.subtitleLabel.textColor = self.style.subtitleLabelTextColor + } + + @objc + private func handleTouchUpInside() { + self.onClick?() + } + + // MARK: Inner Types + + enum Style { + case blue + case green + case violet + + fileprivate var backgroundColor: UIColor { + switch self { + case .blue: + return .dynamic(light: .stepikOverlayDarkBlueFixed, dark: .stepikOverlayBlueBackground) + case .green: + return .stepikOverlayGreenBackground + case .violet: + return .dynamic(light: .stepikViolet05Fixed, dark: .stepikViolet05Fixed.withAlphaComponent(0.12)) + } + } + + fileprivate var titleLabelTextColor: UIColor { + .dynamic(light: .black, dark: .stepikMaterialPrimaryText) + } + + fileprivate var subtitleLabelTextColor: UIColor { + switch self { + case .blue, .violet: + return .dynamic(light: .white, dark: .stepikMaterialSecondaryText) + case .green: + return .stepikMaterialSecondaryText + } + } + + fileprivate var illustrationImage: UIImage? { + let imageName: String + + switch self { + case .blue: + imageName = "promo-banner-illustration-bicycle-green" + case .green: + imageName = "promo-banner-illustration-work" + case .violet: + imageName = "promo-banner-illustration-bicycle-violet" + } + + return UIImage(named: imageName) + } + } +} + +// MARK: - PromoBannerView: ProgrammaticallyInitializableViewProtocol - + +extension PromoBannerView: ProgrammaticallyInitializableViewProtocol { + func setupView() { + self.addTarget(self, action: #selector(self.handleTouchUpInside), for: .touchUpInside) + } + + func addSubviews() { + self.addSubview(self.illustrationImageView) + self.addSubview(self.titleLabel) + self.addSubview(self.subtitleLabel) + } + + func makeConstraints() { + self.titleLabel.translatesAutoresizingMaskIntoConstraints = false + self.titleLabel.snp.makeConstraints { make in + make.top.leading.equalToSuperview().inset(self.appearance.titleLabelInsets.edgeInsets) + make.width.equalToSuperview().multipliedBy(0.5) + } + + self.subtitleLabel.translatesAutoresizingMaskIntoConstraints = false + self.subtitleLabel.snp.makeConstraints { make in + make.top.equalTo(self.titleLabel.snp.bottom).offset(self.appearance.subtitleLabelInsets.top) + make.leading.equalToSuperview().offset(self.appearance.subtitleLabelInsets.left) + make.bottom.equalToSuperview().offset(-self.appearance.subtitleLabelInsets.bottom) + make.width.equalToSuperview().multipliedBy(0.6) + } + + self.illustrationImageView.translatesAutoresizingMaskIntoConstraints = false + self.illustrationImageView.snp.makeConstraints { make in + make.top.bottom.trailing.equalToSuperview() + make.height.equalToSuperview() + make.width.equalToSuperview().multipliedBy(0.4) + } + } +} + +extension PromoBannerView.Style { + init(colorType: PromoBanner.ColorType) { + switch colorType { + case .blue: + self = .blue + case .green: + self = .green + case .violet: + self = .violet + } + } +} diff --git a/Stepic/Theme/ColorPalette.swift b/Stepic/Theme/ColorPalette.swift index 6587ed1b9e..0b46962a98 100644 --- a/Stepic/Theme/ColorPalette.swift +++ b/Stepic/Theme/ColorPalette.swift @@ -117,6 +117,8 @@ extension UIColor { static let stepikBlueFixed = ColorPalette.blue600 /// A non adaptable color with hex value #56A4FF (blue03). static let stepikLightBlueFixed = ColorPalette.lightBlue400 + /// A non adaptable color with hex value #91C7FF. + static let stepikOverlayDarkBlueFixed = ColorPalette.blueDarkColorOverlay // MARK: Yellow From d354f2f5b176c447c023ac100fb8679b665f6959 Mon Sep 17 00:00:00 2001 From: Stepik Bot Date: Wed, 16 Feb 2022 03:02:12 +0000 Subject: [PATCH 05/13] "Set version to 1.210 & bump build" --- Stepic.xcodeproj/project.pbxproj | 24 +++++++++++----------- Stepic/Info-Develop.plist | 4 ++-- Stepic/Info-Production.plist | 4 ++-- Stepic/Info-Release.plist | 4 ++-- StepicTests/Info-Develop.plist | 4 ++-- StepicTests/Info-Production.plist | 4 ++-- StepicTests/Info-Release.plist | 4 ++-- StickerPackExtension/Info-Develop.plist | 4 ++-- StickerPackExtension/Info-Production.plist | 4 ++-- StickerPackExtension/Info-Release.plist | 4 ++-- 10 files changed, 30 insertions(+), 30 deletions(-) diff --git a/Stepic.xcodeproj/project.pbxproj b/Stepic.xcodeproj/project.pbxproj index d66ede56c4..3e2e0c1b9d 100644 --- a/Stepic.xcodeproj/project.pbxproj +++ b/Stepic.xcodeproj/project.pbxproj @@ -13064,7 +13064,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 411; + CURRENT_PROJECT_VERSION = 412; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = "StickerPackExtension/Info-Production.plist"; @@ -13089,7 +13089,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 411; + CURRENT_PROJECT_VERSION = 412; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = "StickerPackExtension/Info-Production.plist"; IPHONEOS_DEPLOYMENT_TARGET = 11.0; @@ -13231,7 +13231,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 411; + CURRENT_PROJECT_VERSION = 412; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_BITCODE = YES; INFOPLIST_FILE = "Stepic/Info-Production.plist"; @@ -13261,7 +13261,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 411; + CURRENT_PROJECT_VERSION = 412; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_BITCODE = YES; "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64; @@ -13352,7 +13352,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 411; + CURRENT_PROJECT_VERSION = 412; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_BITCODE = YES; INFOPLIST_FILE = "Stepic/Info-Develop.plist"; @@ -13404,7 +13404,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 411; + CURRENT_PROJECT_VERSION = 412; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = "StickerPackExtension/Info-Develop.plist"; @@ -13485,7 +13485,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 411; + CURRENT_PROJECT_VERSION = 412; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_BITCODE = YES; INFOPLIST_FILE = "Stepic/Info-Develop.plist"; @@ -13533,7 +13533,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 411; + CURRENT_PROJECT_VERSION = 412; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = "StickerPackExtension/Info-Develop.plist"; IPHONEOS_DEPLOYMENT_TARGET = 11.0; @@ -14053,7 +14053,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 411; + CURRENT_PROJECT_VERSION = 412; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_BITCODE = YES; INFOPLIST_FILE = "Stepic/Info-Release.plist"; @@ -14107,7 +14107,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 411; + CURRENT_PROJECT_VERSION = 412; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = "StickerPackExtension/Info-Release.plist"; @@ -14189,7 +14189,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 411; + CURRENT_PROJECT_VERSION = 412; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_BITCODE = YES; INFOPLIST_FILE = "Stepic/Info-Release.plist"; @@ -14237,7 +14237,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 411; + CURRENT_PROJECT_VERSION = 412; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = "StickerPackExtension/Info-Release.plist"; IPHONEOS_DEPLOYMENT_TARGET = 11.0; diff --git a/Stepic/Info-Develop.plist b/Stepic/Info-Develop.plist index 5ebba1278a..ea08d775a6 100644 --- a/Stepic/Info-Develop.plist +++ b/Stepic/Info-Develop.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.209-develop + 1.210-develop CFBundleSignature ???? CFBundleURLTypes @@ -62,7 +62,7 @@ CFBundleVersion - 411 + 412 FacebookAppID 171127739724012 FacebookDisplayName diff --git a/Stepic/Info-Production.plist b/Stepic/Info-Production.plist index 135bf935c5..caab8470cc 100644 --- a/Stepic/Info-Production.plist +++ b/Stepic/Info-Production.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.209 + 1.210 CFBundleSignature ???? CFBundleURLTypes @@ -62,7 +62,7 @@ CFBundleVersion - 411 + 412 FacebookAppID 171127739724012 FacebookDisplayName diff --git a/Stepic/Info-Release.plist b/Stepic/Info-Release.plist index b952de9781..0c690279a3 100644 --- a/Stepic/Info-Release.plist +++ b/Stepic/Info-Release.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.209-release + 1.210-release CFBundleSignature ???? CFBundleURLTypes @@ -62,7 +62,7 @@ CFBundleVersion - 411 + 412 FacebookAppID 171127739724012 FacebookDisplayName diff --git a/StepicTests/Info-Develop.plist b/StepicTests/Info-Develop.plist index 569253aea2..5bf0886b26 100644 --- a/StepicTests/Info-Develop.plist +++ b/StepicTests/Info-Develop.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.209-develop + 1.210-develop CFBundleSignature ???? CFBundleVersion - 411 + 412 diff --git a/StepicTests/Info-Production.plist b/StepicTests/Info-Production.plist index 70412dcf03..6dedcf82c8 100644 --- a/StepicTests/Info-Production.plist +++ b/StepicTests/Info-Production.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.209 + 1.210 CFBundleSignature ???? CFBundleVersion - 411 + 412 diff --git a/StepicTests/Info-Release.plist b/StepicTests/Info-Release.plist index 6ff8951a8e..6a18a8312e 100644 --- a/StepicTests/Info-Release.plist +++ b/StepicTests/Info-Release.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.209-release + 1.210-release CFBundleSignature ???? CFBundleVersion - 411 + 412 diff --git a/StickerPackExtension/Info-Develop.plist b/StickerPackExtension/Info-Develop.plist index 2a945cb81f..13680e08a2 100644 --- a/StickerPackExtension/Info-Develop.plist +++ b/StickerPackExtension/Info-Develop.plist @@ -17,9 +17,9 @@ CFBundlePackageType XPC! CFBundleShortVersionString - 1.209-develop + 1.210-develop CFBundleVersion - 411 + 412 UIRequiredDeviceCapabilities arm64 diff --git a/StickerPackExtension/Info-Production.plist b/StickerPackExtension/Info-Production.plist index b6df31ab44..9f3d776e32 100644 --- a/StickerPackExtension/Info-Production.plist +++ b/StickerPackExtension/Info-Production.plist @@ -17,9 +17,9 @@ CFBundlePackageType XPC! CFBundleShortVersionString - 1.209 + 1.210 CFBundleVersion - 411 + 412 UIRequiredDeviceCapabilities arm64 diff --git a/StickerPackExtension/Info-Release.plist b/StickerPackExtension/Info-Release.plist index 589d6798a8..9b5f2678e0 100644 --- a/StickerPackExtension/Info-Release.plist +++ b/StickerPackExtension/Info-Release.plist @@ -17,9 +17,9 @@ CFBundlePackageType XPC! CFBundleShortVersionString - 1.209-release + 1.210-release CFBundleVersion - 411 + 412 UIRequiredDeviceCapabilities arm64 From deee4bb35aa49b967ab41d7e4d0b3f767197387e Mon Sep 17 00:00:00 2001 From: Ivan Magda Date: Wed, 16 Feb 2022 08:49:32 +0300 Subject: [PATCH 06/13] Update release notes for beta testers --- fastlane/release-notes.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/fastlane/release-notes.txt b/fastlane/release-notes.txt index 19bf4f171a..7134cb1dbe 100644 --- a/fastlane/release-notes.txt +++ b/fastlane/release-notes.txt @@ -1,4 +1,4 @@ Что тестировать: -- Обновить вкладку Инфо о курсе APPS-3590 -- Смена имени в сертификате APPS-3573 -- При первом переходе на экран со списком сертификатов отображается, что нет данных, пока происходит загрузка сертификатов APPS-3596 \ No newline at end of file +- Домашний экран / Итерация 1 / Релиз первой версии APPS-3588 +- Баннеры для привлечения преподавателей APPS-3602 +- Вынести plain модели в отдельный модуль APPS-3607 \ No newline at end of file From 641e35b305165a187b1cafa005a280776ef93dc8 Mon Sep 17 00:00:00 2001 From: Ivan Magda Date: Wed, 16 Feb 2022 08:52:33 +0300 Subject: [PATCH 07/13] Set version to 1.210 & bump build --- Stepic.xcodeproj/project.pbxproj | 24 +++++++++++----------- Stepic/Info-Develop.plist | 2 +- Stepic/Info-Production.plist | 2 +- Stepic/Info-Release.plist | 2 +- StepicTests/Info-Develop.plist | 2 +- StepicTests/Info-Production.plist | 2 +- StepicTests/Info-Release.plist | 2 +- StepicUITests/Info-Develop.plist | 4 ++-- StepicUITests/Info-Production.plist | 4 ++-- StepicUITests/Info-Release.plist | 4 ++-- StepicWidget/Info-Develop.plist | 4 ++-- StepicWidget/Info-Production.plist | 4 ++-- StepicWidget/Info-Release.plist | 4 ++-- StickerPackExtension/Info-Develop.plist | 2 +- StickerPackExtension/Info-Production.plist | 2 +- StickerPackExtension/Info-Release.plist | 2 +- 16 files changed, 33 insertions(+), 33 deletions(-) diff --git a/Stepic.xcodeproj/project.pbxproj b/Stepic.xcodeproj/project.pbxproj index 3e2e0c1b9d..b3342c3b3b 100644 --- a/Stepic.xcodeproj/project.pbxproj +++ b/Stepic.xcodeproj/project.pbxproj @@ -13064,7 +13064,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 412; + CURRENT_PROJECT_VERSION = 413; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = "StickerPackExtension/Info-Production.plist"; @@ -13089,7 +13089,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 412; + CURRENT_PROJECT_VERSION = 413; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = "StickerPackExtension/Info-Production.plist"; IPHONEOS_DEPLOYMENT_TARGET = 11.0; @@ -13231,7 +13231,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 412; + CURRENT_PROJECT_VERSION = 413; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_BITCODE = YES; INFOPLIST_FILE = "Stepic/Info-Production.plist"; @@ -13261,7 +13261,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 412; + CURRENT_PROJECT_VERSION = 413; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_BITCODE = YES; "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64; @@ -13352,7 +13352,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 412; + CURRENT_PROJECT_VERSION = 413; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_BITCODE = YES; INFOPLIST_FILE = "Stepic/Info-Develop.plist"; @@ -13404,7 +13404,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 412; + CURRENT_PROJECT_VERSION = 413; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = "StickerPackExtension/Info-Develop.plist"; @@ -13485,7 +13485,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 412; + CURRENT_PROJECT_VERSION = 413; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_BITCODE = YES; INFOPLIST_FILE = "Stepic/Info-Develop.plist"; @@ -13533,7 +13533,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 412; + CURRENT_PROJECT_VERSION = 413; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = "StickerPackExtension/Info-Develop.plist"; IPHONEOS_DEPLOYMENT_TARGET = 11.0; @@ -14053,7 +14053,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 412; + CURRENT_PROJECT_VERSION = 413; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_BITCODE = YES; INFOPLIST_FILE = "Stepic/Info-Release.plist"; @@ -14107,7 +14107,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 412; + CURRENT_PROJECT_VERSION = 413; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = "StickerPackExtension/Info-Release.plist"; @@ -14189,7 +14189,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 412; + CURRENT_PROJECT_VERSION = 413; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_BITCODE = YES; INFOPLIST_FILE = "Stepic/Info-Release.plist"; @@ -14237,7 +14237,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 412; + CURRENT_PROJECT_VERSION = 413; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = "StickerPackExtension/Info-Release.plist"; IPHONEOS_DEPLOYMENT_TARGET = 11.0; diff --git a/Stepic/Info-Develop.plist b/Stepic/Info-Develop.plist index ea08d775a6..61d5e4a0f1 100644 --- a/Stepic/Info-Develop.plist +++ b/Stepic/Info-Develop.plist @@ -62,7 +62,7 @@ CFBundleVersion - 412 + 413 FacebookAppID 171127739724012 FacebookDisplayName diff --git a/Stepic/Info-Production.plist b/Stepic/Info-Production.plist index caab8470cc..b58a6c864c 100644 --- a/Stepic/Info-Production.plist +++ b/Stepic/Info-Production.plist @@ -62,7 +62,7 @@ CFBundleVersion - 412 + 413 FacebookAppID 171127739724012 FacebookDisplayName diff --git a/Stepic/Info-Release.plist b/Stepic/Info-Release.plist index 0c690279a3..c20844640c 100644 --- a/Stepic/Info-Release.plist +++ b/Stepic/Info-Release.plist @@ -62,7 +62,7 @@ CFBundleVersion - 412 + 413 FacebookAppID 171127739724012 FacebookDisplayName diff --git a/StepicTests/Info-Develop.plist b/StepicTests/Info-Develop.plist index 5bf0886b26..8675a1a83b 100644 --- a/StepicTests/Info-Develop.plist +++ b/StepicTests/Info-Develop.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 412 + 413 diff --git a/StepicTests/Info-Production.plist b/StepicTests/Info-Production.plist index 6dedcf82c8..f945ce07ca 100644 --- a/StepicTests/Info-Production.plist +++ b/StepicTests/Info-Production.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 412 + 413 diff --git a/StepicTests/Info-Release.plist b/StepicTests/Info-Release.plist index 6a18a8312e..3e131a3c07 100644 --- a/StepicTests/Info-Release.plist +++ b/StepicTests/Info-Release.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 412 + 413 diff --git a/StepicUITests/Info-Develop.plist b/StepicUITests/Info-Develop.plist index 924a38dbac..4120aa3b0e 100644 --- a/StepicUITests/Info-Develop.plist +++ b/StepicUITests/Info-Develop.plist @@ -15,8 +15,8 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.209-develop + 1.210-develop CFBundleVersion - 411 + 413 diff --git a/StepicUITests/Info-Production.plist b/StepicUITests/Info-Production.plist index f3e5206a68..4d82938cd8 100644 --- a/StepicUITests/Info-Production.plist +++ b/StepicUITests/Info-Production.plist @@ -15,8 +15,8 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.209 + 1.210 CFBundleVersion - 411 + 413 diff --git a/StepicUITests/Info-Release.plist b/StepicUITests/Info-Release.plist index 69e880636e..581b9429ba 100644 --- a/StepicUITests/Info-Release.plist +++ b/StepicUITests/Info-Release.plist @@ -15,8 +15,8 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.209-release + 1.210-release CFBundleVersion - 411 + 413 diff --git a/StepicWidget/Info-Develop.plist b/StepicWidget/Info-Develop.plist index a2157922ea..ac5921b9c1 100644 --- a/StepicWidget/Info-Develop.plist +++ b/StepicWidget/Info-Develop.plist @@ -17,9 +17,9 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.209-develop + 1.210-develop CFBundleVersion - 411 + 413 NSExtension NSExtensionPointIdentifier diff --git a/StepicWidget/Info-Production.plist b/StepicWidget/Info-Production.plist index 4ba657c8e0..ee5d10fae6 100644 --- a/StepicWidget/Info-Production.plist +++ b/StepicWidget/Info-Production.plist @@ -17,9 +17,9 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.209 + 1.210 CFBundleVersion - 411 + 413 NSExtension NSExtensionPointIdentifier diff --git a/StepicWidget/Info-Release.plist b/StepicWidget/Info-Release.plist index eb60665631..66af63186f 100644 --- a/StepicWidget/Info-Release.plist +++ b/StepicWidget/Info-Release.plist @@ -17,9 +17,9 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.209-release + 1.210-release CFBundleVersion - 411 + 413 NSExtension NSExtensionPointIdentifier diff --git a/StickerPackExtension/Info-Develop.plist b/StickerPackExtension/Info-Develop.plist index 13680e08a2..86ebc40405 100644 --- a/StickerPackExtension/Info-Develop.plist +++ b/StickerPackExtension/Info-Develop.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 1.210-develop CFBundleVersion - 412 + 413 UIRequiredDeviceCapabilities arm64 diff --git a/StickerPackExtension/Info-Production.plist b/StickerPackExtension/Info-Production.plist index 9f3d776e32..b9d5af6268 100644 --- a/StickerPackExtension/Info-Production.plist +++ b/StickerPackExtension/Info-Production.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 1.210 CFBundleVersion - 412 + 413 UIRequiredDeviceCapabilities arm64 diff --git a/StickerPackExtension/Info-Release.plist b/StickerPackExtension/Info-Release.plist index 9b5f2678e0..1c1b9bc6bd 100644 --- a/StickerPackExtension/Info-Release.plist +++ b/StickerPackExtension/Info-Release.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 1.210-release CFBundleVersion - 412 + 413 UIRequiredDeviceCapabilities arm64 From 2ecdd33f5be08b8cf5a9d9cf0e2bf6d3a0a707dd Mon Sep 17 00:00:00 2001 From: Ivan Magda Date: Wed, 16 Feb 2022 19:31:57 +0300 Subject: [PATCH 08/13] Add new home fixes APPS-3588 --- .../Images.xcassets/plus.imageset/Contents.json | 15 +++++++++++++++ Stepic/Images.xcassets/plus.imageset/plus.pdf | Bin 0 -> 4458 bytes .../NewExploreBlockPlaceholderView.swift | 16 ++++++---------- .../Modules/Home/HomeViewController.swift | 11 ++++------- Stepic/en.lproj/Localizable.strings | 1 - Stepic/ru.lproj/Localizable.strings | 1 - 6 files changed, 25 insertions(+), 19 deletions(-) create mode 100644 Stepic/Images.xcassets/plus.imageset/Contents.json create mode 100644 Stepic/Images.xcassets/plus.imageset/plus.pdf 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 0000000000000000000000000000000000000000..28c8c8d8db7b421cd71f87c9fc20a847eff57a0f GIT binary patch literal 4458 zcmeHKU5Fc16jmx)uXRfmTF^?bZew=5O=jZE zWOp`Fg{lwYgDkkvhef*BRj^nD@o6jeK?T85)K(C+D)dR(`c$eGt>?~9lHI++n`Jcy2hvhodO)@ud^jPy};WpMD;tQkd+aTG^VxQ7{z{#+VGN6^j~h zu1FTmQCU|g%4Cpf8C0qwd+ESJUfaNLeC_Je;k#b_YuiQn!p6mwV{d%<(51guxY$?6 z7FHHAU!?3aTV8p4?Cj>r?VIhvBP%~Y`qH60PF~-={9SM9;mZ%~IC9^WsilE(&(6{F z@h8U`RZ_k%_~R`fZQZ~1*Hh2-@9aHy@4`E$XI}3q-;aLy=e@;QZTZkA6XnIX-hbrU zZ&!}y3R9miZ8&#gZ2$4gp$}F+{F?vXHuv3`=-;yU#c$t4pIzLyZPOWPc_^~F{oP-< z!{+(Nub=uUYiv4eiaUBVX%iz77SU@|dZ zqnM0KWr_oscjAsq2GuNh)aNBn6;&G4ZTt+FI2?eOFl!7E2+Rhbq>2AR?AgcPW$*A1DPnBh@q>x)(F z4_B)%lRZnuRKTYy+wfN?`t>N1RDpDd|{j`=Vj4f7oCo3X4~k^s11 zEgMp8MwQL5Zj}4`okUp}R!h~j93!SLmlQaG=L9an4MZZM5aMD%j_YsBInb8VEjlY% zQbjL2(HJqNM@5T3+n^=H;W*-&2T&3fqqu~9TTB41*nzdCEy4j!>ujP3n5?sLJPhDE zn+UzHx5eT&`o%e*!GG2=B(*{fXS)~G28EIt83kRpFl*eMm{F~yV}`v$rfErr<#ZU2 ziYO2m+B=K_gS4z_L$+F)(DlkwH3h&$Ew&JGP}hO1r=@B`GWro|@#mXb__Z)c-Uje7 zcXomgILT8>f>aWY`^9-Xh^^g8^8@&SXI)&$!MO-*Rpg*eXL>7V&jPkxp7cVD8XGBl(Lg{wN{0yuwAWa zX3|avC3rQ^BwRBy34)WMl#+$qu;&7+bkLjGxw*OUoXCci@QFl%Z~_s85O{>ldCijS zkY@Hm1caR!Icmy=T4TeESxjkKuUqM05GZuBD{?b(t!`ADxQa|@g;v=+fxwnw1!0_0 zE=+BPV>3zThm}WEC~z@{}Sh7cAMNxC_rSMb?CI@3j^A-wUs`VWOE~2g0zd zR|(vJsztAASS9^2YF4+#2uL~QuBpN`0RzAVX)@;a3O(J_gMoI_k8YBVAnwA|g{vb3 zI&$t#R~N315a`IcJ6&D4Izpf$=l^HATHo66^_~vSu`g-3TC2D{NyYo}2^36AQpK?7 z*?Im(X~gpbphSJi<6LmOAc98lZ7mrD@_@;p1Zvknq)h;UZlSZ7DIZV^fgw# Date: Wed, 16 Feb 2022 23:28:19 +0300 Subject: [PATCH 09/13] Add promo banners fixes APPS-3602 --- .../BaseExploreViewController.swift | 34 +++++------ .../Explore/ExploreViewController.swift | 57 +++++++++++++------ .../Modules/Home/HomeViewController.swift | 46 +++++++++++---- .../NewProfile/NewProfileViewController.swift | 12 ++-- Stepic/Sources/Views/PromoBannerView.swift | 27 ++++++--- 5 files changed, 118 insertions(+), 58 deletions(-) diff --git a/Stepic/Sources/Modules/BaseExplore/BaseExploreViewController.swift b/Stepic/Sources/Modules/BaseExplore/BaseExploreViewController.swift index 688d9351ab..6bb067cf51 100644 --- a/Stepic/Sources/Modules/BaseExplore/BaseExploreViewController.swift +++ b/Stepic/Sources/Modules/BaseExplore/BaseExploreViewController.swift @@ -10,9 +10,7 @@ protocol BaseExploreViewControllerProtocol: AnyObject { func displayProfile(viewModel: BaseExplore.ProfilePresentation.ViewModel) } -protocol SubmoduleType: UniqueIdentifiable { - var position: Int { get } -} +protocol ExploreSubmoduleType: UniqueIdentifiable {} class BaseExploreViewController: UIViewController { let interactor: BaseExploreInteractorProtocol @@ -47,22 +45,24 @@ class BaseExploreViewController: UIViewController { // MARK: Modules func registerSubmodule(_ submodule: Submodule) { - self.submodules.append(submodule) - self.submodules.sort { $0.type.position < $1.type.position } + defer { + self.submodules.append(submodule) + self.submodules.sort { self.getSubmodulePosition(type: $0.type) < self.getSubmodulePosition(type: $1.type) } + } 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 - } + let insertionIndex: Int = { + for (idx, sub) in self.submodules.enumerated() + where self.getSubmodulePosition(type: sub.type) >= self.getSubmodulePosition(type: submodule.type) { + return idx + } + return self.submodules.endIndex + }() + + self.exploreView?.insertBlockView(submodule.view, at: insertionIndex) } func removeLanguageDependentSubmodules() { @@ -77,10 +77,12 @@ class BaseExploreViewController: UIViewController { self.submodules = self.submodules.filter { submodule.view != $0.view } } - func getSubmodule(type: SubmoduleType) -> Submodule? { + func getSubmodule(type: ExploreSubmoduleType) -> Submodule? { self.submodules.first(where: { $0.type.uniqueIdentifier == type.uniqueIdentifier }) } + func getSubmodulePosition(type: ExploreSubmoduleType) -> Int { 0 } + final func tryToSetOnlineState(moduleInput: CourseListInputProtocol) { self.interactor.doOnlineModeReset(request: .init(modules: [moduleInput])) } @@ -126,7 +128,7 @@ class BaseExploreViewController: UIViewController { let viewController: UIViewController? let view: UIView let isLanguageDependent: Bool - let type: SubmoduleType + let type: ExploreSubmoduleType } } diff --git a/Stepic/Sources/Modules/Explore/ExploreViewController.swift b/Stepic/Sources/Modules/Explore/ExploreViewController.swift index 297c4ea682..277a4349e9 100644 --- a/Stepic/Sources/Modules/Explore/ExploreViewController.swift +++ b/Stepic/Sources/Modules/Explore/ExploreViewController.swift @@ -21,17 +21,20 @@ final class ExploreViewController: BaseExploreViewController { static let modulesRefreshDelay: TimeInterval = 0.3 } - static let submodulesOrder: [Explore.Submodule] = [ + fileprivate static let submodulesOrder: [Explore.Submodule] = [ .stories, .languageSwitch, .catalogBlocks, .visitedCourses ] + private static let promoBannersSubmodulesOrderOffset = 2 + private var submodulesOrderMap: [UniqueIdentifierType: Int] = [:] private var state: Explore.ViewControllerState private lazy var exploreInteractor = self.interactor as? ExploreInteractorProtocol private var currentContentLanguage: ContentLanguage? + private var currentPromoBanners = [PromoBanner]() private var currentStoriesSubmoduleState = StoriesState.shown // SearchResults private var searchResultsModuleInput: SearchResultsModuleInputProtocol? @@ -69,8 +72,11 @@ final class ExploreViewController: BaseExploreViewController { initialState: Explore.ViewControllerState = .loading ) { self.state = initialState + super.init(interactor: interactor, analytics: analytics) + self.searchBar.searchBarDelegate = self + self.buildSubmodulesOrderMap() } @available(*, unavailable) @@ -129,6 +135,9 @@ final class ExploreViewController: BaseExploreViewController { return } + strongSelf.currentPromoBanners = promoBanners + strongSelf.buildSubmodulesOrderMap() + strongSelf.removeLanguageDependentSubmodules() strongSelf.initLanguageDependentSubmodules(contentLanguage: language) @@ -141,6 +150,8 @@ final class ExploreViewController: BaseExploreViewController { self.state = newState } + // MARK: - Display submodules + override func refreshContentAfterLanguageChange() { self.exploreInteractor?.doContentLoad(request: .init()) } @@ -149,6 +160,28 @@ final class ExploreViewController: BaseExploreViewController { self.exploreInteractor?.doContentLoad(request: .init()) } + override func getSubmodulePosition(type: ExploreSubmoduleType) -> Int { + self.submodulesOrderMap[type.uniqueIdentifier] ?? 0 + } + + private func buildSubmodulesOrderMap() { + var orderMap = Dictionary( + uniqueKeysWithValues: Self.submodulesOrder.enumerated().map { ($0.element.uniqueIdentifier, $0.offset) } + ) + + for (idx, banner) in self.currentPromoBanners.enumerated() { + let position = banner.position + Self.promoBannersSubmodulesOrderOffset + orderMap[banner.uniqueIdentifier] = position + idx + + let submodulesToShift = Set(Self.submodulesOrder.suffix(from: position).map(\.uniqueIdentifier)) + for (key, value) in orderMap where submodulesToShift.contains(key) { + orderMap[key] = value + 1 + } + } + + self.submodulesOrderMap = orderMap + } + private func initLanguageDependentSubmodules(contentLanguage: ContentLanguage) { // Stories let shouldRefreshStories = self.currentStoriesSubmoduleState == .shown @@ -327,20 +360,17 @@ final class ExploreViewController: BaseExploreViewController { } private func registerPromoBanner(_ promoBanner: PromoBanner) { - var updatedPromoBanner = promoBanner - updatedPromoBanner.position += 2 - - if let module = self.getSubmodule(type: updatedPromoBanner) { + if let module = self.getSubmodule(type: promoBanner) { self.removeSubmodule(module) } - guard let colorType = updatedPromoBanner.colorType else { + guard let colorType = promoBanner.colorType else { return } let view = PromoBannerView() - view.title = updatedPromoBanner.title - view.subtitle = updatedPromoBanner.description + view.title = promoBanner.title + view.subtitle = promoBanner.description view.style = .init(colorType: colorType) view.onClick = { [weak self] in guard let strongSelf = self else { @@ -380,7 +410,7 @@ final class ExploreViewController: BaseExploreViewController { viewController: nil, view: containerView, isLanguageDependent: true, - type: updatedPromoBanner + type: promoBanner ) ) @@ -388,14 +418,7 @@ final class ExploreViewController: BaseExploreViewController { } } -extension Explore.Submodule: SubmoduleType { - var position: Int { - guard let position = ExploreViewController.submodulesOrder.firstIndex(of: self) else { - fatalError("Given submodule type has unknown position") - } - return position - } -} +extension Explore.Submodule: ExploreSubmoduleType {} // MARK: - ExploreViewController: ExploreViewControllerProtocol - diff --git a/Stepic/Sources/Modules/Home/HomeViewController.swift b/Stepic/Sources/Modules/Home/HomeViewController.swift index bc5cd77b3e..2d81638f1e 100644 --- a/Stepic/Sources/Modules/Home/HomeViewController.swift +++ b/Stepic/Sources/Modules/Home/HomeViewController.swift @@ -22,9 +22,12 @@ final class HomeViewController: BaseExploreViewController { .visitedCourses, .popularCourses ] + private static let promoBannersSubmodulesOrderOffset = 1 + private var submodulesOrderMap: [UniqueIdentifierType: Int] = [:] private var lastContentLanguage: ContentLanguage? private var lastIsAuthorizedFlag = false + private var lastPromoBanners = [PromoBanner]() private var currentEnrolledCourseListState: EnrolledCourseListState? private var currentReviewsAndWishlistState: ReviewsAndWishlistState? @@ -38,6 +41,7 @@ final class HomeViewController: BaseExploreViewController { super.init(interactor: interactor, analytics: analytics) self.title = NSLocalizedString("Home", comment: "") + self.buildSubmodulesOrderMap() } @available(*, unavailable) @@ -87,6 +91,28 @@ final class HomeViewController: BaseExploreViewController { self.homeInteractor?.doContentLoad(request: .init()) } + override func getSubmodulePosition(type: ExploreSubmoduleType) -> Int { + self.submodulesOrderMap[type.uniqueIdentifier] ?? 0 + } + + private func buildSubmodulesOrderMap() { + var orderMap = Dictionary( + uniqueKeysWithValues: Self.submodulesOrder.enumerated().map { ($0.element.uniqueIdentifier, $0.offset) } + ) + + for banner in self.lastPromoBanners { + let position = banner.position + Self.promoBannersSubmodulesOrderOffset + orderMap[banner.uniqueIdentifier] = position + + let submodulesToShift = Set(Self.submodulesOrder.suffix(from: position).map(\.uniqueIdentifier)) + for (key, value) in orderMap where submodulesToShift.contains(key) { + orderMap[key] = value + 1 + } + } + + self.submodulesOrderMap = orderMap + } + // MARK: - Streak activity private enum StreakActivityState { @@ -534,14 +560,13 @@ final class HomeViewController: BaseExploreViewController { } var headerViewInsets = ExploreBlockContainerView.Appearance().headerViewInsets - if promoBanner.position > 0 { - headerViewInsets.top = 0 - } var contentViewInsets = CourseListContainerViewFactory.Appearance.horizontalContentInsets contentViewInsets.left = headerViewInsets.left contentViewInsets.right = headerViewInsets.right + headerViewInsets.top = contentViewInsets.bottom + let containerView = CourseListContainerViewFactory(colorMode: .light) .makeHorizontalContainerView( for: view, @@ -613,11 +638,15 @@ extension HomeViewController: HomeViewControllerProtocol { strongSelf.lastContentLanguage = viewModel.contentLanguage strongSelf.lastIsAuthorizedFlag = viewModel.isAuthorized + strongSelf.lastPromoBanners = viewModel.promoBanners + + strongSelf.buildSubmodulesOrderMap() let shouldDisplayContinueCourse = viewModel.isAuthorized let shouldDisplayAnonymousPlaceholder = !viewModel.isAuthorized let shouldDisplayReviewsAndWishlist = viewModel.isAuthorized + strongSelf.removeLanguageDependentSubmodules() strongSelf.refreshContinueCourse(state: shouldDisplayContinueCourse ? .shown : .hidden) strongSelf.refreshStateForEnrolledCourses(state: shouldDisplayAnonymousPlaceholder ? .anonymous : .normal) strongSelf.refreshReviewsAndWishlist(state: shouldDisplayReviewsAndWishlist ? .shown : .hidden) @@ -641,15 +670,8 @@ extension HomeViewController: BaseExploreViewDelegate { } } -extension Home.Submodule: SubmoduleType { - var position: Int { - guard let position = HomeViewController.submodulesOrder.firstIndex(of: self) else { - fatalError("Given submodule type has unknown position") - } - return position - } -} +extension Home.Submodule: ExploreSubmoduleType {} -extension PromoBanner: SubmoduleType { +extension PromoBanner: ExploreSubmoduleType { var uniqueIdentifier: UniqueIdentifierType { "promoBanner\(self.position)" } } diff --git a/Stepic/Sources/Modules/NewProfile/NewProfileViewController.swift b/Stepic/Sources/Modules/NewProfile/NewProfileViewController.swift index 065343e6f7..cbf5e56205 100644 --- a/Stepic/Sources/Modules/NewProfile/NewProfileViewController.swift +++ b/Stepic/Sources/Modules/NewProfile/NewProfileViewController.swift @@ -13,6 +13,10 @@ protocol NewProfileViewControllerProtocol: AnyObject { func displayRefreshControl(response: NewProfile.RefreshControlUpdate.ViewModel) } +protocol NewProfileSubmoduleType: UniqueIdentifiable { + var position: Int { get } +} + final class NewProfileViewController: UIViewController, ControllerWithStepikPlaceholder { enum Animation { static let startRefreshDelay: TimeInterval = 1.0 @@ -342,7 +346,7 @@ final class NewProfileViewController: UIViewController, ControllerWithStepikPlac self.submodules = self.submodules.filter { submodule.view != $0.view } } - private func getSubmodule(type: SubmoduleType) -> Submodule? { + private func getSubmodule(type: NewProfileSubmoduleType) -> Submodule? { self.submodules.first(where: { $0.type.uniqueIdentifier == type.uniqueIdentifier }) } @@ -697,9 +701,9 @@ final class NewProfileViewController: UIViewController, ControllerWithStepikPlac weak var viewController: UIViewController? weak var view: UIView? - let type: SubmoduleType + let type: NewProfileSubmoduleType - init(viewController: UIViewController?, view: UIView?, type: SubmoduleType) { + init(viewController: UIViewController?, view: UIView?, type: NewProfileSubmoduleType) { self.viewController = viewController self.view = view self.type = type @@ -806,7 +810,7 @@ extension NewProfileViewController: NewProfileViewControllerProtocol { // MARK: - NewProfile.Submodule: SubmoduleType - -extension NewProfile.Submodule: SubmoduleType { +extension NewProfile.Submodule: NewProfileSubmoduleType { var position: Int { guard let position = NewProfileViewController.submodulesOrder.firstIndex(of: self) else { fatalError("Given submodule type has unknown position") diff --git a/Stepic/Sources/Views/PromoBannerView.swift b/Stepic/Sources/Views/PromoBannerView.swift index f1e4c06c54..614e68e0af 100644 --- a/Stepic/Sources/Views/PromoBannerView.swift +++ b/Stepic/Sources/Views/PromoBannerView.swift @@ -8,7 +8,7 @@ extension PromoBannerView { let shadowColor = UIColor.black let shadowOffset = CGSize(width: 0, height: 1) let shadowRadius: CGFloat = 4.0 - let shadowOpacity: Float = 0.05 + let shadowOpacity: Float = 0.1 let titleLabelFont = UIFont.systemFont(ofSize: 18, weight: .bold) let titleLabelInsets = LayoutInsets(top: 16, left: 16) @@ -41,6 +41,8 @@ final class PromoBannerView: UIControl { return imageView }() + private lazy var backgroundColorView = UIView() + var title: String? { didSet { self.titleLabel.text = self.title @@ -103,6 +105,9 @@ final class PromoBannerView: UIControl { self.layer.cornerRadius = self.appearance.cornerRadius self.layer.masksToBounds = true + self.backgroundColorView.layer.cornerRadius = self.appearance.cornerRadius + self.backgroundColorView.clipsToBounds = true + self.layer.shadowColor = self.appearance.shadowColor.cgColor self.layer.shadowOffset = self.appearance.shadowOffset self.layer.shadowRadius = self.appearance.shadowRadius @@ -117,7 +122,9 @@ final class PromoBannerView: UIControl { // MARK: Private API private func updateStyle() { - self.backgroundColor = self.style.backgroundColor + self.backgroundColor = self.style == .green ? .white : .clear + self.backgroundColorView.backgroundColor = self.style.backgroundColor + self.illustrationImageView.image = self.style.illustrationImage self.titleLabel.textColor = self.style.titleLabelTextColor @@ -139,24 +146,22 @@ final class PromoBannerView: UIControl { fileprivate var backgroundColor: UIColor { switch self { case .blue: - return .dynamic(light: .stepikOverlayDarkBlueFixed, dark: .stepikOverlayBlueBackground) + return .stepikOverlayDarkBlueFixed case .green: return .stepikOverlayGreenBackground case .violet: - return .dynamic(light: .stepikViolet05Fixed, dark: .stepikViolet05Fixed.withAlphaComponent(0.12)) + return .stepikViolet05Fixed } } - fileprivate var titleLabelTextColor: UIColor { - .dynamic(light: .black, dark: .stepikMaterialPrimaryText) - } + fileprivate var titleLabelTextColor: UIColor { .black } fileprivate var subtitleLabelTextColor: UIColor { switch self { case .blue, .violet: - return .dynamic(light: .white, dark: .stepikMaterialSecondaryText) + return .white case .green: - return .stepikMaterialSecondaryText + return .black.withAlphaComponent(0.6) } } @@ -185,12 +190,16 @@ extension PromoBannerView: ProgrammaticallyInitializableViewProtocol { } func addSubviews() { + self.addSubview(self.backgroundColorView) self.addSubview(self.illustrationImageView) self.addSubview(self.titleLabel) self.addSubview(self.subtitleLabel) } func makeConstraints() { + self.backgroundColorView.translatesAutoresizingMaskIntoConstraints = false + self.backgroundColorView.snp.makeConstraints { $0.edges.equalToSuperview() } + self.titleLabel.translatesAutoresizingMaskIntoConstraints = false self.titleLabel.snp.makeConstraints { make in make.top.leading.equalToSuperview().inset(self.appearance.titleLabelInsets.edgeInsets) From 347cead5a26c2f51c94ef04eaa98ebd046533ea9 Mon Sep 17 00:00:00 2001 From: Ivan Magda Date: Wed, 16 Feb 2022 23:28:44 +0300 Subject: [PATCH 10/13] Bump build --- Stepic.xcodeproj/project.pbxproj | 24 +++++++++++----------- Stepic/Info-Develop.plist | 2 +- Stepic/Info-Production.plist | 2 +- Stepic/Info-Release.plist | 2 +- StepicTests/Info-Develop.plist | 2 +- StepicTests/Info-Production.plist | 2 +- StepicTests/Info-Release.plist | 2 +- StepicUITests/Info-Develop.plist | 2 +- StepicUITests/Info-Production.plist | 2 +- StepicUITests/Info-Release.plist | 2 +- StepicWidget/Info-Develop.plist | 2 +- StepicWidget/Info-Production.plist | 2 +- StepicWidget/Info-Release.plist | 2 +- StickerPackExtension/Info-Develop.plist | 2 +- StickerPackExtension/Info-Production.plist | 2 +- StickerPackExtension/Info-Release.plist | 2 +- 16 files changed, 27 insertions(+), 27 deletions(-) diff --git a/Stepic.xcodeproj/project.pbxproj b/Stepic.xcodeproj/project.pbxproj index b3342c3b3b..f61cec3a88 100644 --- a/Stepic.xcodeproj/project.pbxproj +++ b/Stepic.xcodeproj/project.pbxproj @@ -13064,7 +13064,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 413; + CURRENT_PROJECT_VERSION = 414; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = "StickerPackExtension/Info-Production.plist"; @@ -13089,7 +13089,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 413; + CURRENT_PROJECT_VERSION = 414; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = "StickerPackExtension/Info-Production.plist"; IPHONEOS_DEPLOYMENT_TARGET = 11.0; @@ -13231,7 +13231,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 413; + CURRENT_PROJECT_VERSION = 414; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_BITCODE = YES; INFOPLIST_FILE = "Stepic/Info-Production.plist"; @@ -13261,7 +13261,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 413; + CURRENT_PROJECT_VERSION = 414; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_BITCODE = YES; "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64; @@ -13352,7 +13352,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 413; + CURRENT_PROJECT_VERSION = 414; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_BITCODE = YES; INFOPLIST_FILE = "Stepic/Info-Develop.plist"; @@ -13404,7 +13404,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 413; + CURRENT_PROJECT_VERSION = 414; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = "StickerPackExtension/Info-Develop.plist"; @@ -13485,7 +13485,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 413; + CURRENT_PROJECT_VERSION = 414; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_BITCODE = YES; INFOPLIST_FILE = "Stepic/Info-Develop.plist"; @@ -13533,7 +13533,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 413; + CURRENT_PROJECT_VERSION = 414; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = "StickerPackExtension/Info-Develop.plist"; IPHONEOS_DEPLOYMENT_TARGET = 11.0; @@ -14053,7 +14053,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 413; + CURRENT_PROJECT_VERSION = 414; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_BITCODE = YES; INFOPLIST_FILE = "Stepic/Info-Release.plist"; @@ -14107,7 +14107,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 413; + CURRENT_PROJECT_VERSION = 414; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = "StickerPackExtension/Info-Release.plist"; @@ -14189,7 +14189,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 413; + CURRENT_PROJECT_VERSION = 414; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_BITCODE = YES; INFOPLIST_FILE = "Stepic/Info-Release.plist"; @@ -14237,7 +14237,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 413; + CURRENT_PROJECT_VERSION = 414; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = "StickerPackExtension/Info-Release.plist"; IPHONEOS_DEPLOYMENT_TARGET = 11.0; diff --git a/Stepic/Info-Develop.plist b/Stepic/Info-Develop.plist index 61d5e4a0f1..1445e44a56 100644 --- a/Stepic/Info-Develop.plist +++ b/Stepic/Info-Develop.plist @@ -62,7 +62,7 @@ CFBundleVersion - 413 + 414 FacebookAppID 171127739724012 FacebookDisplayName diff --git a/Stepic/Info-Production.plist b/Stepic/Info-Production.plist index b58a6c864c..65b4f3b5c0 100644 --- a/Stepic/Info-Production.plist +++ b/Stepic/Info-Production.plist @@ -62,7 +62,7 @@ CFBundleVersion - 413 + 414 FacebookAppID 171127739724012 FacebookDisplayName diff --git a/Stepic/Info-Release.plist b/Stepic/Info-Release.plist index c20844640c..994cadb76e 100644 --- a/Stepic/Info-Release.plist +++ b/Stepic/Info-Release.plist @@ -62,7 +62,7 @@ CFBundleVersion - 413 + 414 FacebookAppID 171127739724012 FacebookDisplayName diff --git a/StepicTests/Info-Develop.plist b/StepicTests/Info-Develop.plist index 8675a1a83b..7517d039a8 100644 --- a/StepicTests/Info-Develop.plist +++ b/StepicTests/Info-Develop.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 413 + 414 diff --git a/StepicTests/Info-Production.plist b/StepicTests/Info-Production.plist index f945ce07ca..b15e1a1a40 100644 --- a/StepicTests/Info-Production.plist +++ b/StepicTests/Info-Production.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 413 + 414 diff --git a/StepicTests/Info-Release.plist b/StepicTests/Info-Release.plist index 3e131a3c07..27192c5779 100644 --- a/StepicTests/Info-Release.plist +++ b/StepicTests/Info-Release.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 413 + 414 diff --git a/StepicUITests/Info-Develop.plist b/StepicUITests/Info-Develop.plist index 4120aa3b0e..273d7026b4 100644 --- a/StepicUITests/Info-Develop.plist +++ b/StepicUITests/Info-Develop.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 1.210-develop CFBundleVersion - 413 + 414 diff --git a/StepicUITests/Info-Production.plist b/StepicUITests/Info-Production.plist index 4d82938cd8..9d023fcb34 100644 --- a/StepicUITests/Info-Production.plist +++ b/StepicUITests/Info-Production.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 1.210 CFBundleVersion - 413 + 414 diff --git a/StepicUITests/Info-Release.plist b/StepicUITests/Info-Release.plist index 581b9429ba..613db5d5c9 100644 --- a/StepicUITests/Info-Release.plist +++ b/StepicUITests/Info-Release.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 1.210-release CFBundleVersion - 413 + 414 diff --git a/StepicWidget/Info-Develop.plist b/StepicWidget/Info-Develop.plist index ac5921b9c1..a18f668d9c 100644 --- a/StepicWidget/Info-Develop.plist +++ b/StepicWidget/Info-Develop.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 1.210-develop CFBundleVersion - 413 + 414 NSExtension NSExtensionPointIdentifier diff --git a/StepicWidget/Info-Production.plist b/StepicWidget/Info-Production.plist index ee5d10fae6..ecad5b6bfa 100644 --- a/StepicWidget/Info-Production.plist +++ b/StepicWidget/Info-Production.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 1.210 CFBundleVersion - 413 + 414 NSExtension NSExtensionPointIdentifier diff --git a/StepicWidget/Info-Release.plist b/StepicWidget/Info-Release.plist index 66af63186f..0984fdc075 100644 --- a/StepicWidget/Info-Release.plist +++ b/StepicWidget/Info-Release.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 1.210-release CFBundleVersion - 413 + 414 NSExtension NSExtensionPointIdentifier diff --git a/StickerPackExtension/Info-Develop.plist b/StickerPackExtension/Info-Develop.plist index 86ebc40405..47fbeb8019 100644 --- a/StickerPackExtension/Info-Develop.plist +++ b/StickerPackExtension/Info-Develop.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 1.210-develop CFBundleVersion - 413 + 414 UIRequiredDeviceCapabilities arm64 diff --git a/StickerPackExtension/Info-Production.plist b/StickerPackExtension/Info-Production.plist index b9d5af6268..926a9a07e3 100644 --- a/StickerPackExtension/Info-Production.plist +++ b/StickerPackExtension/Info-Production.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 1.210 CFBundleVersion - 413 + 414 UIRequiredDeviceCapabilities arm64 diff --git a/StickerPackExtension/Info-Release.plist b/StickerPackExtension/Info-Release.plist index 1c1b9bc6bd..1cbd5b16f2 100644 --- a/StickerPackExtension/Info-Release.plist +++ b/StickerPackExtension/Info-Release.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 1.210-release CFBundleVersion - 413 + 414 UIRequiredDeviceCapabilities arm64 From 5944986fc8a016f7938e8b02f638d48e10808881 Mon Sep 17 00:00:00 2001 From: Ivan Magda Date: Wed, 16 Feb 2022 23:38:45 +0300 Subject: [PATCH 11/13] Return unique promo banners --- Stepic/Sources/Services/PromoBannersService.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Stepic/Sources/Services/PromoBannersService.swift b/Stepic/Sources/Services/PromoBannersService.swift index f35cee0012..1da88e4602 100644 --- a/Stepic/Sources/Services/PromoBannersService.swift +++ b/Stepic/Sources/Services/PromoBannersService.swift @@ -6,7 +6,13 @@ protocol PromoBannersServiceProtocol: AnyObject { extension PromoBannersServiceProtocol { func getPromoBanners(language: ContentLanguage, screen: PromoBanner.ScreenType) -> [PromoBanner] { - self.getPromoBanners().filter { $0.lang == language.languageString && $0.screenType == screen } + var seen: Set = [] + return self.getPromoBanners() + .filter { banner in + banner.lang == language.languageString && + banner.screenType == screen && + seen.insert(banner.position).inserted + } } } From aa4a0d968bd5c6f581080e8121fa7e4849bd9a25 Mon Sep 17 00:00:00 2001 From: Ivan Magda Date: Thu, 17 Feb 2022 14:12:58 +0300 Subject: [PATCH 12/13] Add fixes APPS-3588, APPS-3602 --- Stepic/Sources/Modules/Explore/ExploreViewController.swift | 4 ++++ Stepic/Sources/Modules/Home/HomeViewController.swift | 4 ++++ Stepic/en.lproj/Localizable.strings | 4 ++-- Stepic/ru.lproj/Localizable.strings | 2 +- 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/Stepic/Sources/Modules/Explore/ExploreViewController.swift b/Stepic/Sources/Modules/Explore/ExploreViewController.swift index 277a4349e9..abf71748b4 100644 --- a/Stepic/Sources/Modules/Explore/ExploreViewController.swift +++ b/Stepic/Sources/Modules/Explore/ExploreViewController.swift @@ -173,6 +173,10 @@ final class ExploreViewController: BaseExploreViewController { let position = banner.position + Self.promoBannersSubmodulesOrderOffset orderMap[banner.uniqueIdentifier] = position + idx + guard 0.. Date: Thu, 17 Feb 2022 14:13:14 +0300 Subject: [PATCH 13/13] Bump build --- Stepic.xcodeproj/project.pbxproj | 24 +++++++++++----------- Stepic/Info-Develop.plist | 2 +- Stepic/Info-Production.plist | 2 +- Stepic/Info-Release.plist | 2 +- StepicTests/Info-Develop.plist | 2 +- StepicTests/Info-Production.plist | 2 +- StepicTests/Info-Release.plist | 2 +- StepicUITests/Info-Develop.plist | 2 +- StepicUITests/Info-Production.plist | 2 +- StepicUITests/Info-Release.plist | 2 +- StepicWidget/Info-Develop.plist | 2 +- StepicWidget/Info-Production.plist | 2 +- StepicWidget/Info-Release.plist | 2 +- StickerPackExtension/Info-Develop.plist | 2 +- StickerPackExtension/Info-Production.plist | 2 +- StickerPackExtension/Info-Release.plist | 2 +- 16 files changed, 27 insertions(+), 27 deletions(-) diff --git a/Stepic.xcodeproj/project.pbxproj b/Stepic.xcodeproj/project.pbxproj index f61cec3a88..e9d8e1d89c 100644 --- a/Stepic.xcodeproj/project.pbxproj +++ b/Stepic.xcodeproj/project.pbxproj @@ -13064,7 +13064,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 414; + CURRENT_PROJECT_VERSION = 415; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = "StickerPackExtension/Info-Production.plist"; @@ -13089,7 +13089,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 414; + CURRENT_PROJECT_VERSION = 415; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = "StickerPackExtension/Info-Production.plist"; IPHONEOS_DEPLOYMENT_TARGET = 11.0; @@ -13231,7 +13231,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 414; + CURRENT_PROJECT_VERSION = 415; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_BITCODE = YES; INFOPLIST_FILE = "Stepic/Info-Production.plist"; @@ -13261,7 +13261,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 414; + CURRENT_PROJECT_VERSION = 415; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_BITCODE = YES; "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64; @@ -13352,7 +13352,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 414; + CURRENT_PROJECT_VERSION = 415; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_BITCODE = YES; INFOPLIST_FILE = "Stepic/Info-Develop.plist"; @@ -13404,7 +13404,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 414; + CURRENT_PROJECT_VERSION = 415; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = "StickerPackExtension/Info-Develop.plist"; @@ -13485,7 +13485,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 414; + CURRENT_PROJECT_VERSION = 415; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_BITCODE = YES; INFOPLIST_FILE = "Stepic/Info-Develop.plist"; @@ -13533,7 +13533,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 414; + CURRENT_PROJECT_VERSION = 415; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = "StickerPackExtension/Info-Develop.plist"; IPHONEOS_DEPLOYMENT_TARGET = 11.0; @@ -14053,7 +14053,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 414; + CURRENT_PROJECT_VERSION = 415; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_BITCODE = YES; INFOPLIST_FILE = "Stepic/Info-Release.plist"; @@ -14107,7 +14107,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 414; + CURRENT_PROJECT_VERSION = 415; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = "StickerPackExtension/Info-Release.plist"; @@ -14189,7 +14189,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 414; + CURRENT_PROJECT_VERSION = 415; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_BITCODE = YES; INFOPLIST_FILE = "Stepic/Info-Release.plist"; @@ -14237,7 +14237,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 414; + CURRENT_PROJECT_VERSION = 415; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = "StickerPackExtension/Info-Release.plist"; IPHONEOS_DEPLOYMENT_TARGET = 11.0; diff --git a/Stepic/Info-Develop.plist b/Stepic/Info-Develop.plist index 1445e44a56..2e188beae1 100644 --- a/Stepic/Info-Develop.plist +++ b/Stepic/Info-Develop.plist @@ -62,7 +62,7 @@ CFBundleVersion - 414 + 415 FacebookAppID 171127739724012 FacebookDisplayName diff --git a/Stepic/Info-Production.plist b/Stepic/Info-Production.plist index 65b4f3b5c0..127dd990a9 100644 --- a/Stepic/Info-Production.plist +++ b/Stepic/Info-Production.plist @@ -62,7 +62,7 @@ CFBundleVersion - 414 + 415 FacebookAppID 171127739724012 FacebookDisplayName diff --git a/Stepic/Info-Release.plist b/Stepic/Info-Release.plist index 994cadb76e..6f0809a95a 100644 --- a/Stepic/Info-Release.plist +++ b/Stepic/Info-Release.plist @@ -62,7 +62,7 @@ CFBundleVersion - 414 + 415 FacebookAppID 171127739724012 FacebookDisplayName diff --git a/StepicTests/Info-Develop.plist b/StepicTests/Info-Develop.plist index 7517d039a8..7576e544e2 100644 --- a/StepicTests/Info-Develop.plist +++ b/StepicTests/Info-Develop.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 414 + 415 diff --git a/StepicTests/Info-Production.plist b/StepicTests/Info-Production.plist index b15e1a1a40..1ff8f14259 100644 --- a/StepicTests/Info-Production.plist +++ b/StepicTests/Info-Production.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 414 + 415 diff --git a/StepicTests/Info-Release.plist b/StepicTests/Info-Release.plist index 27192c5779..ca531226d7 100644 --- a/StepicTests/Info-Release.plist +++ b/StepicTests/Info-Release.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 414 + 415 diff --git a/StepicUITests/Info-Develop.plist b/StepicUITests/Info-Develop.plist index 273d7026b4..5a6c60c795 100644 --- a/StepicUITests/Info-Develop.plist +++ b/StepicUITests/Info-Develop.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 1.210-develop CFBundleVersion - 414 + 415 diff --git a/StepicUITests/Info-Production.plist b/StepicUITests/Info-Production.plist index 9d023fcb34..070b947b8d 100644 --- a/StepicUITests/Info-Production.plist +++ b/StepicUITests/Info-Production.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 1.210 CFBundleVersion - 414 + 415 diff --git a/StepicUITests/Info-Release.plist b/StepicUITests/Info-Release.plist index 613db5d5c9..b6d3d897d5 100644 --- a/StepicUITests/Info-Release.plist +++ b/StepicUITests/Info-Release.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 1.210-release CFBundleVersion - 414 + 415 diff --git a/StepicWidget/Info-Develop.plist b/StepicWidget/Info-Develop.plist index a18f668d9c..8b18693680 100644 --- a/StepicWidget/Info-Develop.plist +++ b/StepicWidget/Info-Develop.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 1.210-develop CFBundleVersion - 414 + 415 NSExtension NSExtensionPointIdentifier diff --git a/StepicWidget/Info-Production.plist b/StepicWidget/Info-Production.plist index ecad5b6bfa..8729b1e92f 100644 --- a/StepicWidget/Info-Production.plist +++ b/StepicWidget/Info-Production.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 1.210 CFBundleVersion - 414 + 415 NSExtension NSExtensionPointIdentifier diff --git a/StepicWidget/Info-Release.plist b/StepicWidget/Info-Release.plist index 0984fdc075..9c946f88d8 100644 --- a/StepicWidget/Info-Release.plist +++ b/StepicWidget/Info-Release.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 1.210-release CFBundleVersion - 414 + 415 NSExtension NSExtensionPointIdentifier diff --git a/StickerPackExtension/Info-Develop.plist b/StickerPackExtension/Info-Develop.plist index 47fbeb8019..fd39ed0961 100644 --- a/StickerPackExtension/Info-Develop.plist +++ b/StickerPackExtension/Info-Develop.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 1.210-develop CFBundleVersion - 414 + 415 UIRequiredDeviceCapabilities arm64 diff --git a/StickerPackExtension/Info-Production.plist b/StickerPackExtension/Info-Production.plist index 926a9a07e3..17af945068 100644 --- a/StickerPackExtension/Info-Production.plist +++ b/StickerPackExtension/Info-Production.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 1.210 CFBundleVersion - 414 + 415 UIRequiredDeviceCapabilities arm64 diff --git a/StickerPackExtension/Info-Release.plist b/StickerPackExtension/Info-Release.plist index 1cbd5b16f2..89418d4914 100644 --- a/StickerPackExtension/Info-Release.plist +++ b/StickerPackExtension/Info-Release.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 1.210-release CFBundleVersion - 414 + 415 UIRequiredDeviceCapabilities arm64